Cerramos el módulo 8 con una plataforma segura y auditable, pero con una confesión incómoda: los componentes de Rutas Norte siguen teniendo un número de réplicas fijo, escrito a mano. api-reservas tiene replicas: 4 y tienda-web tiene replicas: 3 porque alguien, hace meses, midió un martes cualquiera y le pareció suficiente. Ese número no sabe nada del puente de mayo. El año pasado la plataforma se cayó a los once minutos de abrir la venta, y la respuesta del equipo fue un kubectl scale a la carrera desde un portátil, en el andén de una estación, con el tren saliendo.
Esta lección elimina esa carrera. El HorizontalPodAutoscaler (HPA) es el objeto de Kubernetes que ajusta automáticamente el número de réplicas de un Deployment en función de métricas observadas. No es magia: es un controlador más, con su bucle de reconciliación —el mismo que estudiamos en el módulo 1—, una fórmula aritmética muy concreta y un puñado de parámetros que hay que entender bien para que el resultado sea estabilidad y no un péndulo de pods arrancando y muriendo.
Vamos a ver qué cargas se pueden escalar horizontalmente y cuáles no, la API autoscaling/v2 completa, la fórmula exacta con la que se calculan las réplicas deseadas, el bloque behavior que gobierna la velocidad de subida y bajada, y los dos manifiestos definitivos de api-reservas y tienda-web. Terminaremos con un conflicto que muerde a casi todos los equipos la primera vez: el HPA y el campo replicas del YAML versionado peleándose por el mismo número.
Contenido
- Escalar hacia fuera frente a escalar hacia arriba
- Qué cargas admiten escalado horizontal y cuáles no
- Anatomía del HorizontalPodAutoscaler en
autoscaling/v2 - Los cuatro tipos de métrica:
Resource,Pods,ObjectyExternal - La fórmula exacta del controlador, paso a paso
- La banda de tolerancia y el bailoteo de réplicas
- Requisitos ineludibles:
requestsy metrics-server - Diagnóstico del temido
<unknown> - El bloque
behavior: políticas, periodos y ventana de estabilización - Varias métricas a la vez: gana la que pide más
- Los HPA de Rutas Norte, manifiestos completos
- Simulación de una punta de carga y observación en vivo
- El conflicto entre el HPA y el campo
replicas - Qué no escalar con HPA y el cuello de botella real
- Errores comunes y consejos
- Ejercicios
- Conclusión
- Escalar hacia fuera frente a escalar hacia arriba
Hay exactamente dos formas de darle más capacidad a un servicio, y conviene tener los nombres claros porque el resto del módulo se apoya en ellos.
Escalar hacia arriba (scale up, escalado vertical) es darle más recursos a cada instancia: subir el requests/limits de CPU de 200m a 800m, la memoria de 256Mi a 1Gi. El número de pods no cambia; cada pod es más grande. En Kubernetes esto implica recrear el pod, porque los recursos de un contenedor forman parte de su especificación inmutable (con el matiz del redimensionado en caliente que veremos en 09-02).
Escalar hacia fuera (scale out, escalado horizontal) es poner más instancias del mismo tamaño: pasar de 4 pods de api-reservas a 12 pods idénticos. Cada pod sigue siendo igual de grande; hay más. En Kubernetes esto es cambiar un número entero: el replicas del Deployment.
| Aspecto | Escalado vertical (hacia arriba) | Escalado horizontal (hacia fuera) |
|---|---|---|
| Qué cambia | Tamaño de cada pod (requests/limits) |
Número de pods |
| Techo | El nodo más grande disponible | Prácticamente ilimitado (con nodos) |
| Requiere reiniciar el pod | Sí (salvo redimensionado en caliente) | No, los pods existentes no se tocan |
| Requisito de la aplicación | Que sepa aprovechar más CPU/RAM | Que no tenga estado local ni afinidad de sesión |
| Tolerancia a fallos | No mejora: sigue habiendo un punto único | Mejora: la carga se reparte entre más instancias |
| Coste de granularidad | Salta a escalones grandes | Fino: un pod cada vez |
| Objeto de Kubernetes | VPA (09-02) o edición manual | HPA (esta lección), KEDA (09-04) |
| Velocidad de reacción | Lenta (recreación del pod) | Rápida (segundos, si la imagen está cacheada) |
La regla práctica: el escalado vertical resuelve el problema de una instancia mal dimensionada; el escalado horizontal resuelve el problema del volumen de tráfico. No son alternativas, son ejes distintos. Rutas Norte necesita los dos: acertar con el tamaño de cada pod (09-02) y multiplicar el número de pods cuando llega el puente de mayo (esta lección).
Un detalle importante que muchos pasan por alto: el escalado horizontal también mejora la disponibilidad, mientras que el vertical no. Doce pods de api-reservas repartidos entre nodos sobreviven a la caída de un nodo; un único pod gigantesco, no. Esta idea la retomaremos a fondo en 09-05.
- Qué cargas admiten escalado horizontal y cuáles no
El escalado horizontal solo funciona si cualquier réplica puede atender cualquier petición. Esto se llama ser sin estado (stateless), y es una propiedad de la aplicación, no de Kubernetes. Kubernetes te dejará escalar cualquier cosa; que el resultado sea correcto depende de cómo esté escrita la aplicación.
Repasemos los componentes de Rutas Norte:
| Componente | ¿Escalable horizontalmente? | Motivo |
|---|---|---|
tienda-web (nginx) |
Sí, sin reservas | Sirve activos estáticos y hace de proxy. No guarda nada entre peticiones. |
api-reservas (Node.js) |
Sí | API REST sin estado: la sesión va en un token firmado, el estado en PostgreSQL y redis-cache. |
worker-notificaciones |
Sí, pero la CPU no es la señal correcta | Consume de una cola; añadir consumidores acelera el vaciado. La métrica útil es la longitud de la cola (09-04). |
redis-cache |
No con HPA | Es una caché con estado particionado. Añadir réplicas no reparte la carga sin sharding explícito. |
postgres-reservas |
Rotundamente no | Base de datos primaria. Añadir pods no crea más bases de datos: crea réplicas que se pelean por el mismo volumen o quedan huérfanas. |
informes-ocupacion (CronJob) |
No aplica | Su paralelismo se controla con parallelism en el Job (06-03), no con HPA. |
El caso de postgres-reservas merece un párrafo. Es un StatefulSet con un volumen persistente por réplica. Si pusieras un HPA sobre él y el HPA decidiera pasar de 1 a 5 réplicas, Kubernetes crearía cinco pods postgres-reservas-0 a postgres-reservas-4, cada uno con su propio PVC vacío, cada uno arrancando una base de datos independiente y vacía. El Service repartiría las peticiones entre las cinco. El resultado sería una corrupción silenciosa de datos: unas reservas irían a una base de datos y otras a otra. Escalar una base de datos relacional es un problema de arquitectura de datos (réplicas de lectura, particionado, un operador que sepa hacerlo — 06-07), no un problema de contar pods.
Regla que conviene memorizar: si al añadir una réplica el sistema no responde más peticiones correctas por segundo, el HPA no es tu herramienta.
- Anatomía del HorizontalPodAutoscaler en
autoscaling/v2
autoscaling/v2El HPA es un objeto de la API como cualquier otro. Su versión estable y actual es autoscaling/v2; las versiones v2beta1 y v2beta2 están eliminadas desde hace varias versiones y autoscaling/v1 solo permite CPU (el servidor la sigue sirviendo por compatibilidad, pero no la uses: pierdes behavior y todas las métricas que no sean CPU).
Este es el esqueleto completo, con comentarios sobre cada campo:
apiVersion: autoscaling/v2 # Versión estable. NUNCA autoscaling/v1 en material nuevo.
kind: HorizontalPodAutoscaler
metadata:
name: api-reservas
namespace: rutas-norte-pro
spec:
# A QUÉ objeto le cambia el número de réplicas.
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment # También vale StatefulSet o cualquier recurso con subrecurso /scale
name: api-reservas # Debe existir en el MISMO namespace que el HPA
minReplicas: 4 # Suelo. El HPA nunca bajará de aquí.
maxReplicas: 30 # Techo. El HPA nunca subirá de aquí, pase lo que pase.
# QUÉ mira para decidir. Es una LISTA: puede haber varias.
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65 # Objetivo: 65 % del requests de CPU, de media entre pods
# CÓMO de rápido sube y baja. Opcional pero casi siempre necesario.
behavior:
scaleUp: {}
scaleDown: {}Tres puntos de partida que conviene fijar antes de seguir:
scaleTargetRefapunta a un objeto que expone el subrecurso/scale. Deployments, ReplicaSets y StatefulSets lo exponen. Un DaemonSet no, porque su número de pods lo determina el número de nodos (06-02), y no tendría sentido.minReplicasymaxReplicasson barreras duras. ElmaxReplicases tu red de seguridad económica y operativa: si un error de la aplicación dispara la CPU al 100 % sin que haya tráfico real, el HPA intentará escalar sin fin; el techo lo impide. Ponlo siempre, y ponlo pensando en qué pasaría si se alcanzase.minReplicaspuede ser 0 desde Kubernetes 1.30 si la feature gateHPAScaleToZeroestá activa, pero eso requiere una métrica externa o de objeto y no está habilitado por defecto en la mayoría de clústeres. El escalado a cero de verdad lo haremos con KEDA en 09-04.
El bucle del controlador
El HPA lo gestiona el horizontal-pod-autoscaler que vive dentro del kube-controller-manager. Su ciclo por defecto es de 15 segundos (--horizontal-pod-autoscaler-sync-period). En cada ciclo:
flowchart TD
A[Cada 15 s] --> B[Lee el HPA y el objeto destino]
B --> C[Consulta las metricas<br/>metrics.k8s.io o APIs de agregacion]
C --> D{Metricas disponibles?}
D -- No --> E[TARGETS = unknown<br/>no escala, emite evento]
D -- Si --> F[Aplica la formula<br/>replicas deseadas]
F --> G{Dentro de la<br/>banda de tolerancia?}
G -- Si --> H[No hace nada]
G -- No --> I[Aplica behavior:<br/>estabilizacion y politicas]
I --> J[Acota entre min y maxReplicas]
J --> K[PATCH al subrecurso /scale]
K --> L[El Deployment ajusta su ReplicaSet]
Nótese el final del flujo: el HPA no crea pods. Modifica el campo replicas del Deployment mediante el subrecurso /scale, y a partir de ahí el controlador de Deployments y el de ReplicaSets hacen su trabajo habitual (módulo 2). Es reconciliación encadenada, exactamente el patrón que vimos en 01-02.
- Los cuatro tipos de métrica:
Resource, Pods, Object y External
Resource, Pods, Object y ExternalEl bloque metrics acepta cuatro tipos. Dos los usarás hoy; los otros dos se definen aquí y se explotan en 09-04.
| Tipo | De dónde salen los datos | Qué mide | Ámbito | Se usa en |
|---|---|---|---|---|
Resource |
metrics-server (metrics.k8s.io) |
CPU o memoria de los pods del destino | Por pod, promediado | 09-01 |
ContainerResource |
metrics-server | CPU/memoria de un contenedor concreto del pod | Por contenedor | 09-01 |
Pods |
API de métricas personalizadas (custom.metrics.k8s.io) |
Cualquier métrica emitida por los pods del destino | Por pod, promediado | 09-04 |
Object |
API de métricas personalizadas | Una métrica asociada a otro objeto de Kubernetes (un Ingress, un Service) | Valor único | 09-04 |
External |
API de métricas externas (external.metrics.k8s.io) |
Algo de fuera del clúster: longitud de una cola, mensajes en un topic | Valor único o dividido entre pods | 09-04 |
Resource con Utilization
Es la forma más común. Expresa el objetivo como porcentaje del requests del contenedor:
Esto significa: «mantén el consumo medio de CPU de los pods de api-reservas en torno al 65 % de lo que cada uno tiene reservado en requests.cpu». Si requests.cpu es 500m, el objetivo son 325m por pod.
Es fundamental entender que Utilization se calcula sobre requests, no sobre limits ni sobre la CPU del nodo. Un pod puede mostrar una utilización del 180 % si consume 900m con un requests de 500m; eso es perfectamente posible porque el requests es una reserva mínima, no un tope (módulo 3).
Resource con AverageValue
Expresa el objetivo en unidades absolutas, ignorando el requests:
«Mantén el consumo medio de memoria por pod en 700Mi.»
¿Cuándo usar cada uno?
Utilization |
AverageValue |
|
|---|---|---|
| Se expresa en | Porcentaje del requests |
Unidades (m de CPU, Mi de memoria) |
Requiere requests declarado |
Sí, obligatorio | No |
Se rompe si cambias el requests |
Sí: el objetivo real se mueve solo | No |
| Legibilidad para el equipo | Alta («al 65 % de lo suyo») | Media |
| Recomendado para | CPU, el caso general | Memoria, o cuando el requests cambia a menudo (VPA) |
Un aviso sobre memoria como métrica de HPA: casi nunca es buena idea. Muchos entornos de ejecución (la JVM, el recolector de basura de Node.js, PostgreSQL) no devuelven la memoria al sistema aunque ya no la necesiten. El consumo sube y se queda arriba. Un HPA por memoria escalaría hacia fuera y nunca volvería a bajar, porque la memoria de los pods viejos jamás baja del objetivo. Usa CPU, o mejor aún, una métrica de negocio (09-04).
ContainerResource
Variante de Resource que mira un contenedor concreto en lugar de sumar todos los del pod. Es muy útil en Rutas Norte, donde varios pods llevan sidecars:
metrics:
- type: ContainerResource
containerResource:
name: cpu
container: api # Solo el contenedor "api", no el sidecar de metricas
target:
type: Utilization
averageUtilization: 65Sin esto, un sidecar que consume 50m constantes distorsiona el porcentaje del pod entero, y en pods pequeños la distorsión es grande. Si tu destino tiene sidecars, prefiere ContainerResource.
Pods, Object y External (definición)
Los definimos ahora para que reconozcas la sintaxis, pero su uso real llega en 09-04.
# Pods: una metrica que emiten los propios pods del destino, promediada entre ellos.
- type: Pods
pods:
metric:
name: peticiones_por_segundo
target:
type: AverageValue
averageValue: "80" # 80 peticiones por segundo y pod
# Object: una metrica asociada a OTRO objeto del cluster.
- type: Object
object:
describedObject:
apiVersion: networking.k8s.io/v1
kind: Ingress
name: rutas-norte-publico
metric:
name: peticiones_por_segundo
target:
type: Value
value: "2000" # Valor TOTAL del objeto, no por pod
# External: algo de fuera del cluster.
- type: External
external:
metric:
name: longitud_cola_correos
selector:
matchLabels:
cola: notificaciones-confirmacion
target:
type: AverageValue
averageValue: "500" # 500 mensajes pendientes por podDiferencia clave entre Value y AverageValue en Object y External: con Value el HPA compara el valor bruto con el objetivo; con AverageValue divide el valor entre el número de réplicas antes de comparar. Para una cola siempre quieres AverageValue («cada worker se hace cargo de 500 mensajes»), porque es lo que hace que el número de réplicas crezca con la cola.
Los tipos Pods, Object y External no funcionan de fábrica: necesitan un adaptador de métricas registrado en la capa de agregación de la API. Sin él, el HPA reportará <unknown> eternamente. Eso es exactamente lo que resuelve KEDA en 09-04.
- La fórmula exacta del controlador, paso a paso
Aquí está el corazón de la lección. Todo el comportamiento del HPA sale de una única expresión:
Donde techo() es el redondeo hacia arriba al entero siguiente. Vamos a aplicarla a api-reservas con números reales.
Situación de partida
api-reservas está desplegado con estos recursos (los fijamos en el módulo 3):
Y su HPA tiene averageUtilization: 65, minReplicas: 4, maxReplicas: 30.
Objetivo absoluto por pod: 65 % de 500m = 325m.
Caso 1: martes por la tarde, todo tranquilo
Hay 4 réplicas. kubectl top pods muestra:
NAME CPU(cores) MEMORY(bytes)
api-reservas-7c9d4f8b6d-2mk8p 140m 310Mi
api-reservas-7c9d4f8b6d-5xqzn 155m 298Mi
api-reservas-7c9d4f8b6d-9jw4t 132m 305Mi
api-reservas-7c9d4f8b6d-hb7rc 149m 301MiMedia: (140 + 155 + 132 + 149) / 4 = 144m.
La fórmula pide 2 réplicas. Pero minReplicas es 4, así que se queda en 4. Correcto: no queremos bajar de 4 en producción por disponibilidad.
Caso 2: apertura de la venta del puente de mayo
Sigue habiendo 4 réplicas, pero el tráfico se dispara:
NAME CPU(cores) MEMORY(bytes)
api-reservas-7c9d4f8b6d-2mk8p 910m 680Mi
api-reservas-7c9d4f8b6d-5xqzn 940m 702Mi
api-reservas-7c9d4f8b6d-9jw4t 895m 671Mi
api-reservas-7c9d4f8b6d-hb7rc 925m 694MiMedia: (910 + 940 + 895 + 925) / 4 = 917,5m. Ojo: están rozando su limit de 1000m, o sea, están sufriendo throttling (módulo 3).
El HPA quiere 12 réplicas. Está por debajo de maxReplicas: 30, así que se aplica (sujeto al behavior, que veremos en el apartado 9).
Observa la elegancia de la fórmula: multiplica el número actual por el factor de exceso. Si vas al triple del objetivo con 4 pods, pide 12 pods. Es una regla de tres que asume implícitamente que la carga se reparte por igual entre las réplicas, lo cual es cierto si el Service hace balanceo razonable y las peticiones son homogéneas.
Caso 3: pasa la punta
Ahora hay 12 réplicas y la media baja a 120m:
Pide 5 réplicas. Bajará de 12 a 5, pero no de golpe: la ventana de estabilización y las políticas de scaleDown controlan el ritmo (apartado 9).
Detalles finos de la fórmula que casi nadie conoce
Estos matices explican comportamientos que de otro modo parecen erráticos:
-
Pods sin métricas. Si un pod no tiene métricas todavía (acaba de arrancar), el HPA lo excluye del promedio pero lo cuenta de forma conservadora: al escalar hacia arriba asume consumo 0 para ese pod (para no sobrestimar), y al escalar hacia abajo asume consumo igual al objetivo (para no infraestimar). El resultado es que el HPA es prudente en ambos sentidos mientras hay pods arrancando.
-
Pods no listos. Los pods que no han pasado su sonda
readinessProbese excluyen del cálculo al escalar hacia arriba. Esto evita el clásico bucle infernal: pods nuevos que aún no reciben tráfico, con CPU alta por el arranque, harían creer al HPA que hace falta escalar aún más. -
--horizontal-pod-autoscaler-initial-readiness-delay(30 s por defecto): durante los primeros 30 segundos de vida de un pod, sus métricas se ignoran por completo. La justificación es clara: el arranque de Node.js consume mucha CPU compilando y cargando módulos, y no queremos que ese pico contamine la decisión. -
--horizontal-pod-autoscaler-cpu-initialization-period(5 min por defecto): un margen adicional para métricas de CPU de pods recién listos.
La consecuencia práctica de los puntos 3 y 4 es que el HPA tiene una latencia de arranque de al menos medio minuto por decisión, y por eso el sobreaprovisionamiento de 09-03 y el precalentamiento por cron de 09-04 tienen sentido.
- La banda de tolerancia y el bailoteo de réplicas
Si el HPA aplicase la fórmula literalmente cada 15 segundos, el número de réplicas oscilaría sin parar. Con 4 pods a 320m y un objetivo de 325m la fórmula da techo(4 × 0,984) = 4; con 330m da techo(4 × 1,015) = 5. Una fluctuación de 10m provocaría un pod arriba y abajo indefinidamente. A eso se le llama bailoteo o thrashing, y es caro: cada arranque consume CPU, calienta cachés desde cero y ensucia las métricas.
Kubernetes lo evita con una banda de tolerancia: si el cociente valorActual / valorObjetivo está entre 0,9 y 1,1 (es decir, dentro de un ±10 %), el HPA no hace nada.
ratio = valorActual / valorObjetivo
|ratio - 1| <= 0,10 → no se escala
|ratio - 1| > 0,10 → se aplica la formulaCon nuestro objetivo de 325m, la zona muerta es:
| Consumo medio | ratio | ¿Actúa el HPA? |
|---|---|---|
| 280m | 0,86 | Sí, escala hacia abajo |
| 300m | 0,92 | No, dentro de la banda |
| 325m | 1,00 | No |
| 355m | 1,09 | No, dentro de la banda |
| 380m | 1,17 | Sí, escala hacia arriba |
Este umbral es global del clúster (--horizontal-pod-autoscaler-tolerance), no configurable por HPA... hasta hace poco. Desde Kubernetes 1.33 existe la feature gate HPAConfigurableTolerance, que permite fijar la tolerancia por métrica dentro del behavior:
behavior:
scaleUp:
tolerance: 0.05 # Mas sensible al subir: reacciona con un 5 % de exceso
scaleDown:
tolerance: 0.15 # Mas perezoso al bajarComo feature gate opcional, no la des por hecha: comprueba la versión y la configuración de tu clúster antes de usarla en producción. En el clúster de minikube del curso trabajaremos con la tolerancia por defecto del 10 %.
- Requisitos ineludibles:
requests y metrics-server
requests y metrics-serverDos requisitos, y ninguno es negociable.
Requisito 1: metrics-server instalado
El HPA con métricas de tipo Resource lee de la API metrics.k8s.io, que sirve metrics-server. Lo instalamos en 07-02 con el addon de minikube. Verifícalo:
La columna AVAILABLE debe decir True. Si dice False (MissingEndpoints) o similar, el HPA no funcionará.
Si kubectl top funciona, el HPA tendrá datos. Si no, no hay HPA que valga. Es la primera comprobación siempre.
En minikube, si aún no lo tienes:
minikube -p rutas-norte addons enable metrics-server
# Tarda entre 30 y 60 segundos en empezar a servir datos.Requisito 2: requests declarados en el contenedor
Esta es la trampa más frecuente. El objetivo Utilization se calcula como porcentaje del requests. Si un contenedor no declara requests.cpu, el HPA no tiene denominador, y su métrica queda como <unknown> para siempre. No hay mensaje de error espectacular; simplemente no escala.
# Fragmento del Deployment de api-reservas. SIN ESTO NO HAY HPA.
spec:
template:
spec:
containers:
- name: api
image: registry.rutasnorte.example/api-reservas:1.14.2
resources:
requests:
cpu: 500m # <-- El HPA NECESITA este valor
memory: 512Mi
limits:
cpu: "1"
memory: 1GiY ojo con los sidecars: si el pod tiene un contenedor sin requests.cpu, la métrica del pod entero queda inválida para el HPA cuando usas type: Resource. En Rutas Norte, api-reservas lleva un sidecar exportador de métricas; si ese sidecar no declara requests, el HPA de todo el Deployment se queda ciego. Dos soluciones:
- Declarar
requeststambién en el sidecar (recomendado, y además necesario para la QoS del módulo 3). - Usar
type: ContainerResourceapuntando solo al contenedorapi.
Aplicamos las dos en Rutas Norte: cinturón y tirantes.
- Diagnóstico del temido
<unknown>
<unknown>Tarde o temprano verás esto:
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
api-reservas Deployment/api-reservas <unknown>/65% 4 30 4 3m12s<unknown> significa: «el HPA no ha podido obtener el valor actual de esta métrica». No escala, y punto. El diagnóstico se hace siempre en el mismo orden:
Fíjate en el bloque Conditions y en los eventos del final. Las causas, ordenadas por frecuencia:
Síntoma en describe |
Causa | Solución |
|---|---|---|
FailedGetResourceMetric ... unable to get metrics for resource cpu: no metrics returned from resource metrics API |
metrics-server no está o no responde | Instalar/reparar metrics-server (07-02) |
missing request for cpu on container X |
Un contenedor del pod no declara requests.cpu |
Añadir requests a todos los contenedores, sidecars incluidos |
ScalingActive=False, reason: FailedGetScale |
El scaleTargetRef apunta a un objeto que no existe o está mal escrito |
Corregir name/kind/apiVersion |
did not receive metrics for any ready pods |
Todos los pods llevan menos de 30 s listos, o ninguno pasa la readiness | Esperar; revisar sondas (07-01) |
<unknown> solo en métricas Pods/External |
No hay adaptador de métricas personalizadas registrado | Instalar el adaptador o KEDA (09-04) |
Ejemplo de salida real con el error de requests faltante:
Conditions:
Type Status Reason Message
---- ------ ------ -------
AbleToScale True SucceededGetScale the HPA controller was able to get the target's current scale
ScalingActive False FailedGetResourceMetric the HPA was unable to compute the replica count:
failed to get cpu utilization: missing request for cpu
in container exportador-metricas of Pod api-reservas-7c9d4f8b6d-2mk8p
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedGetResourceMetric 12s (x8 over 2m) horizontal-pod-autoscaler missing request for cpu
Warning FailedComputeMetricsReplicas 12s (x8 over 2m) horizontal-pod-autoscaler invalid metrics (1 invalid out of 1)El mensaje señala con nombre y apellidos el contenedor culpable: exportador-metricas. Añadirle requests.cpu resuelve el problema en el siguiente ciclo de 15 segundos.
Un truco de diagnóstico rápido: consulta directamente la API de métricas para ver qué está viendo el HPA.
kubectl get --raw "/apis/metrics.k8s.io/v1beta1/namespaces/rutas-norte-pro/pods" \
| python3 -m json.tool | head -40Si esa llamada devuelve datos y el HPA sigue en <unknown>, el problema es de requests, no de metrics-server.
- El bloque
behavior: políticas, periodos y ventana de estabilización
behavior: políticas, periodos y ventana de estabilizaciónHasta ahora hemos visto cuántas réplicas quiere el HPA. El bloque behavior decide a qué velocidad llega a ese número. Es la diferencia entre un autoescalado que funciona y uno que da miedo.
Estructura
behavior:
scaleUp:
stabilizationWindowSeconds: 0
selectPolicy: Max
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 4
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 600
selectPolicy: Min
policies:
- type: Percent
value: 20
periodSeconds: 120Campo a campo:
policies — cada política limita cuánto puede cambiar el número de réplicas en una ventana de periodSeconds:
type: Percentconvalue: 100yperiodSeconds: 30significa «en cualquier ventana de 30 segundos puedes como mucho duplicar el número de réplicas» (aumentar un 100 % sobre la base).type: Podsconvalue: 4yperiodSeconds: 30significa «en cualquier ventana de 30 segundos puedes añadir como mucho 4 pods».
selectPolicy — cuando hay varias políticas, decide cuál manda:
| Valor | Efecto | Cuándo usarlo |
|---|---|---|
Max (defecto en scaleUp) |
Permite el cambio más agresivo de todas las políticas | Subir rápido |
Min (defecto en scaleDown) |
Permite el cambio más conservador | Bajar despacio |
Disabled |
Desactiva el escalado en esa dirección | Congelar bajadas durante una campaña |
Ejemplo del efecto de selectPolicy: Max en scaleUp con nuestras políticas y 4 réplicas actuales:
- Política de porcentaje: 100 % de 4 = permite añadir 4 → 8 réplicas.
- Política de pods: permite añadir 4 → 8 réplicas.
Maxtoma el mayor: 8 réplicas.
Con 20 réplicas actuales:
- Porcentaje: 100 % de 20 = permite añadir 20 → 40 réplicas.
- Pods: permite añadir 4 → 24 réplicas.
Maxtoma 40 (limitado luego pormaxReplicas: 30→ 30).
Y con selectPolicy: Min habría elegido 24. Esto ilustra por qué Max al subir tiene sentido: cuando ya tienes muchas réplicas, el porcentaje te da margen para crecer rápido.
stabilizationWindowSeconds — es el parámetro más importante y el peor entendido. Funciona así: el HPA guarda un histórico de las recomendaciones de los últimos N segundos y usa el valor más conservador de esa ventana.
- En
scaleUp, «más conservador» significa el mínimo de las recomendaciones recientes. Con una ventana de 60 s, si en el último minuto el HPA recomendó 12, 8 y 14 réplicas, escalará a 8. Efecto: ignora picos aislados. - En
scaleDown, «más conservador» significa el máximo de las recomendaciones recientes. Con una ventana de 600 s, si en los últimos 10 minutos recomendó 12, 5 y 6, mantendrá 12. Efecto: no baja hasta que la calma se sostiene diez minutos enteros.
Valores por defecto si no defines behavior:
scaleUp |
scaleDown |
|
|---|---|---|
stabilizationWindowSeconds |
0 | 300 (5 minutos) |
policies |
100 % cada 15 s y 4 pods cada 15 s | 100 % cada 15 s |
selectPolicy |
Max |
Min |
Es decir, por defecto el HPA puede duplicarse cada 15 segundos y puede irse a minReplicas de golpe tras 5 minutos de calma. Lo primero suele estar bien; lo segundo es demasiado brusco para Rutas Norte.
La configuración razonada de Rutas Norte: subir rápido, bajar despacio
Nuestra política es deliberadamente asimétrica, y conviene entender por qué:
Subir rápido. El coste de escalar de más durante unos minutos es unos céntimos de cómputo. El coste de escalar de menos durante unos minutos es que la venta del puente de mayo se cae y Rutas Norte pierde miles de euros en billetes no vendidos, más la reputación. Los costes son radicalmente asimétricos, y por tanto la respuesta debe serlo. stabilizationWindowSeconds: 0 en scaleUp: en cuanto la carga sube, se actúa.
Bajar despacio. El tráfico de una web de billetes es irregular por naturaleza: una punta a las 10:00, un valle a las 10:03, otra punta a las 10:05. Si bajásemos rápido, estaríamos destruyendo pods que hay que volver a crear treinta segundos después, con el coste de arranque en frío (conexiones a PostgreSQL, caché de rutas vacía) y una latencia peor para el usuario. stabilizationWindowSeconds: 600 en scaleDown: no bajamos hasta que la calma dura diez minutos. Y aun entonces, bajamos un 20 % cada dos minutos, no de golpe.
La formulación mental: el autoescalado hacia arriba es un mecanismo de disponibilidad; el autoescalado hacia abajo es un mecanismo de coste. La disponibilidad es urgente; el ahorro puede esperar diez minutos.
Congelar el escalado hacia abajo durante una campaña
Un uso avanzado y muy práctico de selectPolicy: Disabled. Durante los tres días del puente de mayo, el equipo de Rutas Norte puede decidir que no quiere ninguna reducción de réplicas, ni siquiera lenta:
Se aplica el viernes por la mañana y se revierte el lunes. Es un interruptor de emergencia legítimo y mucho más seguro que borrar el HPA.
- Varias métricas a la vez: gana la que pide más
El campo metrics es una lista. Cuando hay varias, el HPA calcula la fórmula para cada una por separado y luego aplica una regla muy simple:
Se queda con el número de réplicas MÁS ALTO de todos los cálculos.
Es un OR de necesidad: basta con que una sola métrica pida más réplicas para que se escale. Nunca se promedian ni se combinan.
Ejemplo con dos métricas en api-reservas (5 réplicas actuales):
| Métrica | Objetivo | Valor actual | Réplicas que pide |
|---|---|---|---|
CPU (Utilization) |
65 % | 42 % | techo(5 × 0,646) = 4 |
Peticiones por segundo (Pods) |
80 rps/pod | 190 rps/pod | techo(5 × 2,375) = 12 |
| Decisión final | 12 |
La CPU dice que sobran pods; las peticiones por segundo dicen que faltan muchos. Gana 12. Esta asimetría es intencionada y correcta: el HPA prefiere equivocarse por exceso de capacidad que por defecto.
Consecuencia práctica que hay que tener presente: si una de las métricas se queda en <unknown>, el HPA no puede garantizar que no haga falta escalar por ella. Su comportamiento en ese caso es conservador: si la métrica válida pide subir, sube; si pide bajar, no baja, porque no sabe si la métrica rota justificaría mantener las réplicas. Esto explica el desconcierto habitual de «mi HPA tiene una métrica en <unknown> y se ha quedado clavado arriba».
- Los HPA de Rutas Norte, manifiestos completos
Vamos a escribir los dos manifiestos definitivos. Van en k8s/entornos/pro/, porque los valores de minReplicas difieren por entorno.
HPA de api-reservas
# k8s/entornos/pro/hpa-api-reservas.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-reservas
namespace: rutas-norte-pro
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-reservas
minReplicas: 4 # Suelo de disponibilidad: 4 replicas repartidas entre zonas (09-05)
maxReplicas: 30 # Techo: 30 x 500m = 15 nucleos de requests. Cabe en el plan de capacidad.
metrics:
# Metrica principal: CPU del contenedor "api", excluyendo el sidecar de metricas.
- type: ContainerResource
containerResource:
name: cpu
container: api
target:
type: Utilization
averageUtilization: 65
# 65 % de 500m = 325m por pod. Deja un 35 % de margen para absorber
# el tiempo que tardan en arrancar las replicas nuevas.
behavior:
scaleUp:
# Reaccion inmediata: cero espera. La disponibilidad manda.
stabilizationWindowSeconds: 0
selectPolicy: Max
policies:
# Duplicar el numero de replicas cada 30 s...
- type: Percent
value: 100
periodSeconds: 30
# ...o anadir 6 pods cada 30 s, lo que permita mas.
# Con pocas replicas manda la politica de pods; con muchas, la de porcentaje.
- type: Pods
value: 6
periodSeconds: 30
scaleDown:
# Diez minutos de calma sostenida antes de empezar a bajar.
stabilizationWindowSeconds: 600
selectPolicy: Min
policies:
# Como mucho, quitar el 20 % de las replicas cada 2 minutos.
- type: Percent
value: 20
periodSeconds: 120
# Y nunca mas de 3 pods cada 2 minutos. Con selectPolicy: Min manda la mas suave.
- type: Pods
value: 3
periodSeconds: 120Un cálculo que justifica el maxReplicas: 30: cada pod reserva 500m de CPU y 512Mi de memoria. Treinta pods son 15 núcleos y 15 GiB solo de api-reservas. Sumando tienda-web, worker-notificaciones y las bases de datos, eso define el tamaño del clúster que necesitaremos en 09-03. El maxReplicas no es un número que se pone al azar: es un compromiso de capacidad.
Otro cálculo, sobre la velocidad de subida. Partiendo de 4 réplicas con Max entre «duplicar» y «+6 pods»:
| Momento | Réplicas | Política que manda |
|---|---|---|
| t = 0 s | 4 | — |
| t = 30 s | 10 | +6 pods (duplicar daría 8) |
| t = 60 s | 20 | duplicar (+6 daría 16) |
| t = 90 s | 30 | duplicar daría 40, maxReplicas corta en 30 |
De 4 a 30 réplicas en 90 segundos. Sumando unos 20-30 segundos de arranque de cada pod de Node.js, la plataforma pasa de su capacidad de martes a su capacidad máxima en unos dos minutos. Eso es lo que hay que comparar contra la velocidad a la que llega el tráfico.
HPA de tienda-web
tienda-web es nginx sirviendo activos estáticos. Su perfil es distinto: consume poquísima CPU por petición, arranca en un segundo y su límite real es el ancho de banda y el número de conexiones, no el cómputo.
# k8s/entornos/pro/hpa-tienda-web.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: tienda-web
namespace: rutas-norte-pro
labels:
app: tienda-web
app.kubernetes.io/part-of: rutas-norte
entorno: pro
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: tienda-web
minReplicas: 3
maxReplicas: 15
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
# Umbral MAS BAJO que el de la API: nginx arranca en ~1 s y consume poco,
# asi que podemos permitirnos escalar antes sin coste apreciable.
# Ademas es la puerta de entrada: si se satura, no entra nadie.
averageUtilization: 50
behavior:
scaleUp:
stabilizationWindowSeconds: 0
selectPolicy: Max
policies:
- type: Percent
value: 100
periodSeconds: 15 # Mas agresivo aun: nginx arranca en un segundo
- type: Pods
value: 4
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 300 # 5 min: menos conservador que la API,
# porque nginx no tiene arranque en frio caro
selectPolicy: Min
policies:
- type: Percent
value: 25
periodSeconds: 60Comparación razonada de ambos HPA:
| Parámetro | api-reservas |
tienda-web |
Motivo de la diferencia |
|---|---|---|---|
| Objetivo de CPU | 65 % | 50 % | nginx es barato de escalar; la API no |
minReplicas |
4 | 3 | Disponibilidad mínima acordada por componente |
maxReplicas |
30 | 15 | La API es el componente caro en CPU |
periodSeconds al subir |
30 s | 15 s | nginx arranca en 1 s; Node.js en 20-30 s |
| Estabilización al bajar | 600 s | 300 s | La API tiene arranque en frío costoso (pool de conexiones, caché) |
| Tipo de métrica | ContainerResource |
Resource |
La API tiene sidecar de métricas; nginx no |
Aplicación
kubectl apply -f k8s/entornos/pro/hpa-api-reservas.yaml
kubectl apply -f k8s/entornos/pro/hpa-tienda-web.yaml
kubectl get hpa -n rutas-norte-proNAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
api-reservas Deployment/api-reservas 28%/65% 4 30 4 22s
tienda-web Deployment/tienda-web 11%/50% 3 15 3 19sLa columna TARGETS muestra valorActual/objetivo. Si ves números en lugar de <unknown>, todo está bien conectado.
- Simulación de una punta de carga y observación en vivo
Vamos a provocar una punta en el entorno de desarrollo y ver el HPA trabajar. Usaremos el perfil de minikube del curso.
Preparar el terreno
minikube -p rutas-norte addons enable metrics-server
kubectl -n rutas-norte-dev apply -f k8s/entornos/dev/hpa-api-reservas.yamlPara la demostración, un HPA de desarrollo con umbrales bajos para que reaccione rápido:
# k8s/entornos/dev/hpa-api-reservas.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-reservas
namespace: rutas-norte-dev
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api-reservas
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 40 # Umbral bajo: escala pronto, se ve mejor la demo
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Pods
value: 3
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 120 # Corto para la demo; en pro seria 600Terminal 1: observar
Terminal 2: generar carga
Un pod efímero que martillea el endpoint de búsqueda de rutas en bucle:
kubectl -n rutas-norte-dev run generador-carga \
--image=busybox:1.36 \
--restart=Never \
--rm -it \
-- /bin/sh -c \
'while true; do wget -q -O- http://api-reservas/rutas?origen=BIL\&destino=SDR > /dev/null; done'Explicación del comando:
--rm -itcrea el pod, se conecta a él y lo borra al salir conCtrl+C.http://api-reservasusa el DNS interno del clúster (04-03): resuelve al Service del mismo namespace.- El bucle infinito de
wgetgenera peticiones tan rápido como pueda.
Para una punta más seria, varias réplicas del generador:
kubectl -n rutas-norte-dev create deployment generador-carga \
--image=busybox:1.36 --replicas=6 \
-- /bin/sh -c 'while true; do wget -q -O- http://api-reservas/rutas > /dev/null; done'Lo que se ve en el terminal 1
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
api-reservas Deployment/api-reservas 3%/40% 1 10 1 5m
api-reservas Deployment/api-reservas 3%/40% 1 10 1 5m15s
api-reservas Deployment/api-reservas 187%/40% 1 10 1 5m30s
api-reservas Deployment/api-reservas 187%/40% 1 10 4 5m30s
api-reservas Deployment/api-reservas 142%/40% 1 10 4 5m45s
api-reservas Deployment/api-reservas 142%/40% 1 10 7 5m45s
api-reservas Deployment/api-reservas 94%/40% 1 10 7 6m
api-reservas Deployment/api-reservas 94%/40% 1 10 10 6m
api-reservas Deployment/api-reservas 61%/40% 1 10 10 6m30s
api-reservas Deployment/api-reservas 38%/40% 1 10 10 7mLee la secuencia con la fórmula en la mano:
3%/40%con 1 réplica:techo(1 × 0,075) = 1. Nada que hacer.187%/40%con 1 réplica:techo(1 × 4,675) = 5. Pero la políticaPods: 3 cada 15slimita a 4. Ahí está elbehavioren acción.142%/40%con 4 réplicas:techo(4 × 3,55) = 15. La política limita a 4+3 = 7.94%/40%con 7 réplicas:techo(7 × 2,35) = 17. Limitado a 10 pormaxReplicas.38%/40%con 10 réplicas: ratio 0,95, dentro de la banda de tolerancia. Estable. Objetivo alcanzado.
Ahora detén el generador (Ctrl+C o borrando el Deployment) y observa la bajada:
api-reservas Deployment/api-reservas 2%/40% 1 10 10 9m
api-reservas Deployment/api-reservas 2%/40% 1 10 10 10m
api-reservas Deployment/api-reservas 2%/40% 1 10 1 11mFíjate: la CPU cae inmediatamente al 2 %, pero las réplicas se quedan en 10 durante dos minutos completos. Esa es la stabilizationWindowSeconds: 120. Cumplida la ventana, baja a 1 de golpe (en esta demo no definimos políticas de scaleDown, así que aplica la de por defecto: 100 % cada 15 s).
Los eventos del escalado
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulRescale 5m30s horizontal-pod-autoscaler New size: 4; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 5m15s horizontal-pod-autoscaler New size: 7; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 5m horizontal-pod-autoscaler New size: 10; reason: cpu resource utilization (percentage of request) above target
Normal SuccessfulRescale 30s horizontal-pod-autoscaler New size: 1; reason: All metrics below targetEstos eventos son oro puro para el análisis posterior de un incidente: te dicen exactamente cuándo escaló, a cuánto y por qué. Recuerda que los eventos caducan (una hora por defecto); para el historial largo hace falta el registro centralizado del módulo 7.
- El conflicto entre el HPA y el campo
replicas
replicasEste es el error que muerde a todos los equipos exactamente una vez, y conviene que no sea en el puente de mayo.
El problema
El Deployment de api-reservas que llevamos escribiendo desde el módulo 2 dice:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-reservas
spec:
replicas: 4 # <-- Este campo
...Y ahora un HPA también gobierna ese mismo campo. Tenemos dos autoridades sobre el mismo número.
Mientras nadie aplique el YAML, no pasa nada: el HPA modifica el campo en el servidor y el objeto vive feliz con 22 réplicas. El problema aparece cuando alguien vuelve a aplicar el manifiesto.
Escenario real:
10:00 El HPA escala api-reservas a 22 replicas. La venta va bien.
10:14 Un companero corrige una etiqueta en el Deployment y ejecuta:
kubectl apply -f k8s/base/deployment-api-reservas.yaml
10:14 El manifiesto dice replicas: 4. La API lo acepta.
22 pods -> 4 pods, INSTANTANEAMENTE.
10:14 La plataforma se cae.
10:14 El HPA (siguiente ciclo, hasta 15 s despues) detecta CPU al 400 %
y vuelve a escalar. Pero ya hay 15-90 segundos de caida.Quince segundos de caída total en el pico de venta del año. Todo por un campo que no debería estar ahí.
Y es peor con GitOps: si Argo CD (10-05) tiene el repositorio como fuente de verdad y detecta que el clúster tiene 22 réplicas cuando el Git dice 4, marcará el recurso como OutOfSync y, con sincronización automática, lo «corregirá» a 4. Cada vez. Tendrás un tira y afloja permanente entre el HPA y el operador de GitOps, con la plataforma oscilando entre 4 y 22 réplicas indefinidamente.
La solución: quitar replicas del manifiesto
# k8s/base/deployment-api-reservas.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-reservas
namespace: rutas-norte-pro
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
spec:
# SIN campo "replicas": lo gobierna el HorizontalPodAutoscaler api-reservas.
# Ver k8s/entornos/pro/hpa-api-reservas.yaml
# Si lo devuelves aqui, cada "kubectl apply" tirara la plataforma al suelo
# durante el pico de trafico. No lo hagas.
selector:
matchLabels:
app: api-reservas
template:
metadata:
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
spec:
containers:
- name: api
image: registry.rutasnorte.example/api-reservas:1.14.2
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "1"
memory: 1GiAl omitir replicas, el valor por defecto de la API es 1, pero solo en la creación inicial. En aplicaciones posteriores, el campo simplemente no se toca: kubectl apply compara con la anotación kubectl.kubernetes.io/last-applied-configuration, ve que replicas no estaba antes ni está ahora, y lo deja como está. El HPA sigue mandando.
Una consecuencia a tener en cuenta: la primera vez que crees el Deployment sin replicas, arrancará con 1 pod hasta que el HPA lo lleve a minReplicas en el siguiente ciclo (menos de 15 segundos). Es aceptable en un despliegue nuevo; si te molesta, crea el HPA primero.
Ese comentario explícito en el YAML no es decorativo: es documentación en el lugar donde alguien tomará la decisión equivocada. Escríbelo.
Alternativas y sus matices
| Enfoque | Cómo | Cuándo usarlo |
|---|---|---|
Omitir replicas |
Borrarlo del YAML | Recomendado. Simple, funciona con apply y con GitOps |
| Server-Side Apply con propiedad compartida | kubectl apply --server-side; el HPA es propietario del campo |
Bueno, pero requiere que todo el flujo use SSA |
ignoreDifferences en Argo CD |
Configurar Argo CD para ignorar /spec/replicas |
Necesario si por alguna razón no puedes quitar el campo |
| Kustomize sin patch de réplicas | No usar replicas: en los overlays |
Complementa a la primera opción (10-04) |
Ojo con una trampa de Server-Side Apply: si aplicas con SSA y el campo replicas sí está en tu manifiesto, tomarás la propiedad del campo y el HPA recibirá un conflicto. La API te lo dirá con un error explícito de conflicto de propietarios, lo cual es mejor que el fallo silencioso de kubectl apply clásico, pero sigue siendo un fallo.
Verificar quién manda
kubectl get deployment api-reservas -n rutas-norte-pro \
-o jsonpath='{.metadata.managedFields[*].manager}{"\n"}'Si ves kube-controller-manager entre los gestores, el HPA está escribiendo el campo. Es la confirmación de que la cadena funciona.
- Qué no escalar con HPA y el cuello de botella real
Terminamos con la lección más valiosa de todas, y la que más dinero ahorra.
El error de escalar la capa equivocada
Imagina esta secuencia en el puente de mayo:
- Llega la avalancha de tráfico.
- El HPA de
api-reservasescala de 4 a 30 réplicas en dos minutos. Perfecto. - Las 30 réplicas abren, cada una, su pool de 20 conexiones a
postgres-reservas. Total: 600 conexiones. postgres-reservastienemax_connections = 200.- Las 400 conexiones sobrantes son rechazadas. Los pods de
api-reservasdevuelven error 500. - La CPU de
api-reservassube (los reintentos y el manejo de errores consumen), así que el HPA quiere escalar más.maxReplicaslo corta en 30. - La plataforma está caída con 30 réplicas donde antes se caía con 4. Ha costado más dinero y no ha servido de nada.
Este patrón tiene nombre: desplazar el cuello de botella sin resolverlo. El HPA no lo detecta porque el HPA solo sabe de CPU: no sabe que la base de datos está saturada.
flowchart LR
A[Trafico x10] --> B[tienda-web<br/>HPA: 3 a 15]
B --> C[api-reservas<br/>HPA: 4 a 30]
C --> D[postgres-reservas<br/>1 replica<br/>max_connections=200]
D -.->|CUELLO DE BOTELLA| E[Errores 500]
C --> F[redis-cache<br/>1 replica]
style D fill:#f88,stroke:#900,stroke-width:3px
style E fill:#f88,stroke:#900
La capa que no escala determina la capacidad de todo el sistema. Es la ley del eslabón más débil aplicada a la arquitectura.
Qué hay que hacer en cambio
Con postgres-reservas como límite, las palancas reales son:
| Palanca | Qué hace | Dónde se trata |
|---|---|---|
| Limitar el pool por réplica | Con 30 réplicas × 6 conexiones = 180 < 200 | 09-06 |
| Poner un pool compartido (PgBouncer) | Multiplexa miles de conexiones de aplicación sobre pocas de base de datos | 09-06 |
Cachear en redis-cache |
La consulta de disponibilidad no llega a PostgreSQL | 09-06 |
| Réplicas de lectura | Las búsquedas van a la réplica; solo las reservas al primario | 06-07 (operador) |
| Escalar verticalmente PostgreSQL | Más CPU/RAM y disco más rápido | 09-02 y 09-06 |
Ninguna de ellas es un HPA. La lección: antes de poner un HPA, identifica el recurso escaso. Si no es la CPU de los pods que vas a escalar, el HPA no te va a ayudar.
Lista de comprobación antes de poner un HPA
Pregúntate:
- ¿La aplicación es realmente sin estado? ¿Puede cualquier réplica atender cualquier petición sin memoria local ni afinidad de sesión?
- ¿La CPU es el recurso escaso? ¿O es la base de datos, el disco, la red, un servicio externo o un bloqueo global?
- ¿La carga se reparte por igual? Si un endpoint concentra el 90 % del coste y un solo cliente lo llama, añadir réplicas no ayuda.
- ¿Cuánto tarda un pod nuevo en ser útil? Si tarda tres minutos, el HPA llegará tarde a las puntas rápidas. Habrá que trabajar el arranque (09-06) o precalentar (09-04).
- ¿Hay sitio en el clúster para las réplicas nuevas? Si no, el HPA solo generará pods
Pending(09-03). - ¿El servicio de abajo aguanta N veces más carga? El pool de conexiones, la cuota de la API externa, el límite de tasa.
Solo si las seis respuestas son satisfactorias, el HPA hará lo que esperas.
Errores Comunes y Consejos
Error 1: dejar el campo replicas en el manifiesto. Ya lo hemos desarrollado en el apartado 13, pero merece repetirse porque es el error número uno y sus consecuencias son inmediatas y visibles. Quítalo y deja un comentario en su lugar.
Error 2: escalar por memoria. Los entornos de ejecución con recolector de basura no devuelven memoria al sistema. El HPA sube y no vuelve a bajar nunca. Usa CPU, o una métrica de negocio (09-04). Si de verdad necesitas memoria, usa AverageValue en lugar de Utilization y ten expectativas realistas.
Error 3: poner el objetivo de utilización demasiado alto. Un averageUtilization: 90 parece eficiente, pero deja solo un 10 % de margen. Cuando el HPA detecta la subida, los pods nuevos tardan 30 segundos en estar listos, y durante esos 30 segundos los pods existentes están al 130 % y sufriendo throttling. El objetivo debe dejar hueco para el tiempo de arranque. Entre 50 % y 70 % es el rango sano; cuanto más lento arranque tu aplicación, más bajo el objetivo.
Error 4: maxReplicas sin plan de capacidad. Un maxReplicas: 200 en un clúster de tres nodos no te da 200 réplicas: te da un montón de pods Pending y una falsa sensación de seguridad. Calcula maxReplicas × requests y compáralo con la capacidad real (09-03).
Error 5: olvidar los requests en los sidecars. Un solo contenedor sin requests.cpu deja ciego al HPA de todo el Deployment. En Rutas Norte, el exportador de métricas de api-reservas es el sospechoso habitual.
Error 6: no comprobar que los pods nuevos son útiles. Un pod que arranca y pasa la readiness pero tiene el pool de conexiones vacío y la caché fría atiende peor que uno caliente. Durante los primeros segundos, escalar puede empeorar la latencia media. Ajusta las sondas (07-01) para que un pod solo se declare listo cuando de verdad lo esté.
Consejo 1: pon primero el HPA en modo observación. Créalo con minReplicas igual a maxReplicas (por ejemplo, ambos a 4). El HPA no podrá escalar, pero calculará y publicará las métricas en kubectl get hpa. Déjalo una semana, mira qué habría hecho, y solo entonces abre el rango. Es gratis y evita sustos.
Consejo 2: alerta cuando el HPA esté pegado al techo. Si api-reservas lleva media hora en 30 réplicas, el maxReplicas está limitando la capacidad y nadie se entera. Una alerta en Alertmanager (07-04):
kube_horizontalpodautoscaler_status_current_replicas
>= kube_horizontalpodautoscaler_spec_max_replicasConsejo 3: registra un panel con réplicas y métrica juntas. En Grafana, superponer «réplicas actuales» y «CPU media» en el mismo gráfico hace evidente si el HPA reacciona bien o va por detrás. Es el diagnóstico visual más útil que existe para un HPA.
Consejo 4: prueba el HPA antes de necesitarlo. Un ensayo de carga contra rutas-norte-pre una semana antes del puente de mayo cuesta una tarde y evita la caída del año. Lo haremos formalmente con k6 en 09-06.
Consejo 5: los eventos del HPA caducan. Una hora por defecto. Si quieres el histórico del incidente, necesitas EFK (07-05) o las métricas de kube-state-metrics en Prometheus.
Ejercicios
Ejercicio 1: aplicar la fórmula
tienda-web está desplegado con requests.cpu: 200m y un HPA con averageUtilization: 50, minReplicas: 3, maxReplicas: 15. En este momento hay 6 réplicas y kubectl top da:
tienda-web-6f7d9c4b58-2wq4m 210m
tienda-web-6f7d9c4b58-4kjnx 198m
tienda-web-6f7d9c4b58-7hgpz 225m
tienda-web-6f7d9c4b58-9mzbc 187m
tienda-web-6f7d9c4b58-kx2vt 204m
tienda-web-6f7d9c4b58-tn8fr 216mCalcula: (a) el objetivo absoluto por pod; (b) el consumo medio; (c) el ratio y si está dentro de la banda de tolerancia; (d) las réplicas deseadas; (e) qué hará el HPA con este behavior:
behavior:
scaleUp:
stabilizationWindowSeconds: 0
selectPolicy: Max
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 4
periodSeconds: 15Ejercicio 2: diagnosticar un HPA mudo
Un compañero ha desplegado worker-notificaciones con este HPA y se queja de que nunca escala:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: worker-notificaciones
namespace: rutas-norte-pro
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: worker-notificaciones
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70El Deployment es:
spec:
template:
spec:
containers:
- name: worker
image: registry.rutasnorte.example/worker-notificaciones:2.3.0
resources:
limits:
cpu: 500m
memory: 512Mi
- name: exportador-metricas
image: registry.rutasnorte.example/exportador:1.2.0kubectl get hpa muestra <unknown>/70%. Identifica todos los problemas, escribe los comandos de diagnóstico que ejecutarías y las correcciones. Además, explica por qué incluso arreglando todo esto este HPA seguirá siendo una mala idea para este componente concreto.
Ejercicio 3: diseñar un behavior para un escenario nuevo
Rutas Norte lanza panel-conductores, una aplicación web interna que usan los 40 conductores de la flota. Perfil de carga:
- De lunes a viernes, de 5:30 a 7:00, todos los conductores entran para consultar su ruta del día: la carga pasa de cero a su máximo en quince minutos.
- El resto del día hay uso esporádico, muy bajo.
- Por la noche no hay nadie.
- La aplicación es Node.js y tarda unos 25 segundos en estar lista.
- Es interna: una caída de dos minutos es molesta pero no cuesta dinero.
- El equipo tiene presión de costes: el clúster es compartido y hay ResourceQuota.
Escribe el HPA completo (autoscaling/v2) razonando cada decisión: minReplicas, maxReplicas, métrica, objetivo, y todo el behavior. Compara al menos tres decisiones con las del HPA de api-reservas y explica por qué difieren.
Soluciones
Solución 1
(a) Objetivo absoluto por pod:
(b) Consumo medio:
(c) Ratio y banda de tolerancia:
(d) Réplicas deseadas:
13 réplicas, por debajo de maxReplicas: 15.
(e) Qué hace el behavior:
Réplicas actuales: 6.
- Política
Percent 100 % / 15 s: permite añadir el 100 % de 6 = 6 → hasta 12 réplicas. - Política
Pods 4 / 15 s: permite añadir 4 → hasta 10 réplicas. selectPolicy: Maxtoma el techo más alto: 12.
El HPA quiere 13 pero solo puede llegar a 12 en este paso. Escalará a 12 réplicas ahora.
En el siguiente ciclo (15 s después), con 12 réplicas y suponiendo que la carga total no cambia, el consumo medio bajará a unos 103m por pod. El nuevo ratio sería 1,03, dentro de la banda de tolerancia, así que se quedará en 12 y no llegará nunca a 13. Este es un comportamiento habitual y correcto: la tolerancia absorbe el último salto.
Observación adicional: los pods están al 103 % de su requests (206m contra 200m). No están sufriendo throttling porque su limit es mayor, pero están consumiendo más de lo reservado, lo que significa que dependen de la CPU sobrante del nodo. Es una señal de que el requests de tienda-web está infradimensionado y de que el VPA de 09-02 tendría algo que decir.
Solución 2
Hay cuatro problemas, uno detrás de otro.
Problema 1: el contenedor worker no declara requests.cpu.
Solo tiene limits. Sin requests, el HPA no tiene denominador para el porcentaje.
Matiz importante que muchos desconocen: cuando declaras limits sin requests, Kubernetes rellena automáticamente requests con el valor de limits. Así que este contenedor sí acaba teniendo requests.cpu: 500m. Como efecto secundario, la QoS del pod sería Guaranteed si todos los contenedores estuvieran así (módulo 3). Pero...
Problema 2: el contenedor exportador-metricas no declara ningún recurso.
Este sí es fatal. Sin limits ni requests, no hay relleno automático, y el HPA no puede calcular la utilización del pod. Este es el que produce el <unknown>. Además hace que la QoS del pod sea Burstable en lugar de Guaranteed.
Problema 3: no se sabe si metrics-server está disponible.
Hay que verificarlo antes que nada.
Problema 4 (de diseño, y el más importante): la CPU es la señal equivocada para este componente.
Comandos de diagnóstico, en orden:
# 1. ¿Funciona metrics-server?
kubectl top pods -n rutas-norte-pro -l app=worker-notificaciones
# 2. ¿Qué dice el HPA exactamente?
kubectl describe hpa worker-notificaciones -n rutas-norte-pro
# 3. ¿Qué recursos declara cada contenedor?
kubectl get deployment worker-notificaciones -n rutas-norte-pro \
-o jsonpath='{range .spec.template.spec.containers[*]}{.name}{": "}{.resources}{"\n"}{end}'La salida del paso 3 delataría el problema:
Corrección del Deployment:
spec:
template:
spec:
containers:
- name: worker
image: registry.rutasnorte.example/worker-notificaciones:2.3.0
resources:
requests:
cpu: 300m # Explicito, no dependemos del relleno automatico
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
- name: exportador-metricas
image: registry.rutasnorte.example/exportador:1.2.0
resources:
requests:
cpu: 20m # <-- LA CORRECCION CLAVE
memory: 32Mi
limits:
cpu: 50m
memory: 64MiY, defensivamente, cambiar el HPA a ContainerResource para que el sidecar no distorsione el porcentaje:
metrics:
- type: ContainerResource
containerResource:
name: cpu
container: worker
target:
type: Utilization
averageUtilization: 70Por qué sigue siendo mala idea aunque funcione:
worker-notificaciones consume mensajes de una cola de correos. Su trabajo por mensaje es sobre todo espera de entrada/salida: hablar con el servidor SMTP, esperar la respuesta de red. Eso consume muy poca CPU.
En el puente de mayo puede haber cuarenta mil correos esperando en la cola y el worker estar al 20 % de CPU, porque está bloqueado esperando al SMTP la mayor parte del tiempo. El HPA por CPU vería 20 % contra un objetivo de 70 % y concluiría que sobran réplicas: escalaría hacia abajo justo cuando más hacen falta consumidores.
La métrica correcta es la longitud de la cola: «quiero un worker por cada 500 mensajes pendientes». Eso no lo puede leer el HPA de fábrica; hace falta una métrica externa, y esa es exactamente la razón de ser de KEDA en 09-04.
Solución 3
# k8s/entornos/pro/hpa-panel-conductores.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: panel-conductores
namespace: rutas-norte-pro
labels:
app: panel-conductores
app.kubernetes.io/part-of: rutas-norte
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: panel-conductores
# minReplicas: 1. No es un servicio que genere ingresos y por la noche no hay
# nadie. Una replica basta para el uso esporadico del resto del dia y para
# que la primera peticion de las 5:30 no encuentre el servicio apagado.
# No ponemos 2 porque hay presion de costes y una caida de 2 min es asumible.
minReplicas: 1
# maxReplicas: 6. Son 40 conductores concurrentes como maximo. Con 6 replicas
# tocan a menos de 7 usuarios por pod: sobra. Un techo bajo protege la
# ResourceQuota del namespace compartido.
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
# 50 %, no 65-70 %. La aplicacion tarda 25 s en estar lista, asi que
# necesitamos margen para cubrir ese arranque. Ademas la subida de las
# 5:30 es abrupta: mejor ir por delante.
averageUtilization: 50
behavior:
scaleUp:
# Cero espera. La ventana critica dura 90 minutos al dia y toda la carga
# llega en los primeros 15. Cualquier retraso se come la ventana entera.
stabilizationWindowSeconds: 0
selectPolicy: Max
policies:
# periodSeconds 30, no 15: la aplicacion tarda 25 s en estar lista.
# Con 15 s escalariamos otra vez ANTES de que los pods anteriores
# empezasen a absorber carga, y sobreescalariamos.
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 2
periodSeconds: 30
# Con maxReplicas 6, "+2 pods" cubre bien el rango 1->3->6.
scaleDown:
# 300 s (5 min), no 600. El pico dura 90 minutos y luego no vuelve hasta
# el dia siguiente: no hay puntas intermitentes que justifiquen esperar
# diez minutos, y la presion de costes pide liberar recursos pronto.
stabilizationWindowSeconds: 300
selectPolicy: Min
policies:
# Bajada suave: un pod cada 3 minutos. De 6 a 1 en 15 minutos.
# Suficiente para no dejar tirado a un rezagado de las 7:05.
- type: Pods
value: 1
periodSeconds: 180Comparación razonada con el HPA de api-reservas:
| Decisión | api-reservas |
panel-conductores |
Por qué difieren |
|---|---|---|---|
minReplicas |
4 | 1 | La API genera ingresos y necesita disponibilidad multizona; el panel es interno, con presión de costes y una caída tolerable |
maxReplicas |
30 | 6 | El tráfico de la API es público e impredecible (x10 en el puente); el panel tiene un techo conocido de 40 usuarios |
| Objetivo de CPU | 65 % | 50 % | Ambas son Node.js, pero el panel sufre una subida más abrupta (de cero al máximo en 15 min) y necesita más margen para cubrir los 25 s de arranque |
periodSeconds al subir |
30 s | 30 s | Igual: ambas son Node.js con arranque de 20-30 s. Coincidencia justificada por la misma causa técnica |
| Estabilización al bajar | 600 s | 300 s | El tráfico de la API es intermitente durante horas; el del panel es un único bloque diario que no vuelve |
| Política de bajada | 20 % / 120 s | 1 pod / 180 s | Con solo 6 réplicas, un porcentaje del 20 % daría fracciones inútiles; en números pequeños se razona mejor en pods absolutos |
Consideración adicional que merece nota: este perfil de carga —cero por la noche, avalancha a hora fija— es el candidato perfecto para un disparador cron que precaliente el servicio a las 5:15, en lugar de esperar a que la CPU suba y reaccionar tarde. Eso ya no lo hace el HPA de fábrica: es KEDA, y lo veremos en 09-04. Un HPA reactivo siempre va por detrás de una carga que sube en escalón; solo una señal anticipada resuelve ese caso de verdad.
Conclusión
El HorizontalPodAutoscaler convierte el número de réplicas de un dato fijo escrito a mano en una consecuencia observada del tráfico real. Es la diferencia entre dimensionar para un martes cualquiera y sobrevivir al puente de mayo sin que nadie tenga que abrir el portátil en el andén de una estación.
Lo esencial que te llevas de esta lección:
- Escalar hacia fuera y escalar hacia arriba son ejes distintos. El horizontal resuelve el volumen de tráfico y además mejora la disponibilidad; el vertical resuelve el dimensionado de cada instancia. Rutas Norte necesita los dos.
- Solo se escala horizontalmente lo que es realmente sin estado.
api-reservasytienda-websí;postgres-reservasnunca, y el intento produciría corrupción de datos, no capacidad. - La fórmula es una regla de tres:
techo(replicasActuales × valorActual / valorObjetivo), con una banda de tolerancia del ±10 % que evita el bailoteo. - Sin
requestsen todos los contenedores y sin metrics-server no hay HPA. El<unknown>de la columnaTARGETScasi siempre apunta a un sidecar olvidado. - El
behaviores donde se decide si el autoescalado funciona. La asimetría de Rutas Norte —subir en cero segundos, bajar tras diez minutos de calma— refleja una asimetría real de costes: perder ventas es mucho más caro que sobrar capacidad un rato. - Con varias métricas, gana la que pide más réplicas. Un OR de necesidad, deliberadamente conservador.
- El campo
replicasdebe desaparecer del manifiesto versionado. Si no, unkubectl applyinocente tirará la plataforma en el pico de venta, y con GitOps será una guerra permanente (10-05). - Antes de poner un HPA, encuentra el recurso escaso. Escalar la capa web cuando el límite es la base de datos multiplica el coste sin añadir un solo billete vendido.
Rutas Norte tiene ya api-reservas y tienda-web respondiendo al tráfico por su cuenta. Pero seguimos arrastrando un problema del que hemos hablado varias veces sin resolver del todo: el HPA se apoya en el requests de CPU como referencia de todo su cálculo, y ese requests sigue siendo un número que pusimos a ojo. En 07-02 lo recalibramos comparando con el consumo real, pero lo hicimos a mano, una vez, mirando kubectl top durante una tarde. Si el requests está mal, el porcentaje del HPA miente, y con él todas sus decisiones.
En la próxima lección, Autoescalado Vertical de Pods, veremos el componente que resuelve exactamente ese problema: el VPA observa el consumo real durante días, calcula percentiles y te dice con datos qué requests y qué limits deberían tener tus contenedores. Descubriremos también por qué el VPA y el HPA no pueden gobernar la misma métrica sin pelearse, y cuál es el flujo de trabajo que usan los equipos serios: el VPA como asesor, no como piloto automático.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
