La lección anterior terminó con una incomodidad. El HorizontalPodAutoscaler de api-reservas calcula todas sus decisiones como un porcentaje del requests.cpu, y ese requests.cpu: 500m es un número que alguien puso a ojo hace meses. En 07-02 lo recalibramos comparando con el consumo real, pero lo hicimos a mano, una tarde, mirando kubectl top y anotando en una hoja de cálculo. Si el requests está mal, el porcentaje del HPA miente y todas sus decisiones se apoyan en una referencia falsa.
Y el problema es más amplio que el HPA. Los requests y limits gobiernan la planificación (dónde cabe un pod), la clase de QoS (quién muere primero cuando falta memoria), el OOMKilled, el estrangulamiento de CPU, el consumo de la ResourceQuota del namespace y, en la nube, la factura. Todo eso descansa sobre números que en la mayoría de las organizaciones se escribieron una vez, copiando el manifiesto de otro servicio, y nadie ha vuelto a mirar.
El VerticalPodAutoscaler (VPA) existe para eso. No escala por carga: escala por conocimiento. Observa lo que tus contenedores consumen realmente durante días, calcula percentiles y te dice con datos qué deberían pedir. Esta lección cubre qué es, cómo funciona por dentro, cómo leer sus recomendaciones, por qué la mayoría de equipos serios lo usan como asesor y no como piloto automático, y por qué no puede convivir con el HPA sobre la misma métrica.
Contenido
- El problema real: nadie sabe qué pedir
- Qué es el VPA y qué no es
- No forma parte del núcleo: instalación
- Los tres componentes y cómo colaboran
- El objeto
VerticalPodAutoscalercampo a campo - Los modos de actualización:
Off,Initial,RecreateyAuto - El redimensionado en caliente y su estado actual
resourcePolicy: acotar, excluir y controlar- Leer una recomendación:
target,lowerBound,upperBound,uncappedTarget - El modo
Offcomo asesor: el flujo de trabajo real - La incompatibilidad clásica: VPA y HPA sobre la misma métrica
- Aplicación a Rutas Norte, componente a componente
- Interacción con LimitRange y ResourceQuota
- Los límites del VPA
- Errores comunes y consejos
- Ejercicios
- Conclusión
- El problema real: nadie sabe qué pedir
Pongamos números al problema. Estos son los requests que Rutas Norte lleva arrastrando desde el módulo 3, junto al consumo real medido durante dos semanas con Prometheus:
| Contenedor | requests.cpu |
CPU real (p95) | requests.memory |
Memoria real (p95) | Diagnóstico |
|---|---|---|---|---|---|
api-reservas / api |
500m | 340m | 512Mi | 690Mi | CPU sobrada, memoria corta |
api-reservas / exportador |
20m | 4m | 32Mi | 18Mi | Sobredimensionado x5 |
tienda-web / nginx |
200m | 55m | 128Mi | 64Mi | Sobredimensionado x3 |
worker-notificaciones |
300m | 90m | 256Mi | 410Mi | CPU sobrada, memoria corta |
informes-ocupacion |
500m | 1750m | 512Mi | 1,4Gi | Muy infradimensionado |
postgres-reservas |
2 | 1,2 | 4Gi | 3,6Gi | Razonable |
Cada fila cuenta una historia distinta y cada una cuesta dinero o disponibilidad:
api-reservascon memoria corta. Pide 512Mi y usa 690Mi en el p95. Funciona porque ellimites 1Gi y hay memoria libre en los nodos... hasta que no la hay. El día que un nodo se llena, el kubelet desaloja pods, y un podBurstableque consume por encima de surequestses de los primeros candidatos (módulo 3). Es una bomba de relojería.tienda-websobredimensionado tres veces. Con 15 réplicas en el pico son 3 núcleos reservados de los que se usan 0,8. Esos 2,2 núcleos están bloqueados para el planificador: ningún otro pod puede usarlos aunque estén ociosos. Se traduce en nodos de más.informes-ocupacionmuy infradimensionado. Pide 500m y necesita 1750m. Consecuencia: estrangulamiento brutal. Un informe que debería tardar 8 minutos tarda 40, y algunas noches no termina antes de la ventana de mantenimiento.- El exportador de métricas. 20m de
requestspara un proceso que usa 4m. Parece poco, pero multiplicado por todos los pods de la plataforma son núcleos enteros desperdiciados.
Ahora la pregunta incómoda: ¿cómo se corrige esto? La respuesta habitual —«miramos Grafana y ajustamos»— tiene tres problemas. Requiere que alguien se acuerde. Requiere que ese alguien sepa qué percentil mirar. Y hay que repetirlo cada vez que la aplicación cambia, lo que en la práctica significa nunca.
El VPA automatiza exactamente ese trabajo.
- Qué es el VPA y qué no es
El VerticalPodAutoscaler es un componente que:
- Observa el consumo histórico de CPU y memoria de los contenedores de un conjunto de pods.
- Calcula una recomendación de
requests(y opcionalmentelimits) basada en percentiles de ese histórico, con decaimiento exponencial para dar más peso a lo reciente. - Opcionalmente aplica esa recomendación, recreando los pods con los nuevos valores.
Y muy importante, lo que no es:
| El VPA... | ...pero no |
|---|---|
| Ajusta el tamaño de cada pod | Cambia el número de pods (eso es el HPA) |
| Responde a lo que la aplicación consume | Responde a la carga entrante o a la latencia |
| Corrige un dimensionado equivocado | Absorbe una punta de tráfico |
| Trabaja en escala de horas y días | Reacciona en segundos |
Esta distinción es la clave para entender la relación con el HPA. El HPA es un mecanismo de respuesta a la carga, con constante de tiempo de segundos. El VPA es un mecanismo de corrección del dimensionado, con constante de tiempo de días. No compiten en el mismo terreno... salvo cuando ambos miran la misma métrica, que es el conflicto del apartado 11.
Una forma útil de recordarlo: el HPA responde a la pregunta «¿cuántos?»; el VPA responde a la pregunta «¿de qué tamaño?».
- No forma parte del núcleo: instalación
A diferencia del HPA, que vive dentro del kube-controller-manager y está disponible en cualquier clúster, el VPA es un componente externo que hay que instalar. Vive en el repositorio kubernetes/autoscaler, junto al Cluster Autoscaler que veremos en 09-03.
Esto tiene consecuencias prácticas que conviene conocer antes de empezar:
- Debes comprobar la compatibilidad de versiones entre el VPA y tu Kubernetes. La tabla de compatibilidad está en el repositorio; usar una versión desajustada produce errores oscuros.
- Instala tres Deployments y un puñado de CRDs, RBAC y un webhook de admisión.
- En Kubernetes gestionado (10-06), muchos proveedores lo ofrecen como complemento activable con un interruptor (GKE lo tiene integrado desde hace años). Si estás en la nube, mira primero si tu proveedor te lo da hecho.
Instalación con el script oficial
# 1. Clonar el repositorio del autoescalador en la rama que corresponda
git clone -b vpa-release-1.2 https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
# 2. Ejecutar el instalador. Crea CRDs, RBAC, los tres deployments y el webhook.
./hack/vpa-up.shRequisito previo importante: el VPA necesita metrics-server (el mismo de 07-02) para el updater y el admission-controller, y su recomendador puede leer además de Prometheus si lo configuras. En minikube:
Verificación
NAME READY STATUS RESTARTS AGE
vpa-admission-controller-6b8f9d7c4d-x2klm 1/1 Running 0 2m
vpa-recommender-7d9c5f8b6b-4mnpq 1/1 Running 0 2m
vpa-updater-5f7b8c9d6a-9wrtz 1/1 Running 0 2mY los CRDs registrados:
verticalpodautoscalers.autoscaling.k8s.io 2026-04-12T09:14:22Z
verticalpodautoscalercheckpoints.autoscaling.k8s.io 2026-04-12T09:14:22ZEl segundo CRD, VerticalPodAutoscalerCheckpoint, merece una nota: ahí el recomendador persiste el histograma de consumo cada pocos minutos. Sin él, un reinicio del recomendador perdería todo el histórico acumulado y habría que volver a empezar el periodo de observación. Cuando alguien te diga «reinicié el VPA y ahora las recomendaciones son raras», el checkpoint es lo primero que hay que mirar.
Para desinstalar:
- Los tres componentes y cómo colaboran
El VPA no es un proceso, son tres, con responsabilidades bien separadas. Entender esta separación explica casi todo su comportamiento.
flowchart TD
subgraph Fuentes["Fuentes de datos"]
MS[metrics-server<br/>metrics.k8s.io]
PR[Prometheus<br/>opcional, historico largo]
end
subgraph VPA["Componentes del VPA"]
REC[Recomendador<br/>vpa-recommender]
UPD[Actualizador<br/>vpa-updater]
ADM[Controlador de admision<br/>vpa-admission-controller]
end
OBJ[(Objeto VPA<br/>status.recommendation)]
CKP[(Checkpoint<br/>histograma persistido)]
API[Servidor de API]
POD[Pods de api-reservas]
MS --> REC
PR -.-> REC
REC -->|escribe recomendacion| OBJ
REC <-->|guarda y restaura| CKP
OBJ -->|lee| UPD
OBJ -->|lee| ADM
UPD -->|API de desalojo<br/>solo si updateMode lo permite| API
API -->|recrea el pod| POD
POD -->|peticion de creacion| ADM
ADM -->|MUTA los recursos<br/>antes de persistir| API
style REC fill:#cde,stroke:#369
style UPD fill:#fda,stroke:#960
style ADM fill:#cfc,stroke:#393
El recomendador (vpa-recommender)
Es el cerebro. Su trabajo:
- Lee el consumo de CPU y memoria de todos los pods que coinciden con algún
targetRefde un objeto VPA. - Mantiene, por contenedor, un histograma con decaimiento exponencial: las muestras viejas pesan menos que las recientes. La semivida por defecto es de 24 horas, lo que significa que una muestra de hace un día vale la mitad que una de ahora.
- Calcula percentiles sobre ese histograma y escribe la recomendación en
status.recommendationdel objeto VPA. - Persiste el histograma en un
VerticalPodAutoscalerCheckpointpara sobrevivir a reinicios.
Los percentiles que usa por defecto:
| Recurso | Percentil para target |
Margen añadido |
|---|---|---|
| CPU | p90 del histograma | +15 % de seguridad |
| Memoria | Máximo de las ventanas de 24 h de los últimos 8 días | +15 % de seguridad |
La asimetría es deliberada y muy sensata. Quedarse corto de CPU produce lentitud; quedarse corto de memoria produce OOMKilled. Por eso la memoria se calcula con el pico y la CPU con un percentil alto pero no extremo. Un contenedor puede sobrevivir perfectamente a picos de CPU por encima de su requests (se los presta el nodo si hay); a un pico de memoria por encima de su limit, no.
Detalle adicional: el recomendador observa los eventos OOMKilled y, cuando detecta uno, sube la recomendación de memoria de golpe (aproximadamente un 20 %) sin esperar a que el histograma se ajuste. Es una realimentación directa desde el fallo.
El recomendador funciona siempre, sea cual sea el updateMode. Incluso en modo Off está trabajando y publicando recomendaciones. Esto es precisamente lo que hace útil el modo asesor.
El actualizador (vpa-updater)
Es el brazo ejecutor. Cada minuto:
- Compara los recursos actuales de cada pod con la recomendación vigente.
- Si la diferencia supera un umbral (por defecto, si el valor actual está fuera del rango
[lowerBound, upperBound]), decide que el pod hay que recrearlo. - Desaloja el pod usando la API de desalojo (eviction), la misma que usa
kubectl drain. Esto es fundamental: el actualizador respeta los PodDisruptionBudgets (09-05). Si tu PDB no permite el desalojo, el VPA espera pacientemente. - No modifica el pod: lo mata. El Deployment lo recrea, y el nuevo pod pasa por el controlador de admisión.
Dos salvaguardas que evitan desastres:
- No desaloja un pod si no han pasado al menos 12 horas desde su creación (evita ciclos de recreación).
- Nunca desaloja todos los pods de un mismo controlador a la vez.
El actualizador solo actúa en los modos Recreate y Auto. En Off e Initial está inactivo.
El controlador de admisión (vpa-admission-controller)
Es un MutatingAdmissionWebhook registrado en la API. Cuando llega una petición de creación de pod:
- Comprueba si ese pod coincide con algún VPA activo.
- Si sí, reescribe los
requests(y loslimits, manteniendo la proporción) del pod antes de que se persista en etcd. - El pod nace ya con los valores recomendados. El planificador lo coloca con esos números.
Este es el componente que hace que la recreación funcione. Sin él, el actualizador mataría un pod y el Deployment lo recrearía con los valores originales del manifiesto: un bucle infinito de desalojos inútiles.
Una consecuencia importante y a menudo sorprendente: el VPA no modifica el Deployment. Si haces kubectl get deployment api-reservas -o yaml seguirás viendo requests.cpu: 500m. Los pods reales tendrán otros valores. La mutación ocurre en el nivel del pod, en admisión. Para ver los valores efectivos:
kubectl get pod api-reservas-7c9d4f8b6d-2mk8p -n rutas-norte-pro \
-o jsonpath='{.spec.containers[*].resources}' | python3 -m json.toolUn riesgo derivado: si el webhook de admisión está caído o mal configurado, según su failurePolicy puede bloquear la creación de todos los pods del clúster o dejar que pasen sin mutar. El instalador oficial lo pone en Ignore por defecto, lo cual es lo seguro.
- El objeto
VerticalPodAutoscaler campo a campo
VerticalPodAutoscaler campo a campoapiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-reservas
namespace: rutas-norte-pro
spec:
# A QUE recurso se aplica. Mismo formato que el scaleTargetRef del HPA.
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api-reservas
# QUE hace con la recomendacion.
updatePolicy:
updateMode: "Off" # Off | Initial | Recreate | Auto
minReplicas: 2 # No desaloja si quedarian menos de N replicas
# LIMITES de la recomendacion, por contenedor.
resourcePolicy:
containerPolicies:
- containerName: api
minAllowed:
cpu: 200m
memory: 256Mi
maxAllowed:
cpu: "2"
memory: 2Gi
controlledResources: ["cpu", "memory"]
controlledValues: RequestsAndLimits
- containerName: exportador-metricas
mode: "Off" # Este contenedor queda excluido| Campo | Tipo | Qué hace |
|---|---|---|
targetRef |
objeto | Deployment, StatefulSet, DaemonSet, CronJob... cualquier controlador de pods |
updatePolicy.updateMode |
cadena | Qué hacer con la recomendación (apartado 6) |
updatePolicy.minReplicas |
entero | El actualizador no desaloja si el controlador tiene menos réplicas que esto. Por defecto 2 |
resourcePolicy.containerPolicies[].containerName |
cadena | Nombre del contenedor, o "*" para todos |
.minAllowed / .maxAllowed |
recursos | Suelo y techo de la recomendación |
.mode |
cadena | Auto (defecto) o Off para excluir este contenedor |
.controlledResources |
lista | ["cpu"], ["memory"] o ambos |
.controlledValues |
cadena | RequestsAndLimits (defecto) o RequestsOnly |
Nota sobre targetRef: a diferencia del HPA, no requiere que el objeto exponga el subrecurso /scale. Por eso el VPA sí puede aplicarse a un DaemonSet, mientras que el HPA no. Esto es útil de verdad: los agentes de Fluentd y de Falco que desplegamos en los módulos 7 y 8 son DaemonSets, y sus requests son igual de arbitrarios que los demás.
- Los modos de actualización:
Off, Initial, Recreate y Auto
Off, Initial, Recreate y AutoEl updateMode es la decisión más importante del objeto.
| Modo | El recomendador | Muta pods nuevos | Desaloja pods vivos | Riesgo | Uso típico |
|---|---|---|---|---|---|
Off |
Sí | No | No | Ninguno | Asesoría. El más usado en producción |
Initial |
Sí | Sí | No | Bajo | Aplicar en el siguiente despliegue, sin reinicios extra |
Recreate |
Sí | Sí | Sí | Medio-alto | Cargas que toleran reinicios |
Auto |
Sí | Sí | Sí (hoy = Recreate) |
Medio-alto | Igual que Recreate hoy |
Off — el asesor
El recomendador observa y publica en status.recommendation. Nada más. Ningún pod se toca, ningún manifiesto cambia. Es información pura.
Coste operativo: cero. Riesgo: cero. Valor: alto. Por eso es, con diferencia, el modo más usado en producción seria.
Initial — aplicar solo al crear
El controlador de admisión muta los pods cuando nacen por otra razón: un despliegue nuevo, un reinicio por caída de nodo, un escalado del HPA. El actualizador nunca desaloja nada.
Es un punto intermedio interesante: los valores se aplican de verdad, pero sin causar ni un solo reinicio adicional. La contrapartida es la lentitud: si api-reservas se despliega cada dos semanas, las recomendaciones tardan dos semanas en materializarse. Y crea un efecto desconcertante: dos pods del mismo Deployment, creados en momentos distintos, pueden tener recursos distintos.
Recreate — aplicar desalojando
El actualizador desaloja activamente los pods cuyos recursos se han desviado. Es autoescalado vertical de verdad, con la consecuencia inevitable: cada ajuste es un reinicio del pod.
Para worker-notificaciones es aceptable (procesa mensajes de una cola, un reinicio pierde como mucho el mensaje en vuelo, que la cola reentrega). Para postgres-reservas es otra historia, y lo discutimos en el apartado 12.
minReplicas: 2 es la protección esencial: el actualizador no desalojará si el controlador tiene menos réplicas que ese número. Con una sola réplica, un desalojo es una caída completa del servicio.
Auto — hoy es Recreate
Semánticamente significa «usa el mejor mecanismo disponible». Hoy, en la práctica, Auto se comporta exactamente igual que Recreate en la mayoría de instalaciones. La intención declarada del proyecto es que, cuando el redimensionado en caliente sea estable, Auto lo use y deje de recrear pods.
Consejo operativo: usa Recreate explícitamente si eso es lo que quieres. Así, cuando el significado de Auto cambie con una actualización del VPA, tu comportamiento no cambiará por sorpresa.
- El redimensionado en caliente y su estado actual
Durante años, la limitación fundamental del escalado vertical en Kubernetes fue que los recursos de un contenedor eran inmutables: cambiarlos exigía recrear el pod.
Eso está cambiando. La funcionalidad se llama In-Place Pod Vertical Scaling (redimensionado vertical de pods en caliente), y permite modificar requests y limits de un contenedor en ejecución sin recrear el pod, mediante el subrecurso /resize.
Cómo funciona
Cada contenedor puede declarar una política de reinicio por recurso:
spec:
containers:
- name: api
image: registry.rutasnorte.example/api-reservas:1.14.2
resizePolicy:
- resourceName: cpu
restartPolicy: NotRequired # La CPU se ajusta en caliente
- resourceName: memory
restartPolicy: RestartContainer # La memoria exige reiniciar el contenedor
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "1"
memory: 1GiY el cambio se aplica con:
kubectl patch pod api-reservas-7c9d4f8b6d-2mk8p -n rutas-norte-pro \
--subresource resize \
--patch '{"spec":{"containers":[{"name":"api","resources":{"requests":{"cpu":"800m"}}}]}}'El estado del proceso queda reflejado en el pod:
status:
resize: InProgress
containerStatuses:
- name: api
allocatedResources:
cpu: 500m
resources:
requests:
cpu: 800mEstado actual y advertencia
Esta es la parte importante para tomar decisiones:
- La funcionalidad ha avanzado a beta en Kubernetes 1.33 y sigue madurando. En 1.30-1.32 es alpha y requiere activar la feature gate
InPlacePodVerticalScalingen el servidor de API y en los kubelets. - Reducir la memoria en caliente no está resuelto de forma general: bajar el
limitde memoria por debajo de lo que el proceso ya tiene mapeado puede provocar unOOMKilledinmediato. Por eso la política habitual para memoria esRestartContainer. - La integración del VPA con esta funcionalidad existe pero no está completa ni es el camino por defecto.
- En Kubernetes gestionado (10-06), la feature gate puede no estar disponible: no controlas el plano de control.
Advertencia explícita: no construyas la estrategia de capacidad de producción sobre el redimensionado en caliente todavía. Es la dirección correcta y en un par de versiones será la forma natural de hacer las cosas, pero hoy:
- Puede no estar disponible en tu clúster.
- Su comportamiento con memoria tiene aristas.
- El VPA no la explota plenamente.
Conócela, pruébala en rutas-norte-dev, síguele la pista. En rutas-norte-pro, el flujo de trabajo del apartado 10.
resourcePolicy: acotar, excluir y controlar
resourcePolicy: acotar, excluir y controlarDejar que un recomendador escriba números sin barandillas es una mala idea. El resourcePolicy es donde pones el criterio humano.
Acotar mínimos y máximos
resourcePolicy:
containerPolicies:
- containerName: api
minAllowed:
cpu: 200m # Nunca por debajo: el arranque de Node.js necesita CPU
memory: 256Mi # Nunca por debajo: el heap base ya ocupa esto
maxAllowed:
cpu: "2" # Nunca por encima: cabria en pocos nodos
memory: 2Gi # Nunca por encima: sospecharia de una fuga de memoriaPor qué cada barandilla:
minAllowed. El recomendador se basa en el consumo observado. Si un servicio pasa la noche sin tráfico, su p90 de CPU es casi cero, y sin minAllowed la recomendación bajaría a 15m. Cuando llega el tráfico de la mañana, el pod se ahoga arrancando. El minAllowed codifica «esto es lo mínimo para arrancar y responder decentemente, aunque las métricas digan otra cosa».
maxAllowed. Dos motivos. Primero, planificación: un pod de 8 núcleos solo cabe en nodos grandes, y si tus nodos son de 4 núcleos ese pod se queda Pending para siempre. Segundo, detección de anomalías: si la recomendación llega al techo, hay algo que investigar (una fuga de memoria, una consulta patológica). El maxAllowed convierte un problema silencioso en una señal.
Regla práctica para el maxAllowed: nunca por encima de lo que cabe en un nodo real, con margen para el sistema. En nodos de 4 núcleos y 16 GiB, un maxAllowed razonable ronda 3 núcleos y 12Gi.
Excluir un contenedor
mode: "Off" a nivel de contenedor lo saca del VPA por completo: no se recomienda ni se muta. Cuándo hacerlo:
- Sidecars con recursos ya bien conocidos y estables. Un exportador de Prometheus consume 5m y 20Mi hoy, mañana y siempre. No necesita un recomendador.
- Sidecars de malla de servicio (el proxy de Istio, 08-04). Suelen tener su propia gestión de recursos y el VPA se pisa con ella.
- Contenedores con
requestsdeliberadamente altos por razones que las métricas no ven: reservar CPU para picos de latencia crítica, por ejemplo.
Ojo con un detalle: si excluyes todos los contenedores del pod, el VPA no hará nada, lo cual es correcto, pero el objeto sigue existiendo y confunde. Bórralo mejor.
Comodín para todos los contenedores
resourcePolicy:
containerPolicies:
- containerName: "*"
minAllowed:
cpu: 10m
memory: 32Mi
maxAllowed:
cpu: "1"
memory: 1Gi
- containerName: api # La regla especifica GANA sobre el comodin
maxAllowed:
cpu: "2"
memory: 2GicontrolledResources y controlledValues
- containerName: api
controlledResources: ["memory"] # Solo memoria; la CPU la deja en paz
controlledValues: RequestsOnly # Solo requests; no toca los limitscontrolledResources es la pieza clave para convivir con el HPA: si el HPA escala por CPU, el VPA debe limitarse a la memoria (apartado 11).
controlledValues decide si el VPA toca también los limits:
| Valor | Comportamiento | Consecuencia en QoS |
|---|---|---|
RequestsAndLimits (defecto) |
Ajusta ambos, manteniendo la proporción original | Preserva la QoS: si era Guaranteed, sigue siéndolo |
RequestsOnly |
Solo ajusta requests, deja los limits como están |
Puede cambiar la QoS de Guaranteed a Burstable |
El detalle de la proporción merece un ejemplo. Si el manifiesto tiene requests.cpu: 500m y limits.cpu: 1000m (proporción 1:2) y el VPA recomienda requests.cpu: 800m, con RequestsAndLimits pondrá limits.cpu: 1600m para mantener el 1:2. Es un comportamiento sensato pero que sorprende: el VPA puede subirte los limits por encima de lo que esperabas, y eso interactúa con la ResourceQuota del namespace (apartado 13).
- Leer una recomendación:
target, lowerBound, upperBound, uncappedTarget
target, lowerBound, upperBound, uncappedTargetAquí está el valor real del VPA en modo Off. Veamos una recomendación completa.
Name: api-reservas
Namespace: rutas-norte-pro
API Version: autoscaling.k8s.io/v1
Kind: VerticalPodAutoscaler
Spec:
Target Ref:
API Version: apps/v1
Kind: Deployment
Name: api-reservas
Update Policy:
Update Mode: Off
Status:
Conditions:
Type: RecommendationProvided
Status: True
Last Transition Time: 2026-05-11T08:22:14Z
Recommendation:
Container Recommendations:
Container Name: api
Lower Bound:
Cpu: 287m
Memory: 573Mi
Target:
Cpu: 412m
Memory: 792Mi
Uncapped Target:
Cpu: 412m
Memory: 792Mi
Upper Bound:
Cpu: 1140m
Memory: 1489Mi
Container Name: exportador-metricas
Lower Bound:
Cpu: 4m
Memory: 14Mi
Target:
Cpu: 6m
Memory: 21Mi
Uncapped Target:
Cpu: 6m
Memory: 21Mi
Upper Bound:
Cpu: 18m
Memory: 48MiQué significa cada campo
| Campo | Significado | Qué hacer con él |
|---|---|---|
target |
El valor recomendado. La estimación del recomendador de lo que el contenedor necesita | Este es el que llevas al manifiesto |
lowerBound |
Por debajo de esto, el contenedor está claramente falto de recursos | Si tu valor actual está por debajo, actúa ya |
upperBound |
Por encima de esto, estás desperdiciando de forma clara | Si tu valor actual está por encima, estás pagando de más |
uncappedTarget |
El target antes de aplicar minAllowed/maxAllowed |
Si difiere de target, tus barandillas están recortando |
Los límites lowerBound y upperBound no son percentiles del consumo: son el intervalo de confianza del recomendador. Cuanto menos histórico tenga, más ancho será. Un VPA con dos horas de datos dará un upperBound enorme; con dos semanas, un intervalo estrecho. La anchura del intervalo es un indicador directo de cuánto puedes fiarte de la recomendación.
Y son también el criterio de actuación del actualizador: en modos Recreate/Auto, el pod se desaloja cuando su valor actual sale del rango [lowerBound, upperBound]. Dentro del rango, se deja en paz. Esta banda es el equivalente vertical de la banda de tolerancia del HPA.
Interpretación para api-reservas
Contrastemos con lo que tenemos en el manifiesto:
| Recurso | Actual | lowerBound |
target |
upperBound |
Veredicto |
|---|---|---|---|---|---|
| CPU (api) | 500m | 287m | 412m | 1140m | Dentro del rango, pero un 21 % de más. Ajuste opcional |
| Memoria (api) | 512Mi | 573Mi | 792Mi | 1489Mi | Por debajo del lowerBound. Corregir ya |
| CPU (exportador) | 20m | 4m | 6m | 18m | Por encima del upperBound. Sobredimensionado |
| Memoria (exportador) | 32Mi | 14Mi | 21Mi | 48Mi | Dentro del rango, ajuste menor |
La fila crítica es la memoria de api: 512Mi está por debajo del lowerBound de 573Mi. El VPA está diciendo, con datos de dos semanas, que ese contenedor tiene menos memoria reservada de la que necesita para funcionar con seguridad. Es exactamente el riesgo que intuíamos en el apartado 1, ahora cuantificado.
Y el exportador confirma la otra intuición: 20m reservados para algo que necesita 6m. Con 30 réplicas de api-reservas en el pico, son 420m de CPU reservados y nunca usados: casi medio núcleo bloqueado para el planificador.
uncappedTarget: la señal de que tus barandillas aprietan
Supongamos que ponemos maxAllowed.memory: 512Mi en el resourcePolicy. La recomendación quedaría:
Target:
Memory: 512Mi <- Recortado por maxAllowed
Uncapped Target:
Memory: 792Mi <- Lo que el recomendador queria de verdadCuando target y uncappedTarget difieren, tu maxAllowed (o minAllowed) está mordiendo. Y aquí hay que pensar dos veces, porque hay dos interpretaciones opuestas y ambas son plausibles:
- La barandilla está mal puesta. Fue un número conservador de hace meses y la aplicación ha crecido legítimamente. Súbela.
- La barandilla está haciendo su trabajo. Hay una fuga de memoria y el consumo crece sin parar. El
maxAllowedes lo único que impide que ese pod se coma un nodo entero.
Distinguir entre las dos requiere mirar la tendencia en Grafana (07-04): si la memoria crece de forma monótona y nunca baja, es fuga; si se estabiliza en una meseta más alta, es crecimiento legítimo. Un uncappedTarget que se aleja del target mes a mes es una fuga de memoria hasta que se demuestre lo contrario.
Extraer la recomendación por línea de comandos
Para automatizar el flujo de trabajo:
kubectl get vpa api-reservas -n rutas-norte-pro \
-o jsonpath='{range .status.recommendation.containerRecommendations[*]}{.containerName}{"\t"}{.target.cpu}{"\t"}{.target.memory}{"\n"}{end}'Salida limpia, apta para meterla en un informe semanal o en un comentario automático de una pull request.
- El modo
Off como asesor: el flujo de trabajo real
Off como asesor: el flujo de trabajo realAquí está la práctica que usan los equipos serios, y merece explicarse bien porque va a contracorriente de lo que sugiere el nombre «autoescalador».
La mayoría de organizaciones con Kubernetes en producción usan el VPA exclusivamente en modo Off. No como piloto automático, sino como asesor cuya recomendación revisa una persona antes de llegar a producción.
Por qué
| Motivo | Explicación |
|---|---|
| El manifiesto sigue siendo la verdad | Con Recreate, el YAML dice 500m y el pod tiene 412m. Nadie sabe qué recursos tiene el sistema mirando el repositorio |
| Cada ajuste es un reinicio | En modo activo, el VPA reinicia pods por su cuenta, en cualquier momento. Un reinicio no planificado en el puente de mayo es intolerable |
| Compatibilidad con GitOps | Con Argo CD o Flux (10-05), el VPA activo crea una divergencia permanente entre Git y el clúster. El mismo problema del replicas de 09-01 |
| Revisión humana | Un cambio de recursos merece pasar por una revisión de código. «La memoria de api-reservas sube de 512Mi a 800Mi» es una decisión con consecuencias de coste y capacidad |
| Auditoría | El registro de cambios de recursos queda en el historial de Git, no en logs de un controlador |
El flujo de trabajo, paso a paso
flowchart TD
A[1. Crear el VPA en modo Off] --> B[2. Esperar 2 semanas<br/>incluyendo un ciclo de negocio completo]
B --> C[3. Extraer la recomendacion<br/>kubectl get vpa]
C --> D{4. target dentro de<br/>las barandillas?}
D -- No --> E[Investigar: fuga o<br/>crecimiento legitimo?]
E --> C
D -- Si --> F[5. Comparar con el manifiesto actual]
F --> G{6. Diferencia > 20 %?}
G -- No --> H[No tocar. Volver en un mes]
G -- Si --> I[7. Pull request con los nuevos valores<br/>y la recomendacion en la descripcion]
I --> J[8. Revision del equipo]
J --> K[9. Merge, despliegue en pre, validacion]
K --> L[10. Despliegue en pro]
L --> M[11. Verificar en Grafana:<br/>menos throttling? menos OOM?]
M --> B
Paso 2, dos semanas y por qué. El histograma del recomendador tiene semivida de 24 horas, así que con dos o tres días ya hay señal. Pero dos semanas cubren dos ciclos semanales completos, incluidos fines de semana. En Rutas Norte esto importa mucho: el tráfico de sábado no se parece al de miércoles. Y si hay un cierre mensual o un evento periódico, hay que cubrirlo también.
Paso 6, el umbral del 20 %. Cambiar los recursos de un Deployment implica desplegarlo. No merece la pena hacerlo por un 5 %. El umbral del 20 % es una convención razonable: por debajo, el ruido supera a la señal.
Paso 7, la pull request. Aquí está el corazón del método. El cambio queda documentado:
Titulo: Ajustar recursos de api-reservas segun recomendacion del VPA
Recomendacion del VPA (14 dias de observacion, 2026-04-27 a 2026-05-11):
Contenedor Actual Recomendado Cambio
api / cpu 500m 412m -17,6 %
api / memoria 512Mi 792Mi +54,7 %
exportador / cpu 20m 6m -70,0 %
exportador / memoria 32Mi 21Mi -34,4 %
Justificacion:
- La memoria de "api" estaba POR DEBAJO del lowerBound (573Mi). Riesgo real
de desalojo bajo presion de memoria del nodo. Es la correccion importante.
- La CPU de "api" baja un 17,6 %. Ojo: esto afecta al HPA, que calcula sobre
el requests. Ver nota mas abajo.
- El exportador estaba sobredimensionado x3. Con 30 replicas en el pico son
420m de CPU liberados para el planificador.
Nota sobre el HPA: al bajar requests.cpu de 500m a 412m, el objetivo absoluto
del HPA pasa de 325m a 268m por pod. El HPA escalara ANTES que ahora con la
misma carga. Es el efecto deseado (los pods estaban sobredimensionados), pero
hay que vigilar el numero de replicas en el pico durante la primera semana.
Validado en rutas-norte-pre con la prueba de carga de k6 (09-06): sin
throttling, sin OOM, latencia p95 igual o mejor.Esa nota sobre el HPA es exactamente el tipo de razonamiento que se pierde cuando dejas que un controlador cambie los números por su cuenta.
El VPA en modo Off para toda la plataforma
Una práctica muy rentable: un VPA en modo Off sobre todos los Deployments, StatefulSets y DaemonSets de la plataforma, aunque nunca los apliques automáticamente. El coste es un objeto por controlador y algo de memoria en el recomendador. El beneficio es tener, en cualquier momento, la respuesta a «¿está bien dimensionado esto?».
# Generar un VPA en modo Off para cada Deployment del namespace
for d in $(kubectl get deploy -n rutas-norte-pro -o name | cut -d/ -f2); do
cat <<EOF | kubectl apply -f -
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: $d
namespace: rutas-norte-pro
labels:
app.kubernetes.io/part-of: rutas-norte
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: $d
updatePolicy:
updateMode: "Off"
EOF
doneY un informe semanal comparando recomendación con manifiesto. Ese informe, revisado cada lunes en quince minutos, es probablemente la actividad de mayor retorno por minuto invertido en la operación de un clúster.
- La incompatibilidad clásica: VPA y HPA sobre la misma métrica
Esta es la trampa que hay que conocer sí o sí.
El conflicto
El VPA y el HPA no pueden gobernar el mismo recurso (CPU o memoria) sobre la misma carga de trabajo. Si lo intentas, se retroalimentan de forma destructiva.
Veamos por qué, con api-reservas y números concretos. Supongamos HPA por CPU con averageUtilization: 65 y VPA en modo Recreate controlando CPU.
Estado inicial: 4 replicas, requests.cpu 500m, objetivo HPA 325m/pod.
Carga total: 1600m.
t=0 Consumo: 400m/pod (1600 / 4). Ratio 400/325 = 1,23.
HPA -> techo(4 x 1,23) = 5 replicas.
t=1min 5 replicas, consumo 320m/pod. HPA satisfecho (ratio 0,98).
Pero el VPA observa 320m de consumo y recomienda requests.cpu = 368m
(320m x 1,15 de margen).
t=15m El VPA aplica 368m. AHORA el objetivo del HPA es 65 % de 368m = 239m.
Consumo real sigue en 320m. Ratio = 320/239 = 1,34.
HPA -> techo(5 x 1,34) = 7 replicas.
t=16m 7 replicas, consumo 229m/pod. HPA satisfecho.
El VPA observa 229m y recomienda requests.cpu = 263m.
t=30m El VPA aplica 263m. Objetivo del HPA = 171m. Consumo 229m.
Ratio 1,34. HPA -> 10 replicas.
t=31m 10 replicas, consumo 160m/pod. El VPA recomienda 184m...El bucle es evidente: el HPA reduce el consumo por pod añadiendo réplicas; el VPA interpreta ese consumo menor como que los pods son grandes de más y baja el requests; al bajar el requests, el objetivo absoluto del HPA baja, y el HPA vuelve a escalar. La carga total no ha cambiado ni un vatio, pero hemos pasado de 4 réplicas de 500m a 10 réplicas de 263m, y el proceso continúa.
Cada iteración implica recrear pods (VPA) y crear/destruir pods (HPA). Es una espiral de inestabilidad que además hace muy difícil el diagnóstico, porque cada componente por separado parece estar comportándose bien.
flowchart LR
A[Carga constante] --> B[HPA anade replicas]
B --> C[Baja el consumo POR POD]
C --> D[VPA baja el requests]
D --> E[Baja el objetivo absoluto del HPA]
E --> B
style B fill:#fdd,stroke:#c00
style D fill:#fdd,stroke:#c00
Las combinaciones válidas
| HPA escala por | VPA controla | ¿Válido? | Comentario |
|---|---|---|---|
| CPU | CPU | NO | El bucle descrito arriba |
| Memoria | Memoria | NO | Mismo bucle |
| CPU | Solo memoria | SÍ | Combinación recomendada. Ejes independientes |
| Memoria | Solo CPU | Sí, técnicamente | Pero escalar por memoria ya es mala idea (09-01) |
| Métrica personalizada / externa | CPU y memoria | SÍ | La mejor combinación de todas |
| — (sin HPA) | CPU y memoria | SÍ | El caso simple |
La configuración recomendada para api-reservas, si quisieras VPA activo:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-reservas-memoria
namespace: rutas-norte-pro
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api-reservas
updatePolicy:
updateMode: "Recreate"
minReplicas: 2
resourcePolicy:
containerPolicies:
- containerName: api
# CLAVE: solo memoria. La CPU la gobierna el HPA de 09-01.
controlledResources: ["memory"]
minAllowed:
memory: 512Mi
maxAllowed:
memory: 2Gi
- containerName: exportador-metricas
controlledResources: ["memory"]
minAllowed:
memory: 16Mi
maxAllowed:
memory: 128MicontrolledResources: ["memory"] es la línea que hace que la convivencia sea posible. Sin ella, tienes el bucle.
La combinación óptima: HPA por métrica de negocio + VPA completo
La configuración más elegante aparece cuando el HPA escala por una métrica que no es CPU ni memoria: peticiones por segundo, longitud de una cola. Entonces:
- El HPA decide cuántos pods, según la demanda del negocio.
- El VPA decide de qué tamaño, según el consumo observado.
- No hay realimentación: la métrica de negocio no depende del
requests.
Es exactamente lo que habilita KEDA en 09-04. Cuando worker-notificaciones escale por longitud de cola, un VPA completo sobre él será perfectamente seguro y muy útil.
El VPA en modo Off es siempre seguro
Y esto conviene subrayarlo: el conflicto solo existe cuando el VPA actúa. En modo Off, el VPA no cambia nada, así que puede convivir con cualquier HPA sobre cualquier métrica sin el menor riesgo. Su recomendación de CPU será algo pesimista (medirá pods a los que el HPA ya ha aliviado la carga), pero seguirá siendo información válida.
Este es otro argumento a favor del modo Off: elimina toda una clase de problemas de un plumazo.
- Aplicación a Rutas Norte, componente a componente
Decisiones concretas y justificadas para cada pieza de la plataforma.
api-reservas y tienda-web: VPA en modo Off
Ambos tienen HPA por CPU desde 09-01. Ponemos VPA en modo Off, como asesores.
# k8s/entornos/pro/vpa-api-reservas.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: api-reservas
namespace: rutas-norte-pro
labels:
app: api-reservas
app.kubernetes.io/part-of: rutas-norte
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: api-reservas
updatePolicy:
# MODO OFF. api-reservas tiene un HPA por CPU (09-01). Un VPA activo sobre
# CPU provocaria el bucle de realimentacion. Y aunque limitasemos el VPA a
# memoria, no queremos reinicios no planificados en el componente que
# sostiene la venta. Aqui el VPA es un ASESOR: revisamos su recomendacion
# cada lunes y la llevamos al manifiesto en una pull request.
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: api
minAllowed:
cpu: 200m
memory: 256Mi
maxAllowed:
cpu: "2"
memory: 2Gi
- containerName: exportador-metricas
minAllowed:
cpu: 5m
memory: 16Mi
maxAllowed:
cpu: 100m
memory: 128Mi# k8s/entornos/pro/vpa-tienda-web.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: tienda-web
namespace: rutas-norte-pro
labels:
app: tienda-web
app.kubernetes.io/part-of: rutas-norte
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: tienda-web
updatePolicy:
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: nginx
minAllowed:
cpu: 50m
memory: 64Mi
maxAllowed:
cpu: 500m
memory: 512MiCon las mediciones del apartado 1, tienda-web es candidato claro a bajar de 200m a unos 70m de CPU. Con 15 réplicas en el pico, eso libera casi dos núcleos: menos nodos que arrancar (09-03) y menos factura.
worker-notificaciones: VPA en modo Recreate
Aquí sí lo activamos, y es el mejor candidato de la plataforma.
# k8s/entornos/pro/vpa-worker-notificaciones.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: worker-notificaciones
namespace: rutas-norte-pro
labels:
app: worker-notificaciones
app.kubernetes.io/part-of: rutas-norte
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: worker-notificaciones
updatePolicy:
# MODO RECREATE. Este componente consume mensajes de una cola: un reinicio
# no pierde trabajo (la cola reentrega el mensaje en vuelo) y no hay ningun
# usuario esperando una respuesta. Es la carga que MEJOR tolera el VPA activo.
updateMode: "Recreate"
# Nunca desalojar si quedasen menos de 2 replicas: la cola no puede quedarse
# sin consumidores ni un minuto durante el puente de mayo.
minReplicas: 2
resourcePolicy:
containerPolicies:
- containerName: worker
minAllowed:
cpu: 50m
memory: 128Mi
maxAllowed:
cpu: "1"
memory: 1Gi
# CPU y memoria, ambas: en 09-04 este componente pasara a escalar por
# LONGITUD DE COLA con KEDA, que es una metrica externa. No hay conflicto
# con la CPU, asi que el VPA puede gobernarla sin problema.
controlledResources: ["cpu", "memory"]
- containerName: exportador-metricas
mode: "Off"informes-ocupacion: VPA sobre el CronJob
El caso más rentable de todos. Recordemos: pide 500m y necesita 1750m. Se estrangula, tarda cinco veces más de lo debido y algunas noches no termina.
# k8s/entornos/pro/vpa-informes-ocupacion.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: informes-ocupacion
namespace: rutas-norte-pro
labels:
app: informes-ocupacion
app.kubernetes.io/part-of: rutas-norte
spec:
targetRef:
apiVersion: batch/v1
kind: CronJob # El VPA soporta CronJob directamente
name: informes-ocupacion
updatePolicy:
# MODO INITIAL, no Recreate. Un Job es efimero: no tiene sentido desalojarlo
# a mitad de ejecucion (perderiamos el trabajo hecho y empezaria de cero).
# Con Initial, cada ejecucion NUEVA nace con los recursos correctos.
updateMode: "Initial"
resourcePolicy:
containerPolicies:
- containerName: generador
minAllowed:
cpu: 200m
memory: 256Mi
maxAllowed:
cpu: "4"
memory: 4Gi # Techo generoso: es un proceso por lotes que se
# beneficia de todo lo que le des, y se ejecuta de
# madrugada cuando el cluster esta vacio
controlledResources: ["cpu", "memory"]Initial es la elección correcta para trabajos por lotes, y merece la pena entender por qué: un Job que se desaloja a los diez minutos de una ejecución de treinta pierde todo el trabajo y empieza de nuevo. Con Initial, cada ejecución nocturna nace con los recursos que el recomendador ha aprendido de las ejecuciones anteriores. El VPA aprende de la noche del lunes para la noche del martes.
Efecto esperado: de 40 minutos a unos 9. Y como se ejecuta a las 3 de la madrugada, con el clúster vacío, esos recursos extra no le quitan nada a nadie.
postgres-reservas: la decisión difícil
Aquí hay que pensar. Los datos:
- Es un StatefulSet con una sola réplica primaria.
- Tiene QoS
Guaranteed(requests=limits), decisión deliberada del módulo 3 para que sea el último candidato al desalojo. - Un reinicio implica cortar todas las conexiones, perder la caché de páginas de PostgreSQL y unos segundos de indisponibilidad total de la plataforma.
- Guarda datos personales de clientes: cualquier riesgo de corrupción es inaceptable.
Decisión: VPA en modo Off, con controlledValues: RequestsAndLimits.
# k8s/entornos/pro/vpa-postgres-reservas.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: postgres-reservas
namespace: rutas-norte-pro
labels:
app: postgres-reservas
app.kubernetes.io/part-of: rutas-norte
spec:
targetRef:
apiVersion: apps/v1
kind: StatefulSet
name: postgres-reservas
updatePolicy:
# MODO OFF, sin discusion. Razones:
#
# 1. UNA SOLA REPLICA PRIMARIA. Un desalojo es una caida total de la
# plataforma. Ni siquiera minReplicas nos protege: con 1 replica el
# updater no actuaria, pero no queremos depender de eso.
#
# 2. QoS GUARANTEED. Con controlledValues: RequestsAndLimits el VPA
# mantiene la proporcion requests:limits, que aqui es 1:1, asi que la
# QoS se preservaria. Pero es una propiedad demasiado importante para
# dejarla en manos de un controlador.
#
# 3. EL REINICIO NO ES GRATIS. PostgreSQL pierde la cache de paginas y las
# primeras consultas tras el arranque van a disco. La latencia empeora
# durante minutos.
#
# 4. EL DIMENSIONADO DE UNA BASE DE DATOS NO SE DEDUCE DEL CONSUMO. El
# shared_buffers, el work_mem y el effective_cache_size se configuran
# en armonia con la memoria del contenedor. Cambiar la memoria sin
# tocar postgresql.conf no mejora nada, o empeora.
#
# Aun asi el VPA es MUY util aqui: nos avisara si nos estamos quedando
# cortos antes de que se manifieste como incidente.
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: postgres
minAllowed:
cpu: "1"
memory: 2Gi
maxAllowed:
cpu: "4"
memory: 8Gi
# RequestsAndLimits mantiene la proporcion 1:1 y por tanto la QoS
# Guaranteed, para el dia que decidamos aplicar la recomendacion a mano.
controlledValues: RequestsAndLimits
- containerName: exportador-postgres
minAllowed:
cpu: 10m
memory: 32Mi
maxAllowed:
cpu: 100m
memory: 128MiEl punto 4 del comentario merece énfasis, porque es el argumento de fondo: el dimensionado de una base de datos es una decisión de configuración, no de observación. Subir la memoria del contenedor sin ajustar shared_buffers en armonía con ella no mejora el rendimiento: solo desperdicia memoria. El VPA no sabe nada de postgresql.conf. Un operador de PostgreSQL (06-07) sí, y ese es el camino correcto para escalar una base de datos.
Tabla resumen de las decisiones
| Componente | Modo | controlledResources |
Justificación |
|---|---|---|---|
api-reservas |
Off |
ambos (asesor) | HPA por CPU; sin reinicios no planificados en el componente crítico |
tienda-web |
Off |
ambos (asesor) | HPA por CPU; ahorro claro pendiente de aplicar a mano |
worker-notificaciones |
Recreate |
cpu, memory | Tolera reinicios; escalará por cola (09-04), sin conflicto |
informes-ocupacion |
Initial |
cpu, memory | Job efímero; no desalojar a mitad; el ahorro de tiempo es enorme |
postgres-reservas |
Off |
ambos (asesor) | Una réplica, QoS Guaranteed, reinicio caro, dimensionado ligado a la configuración |
redis-cache |
Off |
ambos (asesor) | Caché con estado; un reinicio la vacía y descarga toda la carga sobre PostgreSQL |
- Interacción con LimitRange y ResourceQuota
El VPA no vive solo. En el módulo 3 pusimos barandillas de namespace que interactúan con él de formas que conviene anticipar.
LimitRange
Recordemos el LimitRange de rutas-norte-pro:
apiVersion: v1
kind: LimitRange
metadata:
name: limites-por-defecto
namespace: rutas-norte-pro
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
min:
cpu: 50m
memory: 64Mi
max:
cpu: "4"
memory: 8GiInteracciones:
- El
maxdel LimitRange gana sobre elmaxAlloweddel VPA. Si el VPA recomienda 6 núcleos y el LimitRange topa en 4, el pod mutado será rechazado en admisión. El VPA es consciente de los LimitRange y ajusta su recomendación, pero si los cambias después de crear el VPA, puede haber un desfase. - El
minpuede elevar una recomendación baja. Si el VPA recomienda 20m y el LimitRange exige un mínimo de 50m, el pod acabará con 50m. - Los
defaultdesaparecen de la ecuación cuando el VPA muta el pod, porque el pod ya llega con valores explícitos.
Regla práctica: mantén el maxAllowed del VPA por debajo del max del LimitRange. Así el recorte se ve en uncappedTarget (información útil) en lugar de producir un rechazo en admisión (fallo confuso).
ResourceQuota
La ResourceQuota es más traicionera, porque el fallo aparece tarde y en otro sitio.
apiVersion: v1
kind: ResourceQuota
metadata:
name: cuota-pro
namespace: rutas-norte-pro
spec:
hard:
requests.cpu: "40"
requests.memory: 80Gi
limits.cpu: "80"
limits.memory: 160GiEl problema surge de la interacción entre HPA y VPA sobre la cuota:
Situacion: api-reservas, requests.cpu 500m, HPA con maxReplicas 30.
Consumo maximo de cuota: 30 x 500m = 15 nucleos. Cabe en los 40 de la cuota.
El VPA recomienda subir a 800m (crecio la aplicacion).
Se aplica.
Consumo maximo AHORA: 30 x 800m = 24 nucleos.
Sumando tienda-web, worker-notificaciones, postgres y redis: 43 nucleos.
LA CUOTA SON 40.
Resultado: durante el puente de mayo, el HPA intenta crear la replica numero 26
y el ReplicaSet recibe:
Error creating: pods "api-reservas-..." is forbidden:
exceeded quota: cuota-pro, requested: requests.cpu=800m,
used: requests.cpu=39.4, limited: requests.cpu=40
El HPA marca ScalingLimited. La plataforma se queda corta EN EL PICO.El fallo no aparece cuando se aplica el VPA: aparece semanas después, en el peor momento posible, y el mensaje de error está en los eventos del ReplicaSet, no en el HPA ni en el VPA. Es un fallo diferido y mal señalizado.
Comprobación obligatoria al aplicar cualquier recomendación del VPA:
El factor 0,8 deja margen para pods efímeros, trabajos y despliegues (que durante un rolling update consumen cuota de la versión vieja y la nueva a la vez).
Cálculo para Rutas Norte con la recomendación:
| Componente | requests.cpu rec. |
maxReplicas |
CPU máxima |
|---|---|---|---|
api-reservas (api) |
412m | 30 | 12,36 |
api-reservas (exportador) |
6m | 30 | 0,18 |
tienda-web |
70m | 15 | 1,05 |
worker-notificaciones |
120m | 20 | 2,40 |
postgres-reservas |
2000m | 1 | 2,00 |
redis-cache |
300m | 1 | 0,30 |
| Total | 18,29 |
18,29 contra una cuota de 40, con margen del 80 % (32). Cabe con holgura. Y fíjate en que la recomendación del VPA reduce el consumo máximo respecto al escenario actual (que sería 15 + 3 + ... ≈ 21), porque tienda-web y el exportador estaban sobredimensionados. Ajustar bien los requests no solo ahorra dinero: da margen para escalar más.
Un aviso sobre controlledValues: RequestsAndLimits
Como vimos, el VPA mantiene la proporción requests:limits. Si tu proporción es 1:2 y el VPA sube el requests un 50 %, el limit sube también un 50 %. Y la ResourceQuota tiene una línea limits.cpu separada que también puede agotarse. Vigila las dos.
- Los límites del VPA
Terminamos con lo que el VPA no puede hacer. Conocer las limitaciones evita esperar de él lo que no da.
Necesita historia
Un VPA recién creado no sabe nada. Sus primeras recomendaciones, en las primeras horas, son poco fiables: el upperBound será enorme y el target reflejará solo lo que ha visto. Regla: no tomes decisiones con menos de una semana de datos, y prefiere dos.
Corolario incómodo: para un servicio nuevo, el VPA no ayuda. Hay que estimar a mano, desplegar, y dejar que el VPA corrija después.
Reacciona lento
Con semivida de 24 horas, un cambio real de perfil tarda días en reflejarse plenamente. Esto es deliberado y correcto —no queremos que un pico de cinco minutos cambie los recursos de toda la plataforma—, pero significa que el VPA no sirve para responder a eventos:
- No sirve para el puente de mayo. Cuando el VPA se dé cuenta de que hace falta más CPU, el puente habrá terminado.
- No sirve para un despliegue que cambia radicalmente el perfil de consumo. Ahí hay que estimar a mano y dejar que el VPA valide después.
El VPA es para el régimen permanente, no para el transitorio. Los transitorios los cubre el HPA (09-01) y KEDA (09-04).
Recrear un pod con estado no es gratis
En modos Recreate y Auto, cada ajuste es un reinicio. El coste depende del componente:
| Componente | Coste del reinicio |
|---|---|
tienda-web (nginx) |
Casi nulo: un segundo, sin estado |
api-reservas |
Moderado: 25 s de arranque, pool de conexiones nuevo, caché fría |
worker-notificaciones |
Bajo: la cola reentrega el mensaje en vuelo |
redis-cache |
Alto: se pierde toda la caché, y toda esa carga cae sobre PostgreSQL |
postgres-reservas |
Muy alto: corte de todas las conexiones, caché de páginas perdida |
El VPA respeta los PDB (09-05), lo cual mitiga el riesgo, pero no lo elimina: un PDB garantiza que no se caiga todo a la vez, no que el reinicio sea indoloro.
No conoce el contexto de negocio
El VPA ve consumo de CPU y memoria. No ve:
- Que el puente de mayo empieza el viernes.
- Que ese pod atiende el proceso de pago y no puede reiniciarse durante una transacción.
- Que la memoria alta de anoche fue una ejecución de mantenimiento y no el régimen normal.
- Que
shared_buffersde PostgreSQL debe cambiar en armonía con la memoria del contenedor.
Todo eso lo pone una persona. Y esa es, resumida en una frase, la justificación del modo Off.
Otras limitaciones a tener presentes
- No gestiona recursos que no sean CPU y memoria. El almacenamiento efímero, las GPU y los recursos extendidos quedan fuera.
- Con
updateModeactivo, no debería usarse junto a un despliegue continuo agresivo: los desalojos del VPA y los rolling updates se solapan de forma poco predecible. - Un solo VPA por carga de trabajo. Dos objetos VPA con
targetRefal mismo Deployment producen comportamiento indefinido. Kubernetes no lo impide; tú sí debes.
Errores Comunes y Consejos
Error 1: poner el VPA en modo Auto sobre CPU cuando ya hay un HPA por CPU. El error catastrófico de esta lección. El bucle de realimentación del apartado 11 tarda horas en manifestarse y es difícil de diagnosticar porque cada componente parece funcionar bien por separado. Si tienes HPA por CPU: controlledResources: ["memory"] o modo Off. Sin excepciones.
Error 2: aplicar la recomendación de un VPA de dos días. El upperBound desmesurado delata que hay poca historia. Espera dos semanas. Si tienes prisa, mira el lowerBound: si tu valor actual está por debajo, eso sí es una señal fiable incluso con pocos datos, porque significa que el contenedor ya se ha quedado corto en observaciones reales.
Error 3: no poner maxAllowed. Sin techo, una fuga de memoria hace que la recomendación crezca sin fin. En modo activo, acabarás con un pod pidiendo 30Gi que no cabe en ningún nodo y se queda Pending eternamente. El maxAllowed convierte esa fuga en una señal visible (uncappedTarget alejándose del target) en lugar de en una caída.
Error 4: creer que el VPA modifica el Deployment. No lo hace. El kubectl get deployment seguirá mostrando los valores del manifiesto. La mutación ocurre en el pod, en admisión. Para ver la realidad hay que mirar los pods, no el controlador. Esta confusión genera muchísimos tickets de «el VPA no funciona».
Error 5: VPA activo sobre una carga de una sola réplica. Un desalojo es una caída total. El minReplicas: 2 por defecto protege, pero si lo bajas a 1 «para que funcione», estás pidiendo cortes de servicio. Si tienes una sola réplica, o pones dos, o usas modo Off.
Error 6: olvidar la ResourceQuota. El apartado 13. Subir los requests sin recalcular requests × maxReplicas contra la cuota produce un fallo diferido que se manifiesta en el pico de tráfico, semanas después, con un mensaje de error en un sitio inesperado.
Consejo 1: empieza por los CronJobs. Son el caso más rentable y el de menor riesgo. Nadie espera una respuesta, el modo Initial es seguro, y el ahorro de tiempo de ejecución suele ser espectacular. informes-ocupacion pasando de 40 a 9 minutos es una victoria visible que compra credibilidad para el resto.
Consejo 2: exporta las recomendaciones a Prometheus. El VPA expone métricas, y kube-state-metrics publica kube_verticalpodautoscaler_status_recommendation_containerrecommendations_target. Con eso puedes construir en Grafana (07-04) un panel de «recomendado contra configurado» para toda la plataforma, y una alerta cuando la desviación supere el 30 %:
abs(
kube_verticalpodautoscaler_status_recommendation_containerrecommendations_target{resource="memory"}
-
kube_pod_container_resource_requests{resource="memory"}
)
/ kube_pod_container_resource_requests{resource="memory"} > 0.3Consejo 3: un VPA en modo Off sobre absolutamente todo. Coste casi nulo, información permanente. Inclúyelo en la plantilla base de todo componente nuevo.
Consejo 4: usa el lowerBound como alerta de riesgo. Un contenedor cuyos requests están por debajo del lowerBound está en peligro real de desalojo o de OOMKilled. Es la señal más accionable que da el VPA, y merece una alerta.
Consejo 5: revisa el uncappedTarget mensualmente. Si crece mes a mes mientras el target se queda pegado al maxAllowed, tienes una fuga de memoria. El VPA la detecta antes de que se convierta en incidente.
Consejo 6: documenta en el manifiesto de dónde salen los números. Un comentario como # requests.memory: 800Mi -- recomendacion VPA 2026-05-11, p95 real 690Mi convierte un número mágico en una decisión trazable. El siguiente que lo lea sabrá si puede tocarlo.
Ejercicios
Ejercicio 1: interpretar una recomendación
Este es el describe del VPA de redis-cache tras tres semanas en modo Off:
Status:
Recommendation:
Container Recommendations:
Container Name: redis
Lower Bound:
Cpu: 180m
Memory: 1420Mi
Target:
Cpu: 245m
Memory: 1980Mi
Uncapped Target:
Cpu: 245m
Memory: 3410Mi
Upper Bound:
Cpu: 390m
Memory: 2048MiEl manifiesto actual dice:
Y el resourcePolicy del VPA:
resourcePolicy:
containerPolicies:
- containerName: redis
minAllowed:
cpu: 100m
memory: 512Mi
maxAllowed:
cpu: "1"
memory: 2GiResponde: (a) qué pasa con la CPU; (b) qué pasa con la memoria y por qué target y uncappedTarget difieren; (c) qué significa que upperBound sea exactamente 2048Mi; (d) qué QoS tiene el pod ahora y qué implica; (e) qué harías, con qué prioridad, y qué comprobarías antes.
Ejercicio 2: diagnosticar un conflicto
tienda-web lleva tres días con un comportamiento extraño. El equipo reporta:
- El número de réplicas oscila entre 3 y 12 varias veces al día sin que el tráfico lo justifique.
- Los pods se reinician con frecuencia, con motivo
Evicted. - Los
requests.cpude los pods no coinciden con el manifiesto y cambian solos.
La configuración es:
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: tienda-web
namespace: rutas-norte-pro
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: tienda-web
minReplicas: 3
maxReplicas: 15
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50
---
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: tienda-web
namespace: rutas-norte-pro
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: tienda-web
updatePolicy:
updateMode: "Auto"Explica exactamente qué está pasando, con la secuencia temporal, y da dos soluciones alternativas con sus manifiestos y sus contrapartidas.
Ejercicio 3: diseñar la estrategia para un componente nuevo
Rutas Norte añade exportador-contable, un componente que genera los ficheros de facturación para la gestoría. Perfil:
- Es un CronJob que se ejecuta el día 1 de cada mes a las 2:00.
- Cada ejecución tarda entre 20 y 90 minutos, según el volumen de reservas del mes.
- Consume mucha memoria: carga en memoria todas las reservas del mes para agregarlas.
- Si falla, hay que relanzarlo a mano y el equipo de administración se queda sin los ficheros ese día.
- Ha sido
OOMKilleddos de los últimos cuatro meses. - Actualmente:
requests.memory: 1Gi,limits.memory: 2Gi,requests.cpu: 500m,limits.cpu: "1". - El namespace tiene una ResourceQuota de 40 núcleos y 80Gi.
Diseña la estrategia completa: ¿VPA sí o no? ¿Qué modo? ¿Qué resourcePolicy? ¿Cuánto tiempo de observación necesitas y por qué es un caso especial? ¿Qué harías además del VPA? Escribe el manifiesto y justifica cada decisión.
Soluciones
Solución 1
(a) La CPU está sobredimensionada.
El manifiesto pide 500m; el upperBound es 390m. El valor actual está por encima del upperBound, lo que significa que el recomendador está seguro de que sobra CPU. El target es 245m: menos de la mitad de lo configurado. Como redis-cache es una sola réplica, el ahorro absoluto es modesto (255m), pero libera capacidad de planificación en un nodo.
(b) La memoria: target 1980Mi, uncappedTarget 3410Mi.
Esta es la observación crítica del ejercicio. El maxAllowed.memory es 2Gi = 2048Mi, y el uncappedTarget de 3410Mi está muy por encima. El recomendador quiere 3410Mi y las barandillas lo están recortando a 1980Mi.
Además, el requests actual (1Gi = 1024Mi) está por debajo del lowerBound (1420Mi). Doble señal de alarma: falta memoria reservada Y el recomendador quiere mucha más de la que le dejamos.
¿Fuga de memoria o crecimiento legítimo? En Redis hay una tercera posibilidad muy específica, y es la más probable: la caché ha crecido hasta llenar el espacio disponible. Redis, sin una política de expulsión configurada, sigue aceptando claves hasta agotar la memoria. El uncappedTarget creciente no indica un fallo del software: indica que maxmemory no está configurado en Redis y la caché crece sin control.
Comprobación:
kubectl exec -n rutas-norte-pro redis-cache-0 -- redis-cli CONFIG GET maxmemory
kubectl exec -n rutas-norte-pro redis-cache-0 -- redis-cli CONFIG GET maxmemory-policy
kubectl exec -n rutas-norte-pro redis-cache-0 -- redis-cli INFO memorySi maxmemory es 0, ese es el problema de fondo, y ningún ajuste de requests lo resuelve: solo retrasa el siguiente OOMKilled.
(c) upperBound exactamente 2048Mi.
No es casualidad: es el maxAllowed. Los límites de la recomendación también se recortan por las barandillas. Cuando ves un upperBound que coincide exactamente con tu maxAllowed, el recomendador te está diciendo que su intervalo de confianza está truncado, y que la recomendación real está fuera de lo que le permites.
(d) QoS del pod.
Como no todos los recursos tienen requests igual a limits, la QoS es Burstable, no Guaranteed (módulo 3).
Implicación seria: bajo presión de memoria en el nodo, un pod Burstable que consume por encima de su requests es candidato prioritario al desalojo. Y redis-cache está consumiendo 1980Mi con un requests de 1024Mi: casi el doble de lo reservado. Es de los primeros que caerán cuando el nodo se llene, y su caída descarga toda la carga sobre postgres-reservas justo cuando el nodo ya está bajo presión. Un efecto dominó de manual.
(e) Plan de acción, por prioridad.
Prioridad 1 (hoy): configurar maxmemory en Redis. Es la corrección de raíz.
# ConfigMap de redis-cache
maxmemory 1500mb
maxmemory-policy allkeys-lru # Expulsa las claves menos usadas al llegar al topeCon esto, Redis deja de crecer sin control y empieza a comportarse como una caché de verdad. El uncappedTarget debería estabilizarse en las siguientes semanas.
Prioridad 2 (esta semana): subir el requests.memory a 2Gi e igualarlo al limit.
resources:
requests:
cpu: 300m # Bajado de 500m: el target es 245m, con margen
memory: 2Gi # Subido de 1Gi: estaba por debajo del lowerBound
limits:
cpu: 300m # Igual que requests
memory: 2Gi # Igual que requests -> QoS GuaranteedEsto consigue tres cosas: cubre el lowerBound, promueve el pod a QoS Guaranteed (deja de ser candidato prioritario al desalojo, coherente con su papel de amortiguador de PostgreSQL), y libera 200m de CPU.
Prioridad 3 (en un mes): revisar de nuevo. Con maxmemory puesto, comprobar que el uncappedTarget se ha estabilizado por debajo de 2Gi. Si sigue creciendo, hay algo más.
Comprobaciones antes de aplicar:
# 1. ¿Cabe en la ResourceQuota? (redis-cache es 1 replica, impacto pequeno)
kubectl describe resourcequota cuota-pro -n rutas-norte-pro
# 2. ¿Hay un nodo con 2Gi libres de verdad?
kubectl describe nodes | grep -A5 "Allocated resources"
# 3. ¿Cuanto tarda en recuperarse la cache tras el reinicio?
# (el cambio de requests obliga a recrear el pod)
# Programarlo en horario de bajo trafico, NUNCA cerca del puente de mayo.Ese último punto es el que se olvida siempre: cambiar los recursos de redis-cache implica reiniciarlo, y reiniciarlo vacía la caché. Durante los minutos siguientes, toda la carga de consultas de disponibilidad va directa a postgres-reservas. Hay que hacerlo un martes a las 4 de la mañana, no un viernes a las 10.
Solución 2
Qué está pasando: el bucle de realimentación HPA-VPA del apartado 11, en estado puro.
El HPA escala tienda-web por CPU. El VPA está en modo Auto sin controlledResources, lo que significa que controla CPU y memoria. Ambos gobiernan la CPU.
Secuencia temporal:
t=0 3 replicas, requests.cpu 200m, objetivo HPA = 50 % de 200m = 100m.
Carga total: 480m -> 160m por pod. Ratio 1,6.
HPA -> techo(3 x 1,6) = 5 replicas.
t=2min 5 replicas, 96m por pod. HPA satisfecho (ratio 0,96, en tolerancia).
t=20min El VPA lleva un rato observando ~96m de consumo. Recomienda
requests.cpu = 110m (96 x 1,15). El actualizador DESALOJA los pods
de uno en uno. <-- REINICIOS "Evicted" que reporta el equipo
t=25min Pods recreados con requests.cpu 110m.
Objetivo del HPA AHORA = 50 % de 110m = 55m.
Consumo real sigue en 96m. Ratio = 96/55 = 1,75.
HPA -> techo(5 x 1,75) = 9 replicas. <-- OSCILACION que reporta el equipo
t=27min 9 replicas, 53m por pod. HPA satisfecho.
El VPA observa 53m, recomienda 61m...
t=45min El VPA aplica 61m. Objetivo HPA = 30m. Consumo 53m. Ratio 1,77.
HPA -> 16 replicas, cortado por maxReplicas a 15.
t=50min 15 replicas, 32m por pod. El VPA recomienda 37m...
Y el ciclo continua, ahora limitado por el techo.
Cuando la carga baja de noche, el proceso se invierte: el VPA sube
el requests (porque los pods estan al limite de una recomendacion
minuscula), el objetivo del HPA sube, el HPA baja replicas, y vuelta
a empezar. De ahi la oscilacion 3 <-> 12 "varias veces al dia".Los tres síntomas quedan explicados: la oscilación de réplicas (HPA reaccionando a objetivos móviles), los Evicted (el actualizador del VPA desalojando), y los requests que no coinciden con el manifiesto y cambian solos (el controlador de admisión mutando los pods).
Comandos que lo confirmarían:
# Los eventos del HPA muestran reescalados frecuentes sin causa de trafico
kubectl describe hpa tienda-web -n rutas-norte-pro
# Los eventos del namespace muestran EvictedByVPA
kubectl get events -n rutas-norte-pro --field-selector reason=EvictedByVPA \
--sort-by=.lastTimestamp
# Los requests reales de los pods difieren del manifiesto
kubectl get pods -n rutas-norte-pro -l app=tienda-web \
-o custom-columns=NOMBRE:.metadata.name,CPU:.spec.containers[0].resources.requests.cpu
# La recomendacion del VPA ha ido bajando
kubectl describe vpa tienda-web -n rutas-norte-proSolución A (recomendada): VPA en modo Off.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: tienda-web
namespace: rutas-norte-pro
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: tienda-web
updatePolicy:
# OFF. El HPA gobierna la CPU. El VPA queda como asesor: revisamos su
# recomendacion cada lunes y la aplicamos a mano en una pull request.
updateMode: "Off"
resourcePolicy:
containerPolicies:
- containerName: nginx
minAllowed:
cpu: 50m
memory: 64Mi
maxAllowed:
cpu: 500m
memory: 512MiVentajas: elimina el conflicto por completo, sin reinicios sorpresa, el manifiesto sigue siendo la verdad, compatible con GitOps. Contrapartida: alguien tiene que aplicar la recomendación a mano.
Solución B: VPA activo solo sobre memoria.
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: tienda-web
namespace: rutas-norte-pro
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: tienda-web
updatePolicy:
updateMode: "Recreate"
minReplicas: 3
resourcePolicy:
containerPolicies:
- containerName: nginx
# LA LINEA CLAVE: solo memoria. La CPU es territorio del HPA.
controlledResources: ["memory"]
minAllowed:
memory: 64Mi
maxAllowed:
memory: 512MiVentajas: la memoria se ajusta sola, sin intervención.
Contrapartidas: sigue habiendo reinicios no planificados; el requests.cpu queda sin ajustar (habría que hacerlo a mano igual); durante el puente de mayo habría que pasar a Off para evitar reinicios en el peor momento.
Cuál elegir. Para tienda-web, la A. El ahorro de memoria de nginx es pequeño (hablamos de decenas de Mi por pod) y no compensa el riesgo de reinicios no planificados en la puerta de entrada de la plataforma. La solución B tendría sentido en una carga con consumo de memoria variable y grande, y que tolerase reinicios: worker-notificaciones es mejor candidato.
Acción inmediata mientras se decide:
kubectl patch vpa tienda-web -n rutas-norte-pro --type merge \
-p '{"spec":{"updatePolicy":{"updateMode":"Off"}}}'Esto detiene la oscilación en el siguiente ciclo del actualizador, sin borrar nada.
Solución 3
¿VPA sí o no? Sí, rotundamente. Es un caso de libro: un trabajo por lotes con consumo variable que ya ha sido OOMKilled dos veces. El coste de un reinicio es cero (el Job es efímero) y el coste del fallo actual es alto (intervención manual, gestoría sin ficheros).
¿Qué modo? Initial. Igual que informes-ocupacion:
Recreateno tiene sentido: desalojar un Job a los 40 minutos de una ejecución de 90 pierde todo el trabajo y empieza de cero. Sería peor que elOOMKilled.Offdesaprovecha la ocasión: aquí sí queremos aplicación automática, porque el consumo varía cada mes con el volumen de reservas y nadie va a revisar la recomendación mensualmente.Initialaplica la recomendación en cada ejecución nueva. Perfecto.
El caso especial de la observación: la frecuencia mensual.
Este es el punto interesante del ejercicio. El histograma del recomendador tiene semivida de 24 horas. Una ejecución mensual significa que, cuando llega la siguiente, la muestra anterior tiene treinta semividas de antigüedad: su peso es 2⁻³⁰, es decir, cero.
El VPA, tal cual, es prácticamente inútil para un trabajo mensual. Cada ejecución empieza casi desde cero.
Tres formas de resolverlo, de menos a más recomendable:
-
Ajustar la semivida del recomendador. El
vpa-recommenderacepta--cpu-histogram-decay-half-lifey--memory-histogram-decay-half-life. Subirlos a720h(30 días) haría que la muestra del mes anterior pesara la mitad. Contrapartida: es un parámetro global del recomendador, afecta a todos los VPA del clúster y volvería lentísimas las recomendaciones deapi-reservas. Solo viable con una segunda instancia del recomendador dedicada, lo cual es complejidad seria. -
Ejecutar el trabajo con más frecuencia en pre. Programarlo semanalmente en
rutas-norte-precon datos representativos, dejar que el VPA aprenda ahí, y llevar la recomendación a producción a mano. Funciona, pero requiere datos de prueba realistas. -
VPA en
Initial+ unminAllowedgeneroso + medición explícita. La opción pragmática y la que recomiendo.
Además del VPA, tres medidas que importan más:
# k8s/entornos/pro/cronjob-exportador-contable.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: exportador-contable
namespace: rutas-norte-pro
labels:
app: exportador-contable
app.kubernetes.io/part-of: rutas-norte
spec:
schedule: "0 2 1 * *" # Dia 1 de cada mes a las 2:00
concurrencyPolicy: Forbid
# MEDIDA 1: reintentos automaticos. Si falla, que lo intente solo antes de
# despertar a nadie. Con 3 intentos y backoff, un OOM puntual se recupera.
jobTemplate:
spec:
backoffLimit: 3
# MEDIDA 2: plazo maximo. Si a las 4 horas no ha terminado, algo va mal
# y queremos saberlo antes de que se solape con la carga de la manana.
activeDeadlineSeconds: 14400
template:
spec:
restartPolicy: OnFailure
containers:
- name: exportador
image: registry.rutasnorte.example/exportador-contable:3.1.0
resources:
requests:
cpu: 500m
# MEDIDA 3, LA IMPORTANTE: subir la memoria YA, sin esperar
# al VPA. Dos OOMKilled en cuatro meses con limit de 2Gi
# significan que 2Gi no basta. Subimos a 4Gi de entrada.
memory: 4Gi
limits:
cpu: "2"
# requests == limits en memoria -> QoS Guaranteed para el
# contenedor. Se ejecuta de madrugada, el cluster esta vacio,
# no le quitamos nada a nadie y garantizamos que no lo
# desalojen por presion de memoria del nodo.
memory: 4GiY el VPA:
# k8s/entornos/pro/vpa-exportador-contable.yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: exportador-contable
namespace: rutas-norte-pro
labels:
app: exportador-contable
app.kubernetes.io/part-of: rutas-norte
spec:
targetRef:
apiVersion: batch/v1
kind: CronJob
name: exportador-contable
updatePolicy:
# INITIAL: cada ejecucion nueva nace con los recursos aprendidos. Nunca
# desalojamos un Job a mitad de camino: perderiamos 40 minutos de trabajo.
updateMode: "Initial"
resourcePolicy:
containerPolicies:
- containerName: exportador
minAllowed:
cpu: 500m
# minAllowed GENEROSO y deliberado. Con ejecucion mensual, el
# histograma del recomendador (semivida 24 h) esta practicamente
# vacio cuando llega la siguiente ejecucion. Sin este suelo, el VPA
# recomendaria valores minusculos basados en casi ningun dato y
# tendriamos un OOMKilled garantizado. El suelo es nuestro seguro.
memory: 4Gi
maxAllowed:
cpu: "4"
# Techo alto: el volumen de reservas crece y diciembre (con el
# cierre anual) puede ser mucho mayor que un mes normal. 8Gi es lo
# que cabe comodamente en un nodo de 16Gi con el cluster vacio de
# madrugada. Si el uncappedTarget llega aqui, hay que revisar el
# algoritmo del exportador: probablemente carga en memoria lo que
# deberia procesar por lotes.
memory: 8Gi
controlledResources: ["cpu", "memory"]
# RequestsAndLimits mantiene la proporcion 1:1 de memoria y por tanto
# la QoS del contenedor.
controlledValues: RequestsAndLimitsComprobación de la ResourceQuota:
Cuota del namespace: 40 nucleos, 80Gi.
Peor caso del exportador: 4 nucleos y 8Gi (si el VPA llega al maxAllowed).
Como se ejecuta a las 2:00 del dia 1, el resto de la plataforma esta en
minReplicas: api-reservas 4, tienda-web 3, worker 2.
Consumo nocturno estimado: ~8 nucleos, ~14Gi.
Con el exportador al maximo: 12 nucleos, 22Gi. Muy por debajo de la cuota.
CONCLUSION: cabe con holgura. Y es una razon mas para ejecutarlo de noche.Plan de seguimiento:
| Cuándo | Qué hacer |
|---|---|
| Inmediato | Aplicar los 4Gi y los reintentos. No esperar al VPA |
| Tras la 1.ª ejecución | kubectl describe vpa para ver el primer target, y medir el pico real con kubectl top pods durante la ejecución |
| Tras la 3.ª ejecución | Comparar los tres target. Si son consistentes, ajustar el minAllowed a un valor más fino |
| Tras 6 meses | Revisar la tendencia. Si el uncappedTarget crece linealmente con el volumen de reservas, el exportador escala mal y hay que procesar por lotes en lugar de cargar todo en memoria |
La lección de fondo del ejercicio: cuando la frecuencia de ejecución es mucho menor que la semivida del histograma, el VPA pierde casi toda su utilidad automática. En esos casos, la barandilla (minAllowed) importa más que la recomendación, y la medición manual de las primeras ejecuciones sigue siendo insustituible. El VPA no exime de pensar: acelera el pensar bien.
Conclusión
El VerticalPodAutoscaler no responde a la carga: responde a la ignorancia. Convierte los requests y limits de números que alguien copió de otro manifiesto en valores derivados del consumo real observado durante días.
Lo esencial:
- El VPA responde a «¿de qué tamaño?»; el HPA a «¿cuántos?». Ejes distintos, constantes de tiempo distintas: días frente a segundos.
- No está en el núcleo. Hay que instalarlo desde
kubernetes/autoscaler, comprobar compatibilidad de versiones, y en la nube mirar primero si el proveedor lo ofrece. - Tres componentes: el recomendador calcula (percentiles con decaimiento exponencial, p90 para CPU, pico para memoria), el actualizador desaloja respetando los PDB, y el controlador de admisión muta los pods al nacer. El Deployment nunca cambia.
Offes el modo de los equipos serios. El manifiesto sigue siendo la verdad, no hay reinicios sorpresa, funciona con GitOps y cada cambio de recursos pasa por una revisión humana.Initialpara trabajos por lotes;Recreatesolo para cargas que toleran reinicios.target,lowerBound,upperBound,uncappedTarget. Eltargetes lo que llevas al manifiesto; estar por debajo dellowerBoundes una alarma; ununcappedTargetque se aleja deltargetmes a mes es una fuga de memoria hasta que se demuestre lo contrario.- VPA y HPA sobre la misma métrica se destruyen mutuamente. El bucle es real y tarda horas en manifestarse.
controlledResources: ["memory"]cuando el HPA escala por CPU, o modoOff. La combinación óptima es HPA por métrica de negocio (09-04) y VPA completo. - El redimensionado en caliente llegará y cambiará las reglas, pero hoy no es base para una estrategia de producción.
- Cuidado con la ResourceQuota:
requestsrecomendado ×maxReplicasdel HPA debe caber, o el fallo aparecerá en el pico de tráfico semanas después.
En Rutas Norte hemos dejado el VPA en modo Off sobre api-reservas, tienda-web, postgres-reservas y redis-cache, activo en Recreate sobre worker-notificaciones y en Initial sobre informes-ocupacion. Y hemos descubierto, con datos, que api-reservas tenía la memoria por debajo de su lowerBound y que tienda-web reservaba el triple de CPU de la que usa.
Con esto, cada pod pide lo que necesita y el número de pods se adapta al tráfico. Pero queda un supuesto oculto en todo lo que hemos hecho: hemos dado por sentado que, cuando el HPA pide treinta réplicas de api-reservas, hay sitio donde ponerlas. El clúster de Rutas Norte tiene cuatro nodos. Treinta pods de api-reservas más quince de tienda-web más los veinte de worker-notificaciones no caben. El planificador los dejará en Pending y el HPA no podrá hacer nada al respecto: habrá pedido las réplicas, el Deployment las habrá creado, y ahí se quedarán, en cola, mientras la venta del puente de mayo se cae exactamente igual que el año pasado.
En la próxima lección, Autoescalado de Clúster, subimos el último escalón: cómo hacer que el clúster añada nodos cuando los pods no caben, por qué retirarlos es mucho más delicado que añadirlos, cuánto tarda de verdad un nodo en estar listo (y por qué eso no salva una punta repentina), y el truco del sobreaprovisionamiento con pods de relleno que ya tenemos casi todas las piezas para entender.
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
