Un Pod puede estar en estado Running y aun así tener problemas serios de rendimiento.
La aplicación puede responder lentamente, consumir memoria constantemente, reiniciarse con OOMKilled o experimentar CPU throttling incluso cuando el nodo parece tener recursos disponibles.
En este segundo artículo de nuestra serie de troubleshooting en Kubernetes vamos a investigar:
alto consumo de CPU;
alto consumo de memoria;
OOMKilled;CPU throttling;
Pod evictions;
presión de recursos a nivel de nodo.
El objetivo no será simplemente encontrar "qué Pod consume más".
Queremos responder:
¿El problema está en la aplicación, en sus requests/limits, o en la capacidad del nodo?
Requests, limits y consumo real
Antes de investigar CPU o memoria necesitamos distinguir tres conceptos:
Resources
|
+------------+------------+
| | |
Request Limit Usage
| | |
v v v
Scheduling Enforcement Actual
Requests
Los requests indican a Kubernetes la cantidad de recursos que un workload solicita.
Por ejemplo:
resources:
requests:
cpu: "250m"
memory: "256Mi"
El scheduler utiliza estos valores para decidir en qué nodo puede ejecutar el Pod.
Un Pod puede utilizar más recursos que su request si existen recursos disponibles y los límites lo permiten.
Limits
Los limits establecen restricciones sobre el consumo.
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
CPU y memoria no se comportan de la misma forma.
CPU limit
|
v
CPU throttling
Memory limit
|
v
Memory pressure / allocation
|
v
Possible OOM kill
Un contenedor no es eliminado simplemente por consumir demasiado CPU. El kernel restringe el tiempo de CPU disponible mediante los mecanismos de control del cgroup.
La memoria funciona diferente: cuando el límite de memoria es alcanzado y una nueva asignación no puede satisfacerse, el kernel puede terminar procesos utilizando el mecanismo OOM.
1. Alto consumo de CPU
Supongamos que recibimos una alerta indicando que la latencia aumentó y encontramos un Pod utilizando una cantidad elevada de CPU.
El primer paso puede ser:
kubectl top pods -A
Para revisar los contenedores individualmente:
kubectl top pod <pod> \
-n <namespace> \
--containers
Y para ordenar Pods por CPU:
kubectl top pods \
-n <namespace> \
--sort-by=cpu
Esto nos da una vista reciente del consumo.
Sin embargo:
kubectl topno sustituye a un sistema de monitoring.
Sus métricas provienen normalmente de Metrics Server y están diseñadas principalmente para proporcionar señales eficientes a componentes como los autoscalers.
Para investigar cuándo comenzó un problema o cómo evolucionó durante horas o días necesitamos métricas históricas.
¿Qué debemos preguntar cuando aumenta el CPU?
No debemos asumir inmediatamente que la solución es agregar más CPU.
Primero:
High CPU
|
+--- ¿Aumentó el tráfico?
|
+--- ¿Cambió la aplicación?
|
+--- ¿Un endpoint específico está consumiendo CPU?
|
+--- ¿Una dependencia está respondiendo lentamente?
|
+--- ¿Existe un loop o proceso runaway?
|
+--- ¿Hay más trabajo en background?
|
+--- ¿El container está siendo throttled?
Revisa también los cambios recientes:
kubectl rollout history \
deployment/<deployment> \
-n <namespace>
Y los recursos configurados:
kubectl get deployment <deployment> \
-n <namespace> \
-o yaml
Busca:
resources:
requests:
cpu:
limits:
cpu:
CPU usage vs CPU limit
Imaginemos:
CPU request: 250m
CPU limit: 500m
CPU usage: 490m
Ese contenedor está trabajando muy cerca de su límite.
Pero ahora imaginemos que el nodo tiene:
Node CPU usage: 35%
Puede parecer contradictorio.
No necesariamente lo es.
El contenedor tiene su propio límite de CPU.
Node
16 CPUs available
|
| Plenty of CPU available
|
+------------------------------+
|
Container |
CPU limit = 500m |
| |
v |
reaches limit |
| |
v |
CPU throttling <---------------------+
Por lo tanto, un nodo puede tener capacidad libre y un contenedor seguir experimentando throttling debido a su propio límite.
2. CPU throttling
Este escenario merece investigarse por separado.
Un workload puede presentar:
Pods: Running
CPU node: Normal
Memory: Normal
Errors: Low
Latency: HIGH
y aun así estar limitado por CPU.
Los CPU limits se aplican mediante mecanismos del kernel y cgroups. Cuando un contenedor agota el tiempo de CPU que puede utilizar según su cuota, debe esperar antes de continuar ejecutándose.
Eso puede aparecer como aumento de latencia.
Métricas de Prometheus
Dependiendo de cómo recolectemos las métricas del runtime/cAdvisor, podemos encontrar métricas como:
container_cpu_usage_seconds_total
container_cpu_cfs_throttled_periods_total
container_cpu_cfs_periods_total
container_cpu_cfs_throttled_seconds_total
Los nombres y labels disponibles pueden variar según el stack de observabilidad y runtime, por lo que siempre debemos verificar qué métricas realmente existen en nuestro entorno.
Una consulta útil, cuando estas métricas están disponibles, puede ser:
rate(
container_cpu_usage_seconds_total{
namespace="production",
pod=~"api-.*",
container!="POD",
container!=""
}[5m]
)
Para investigar la proporción de períodos throttled:
sum by (namespace, pod, container) (
rate(container_cpu_cfs_throttled_periods_total[5m])
)
/
sum by (namespace, pod, container) (
rate(container_cpu_cfs_periods_total[5m])
)
No deberíamos interpretar una métrica de throttling de forma aislada.
Correlaciona:
CPU throttling
+
CPU usage
+
request rate
+
latency
+
application behavior
|
v
Better diagnosis
¿Cómo resolver CPU throttling?
No existe una única respuesta.
Dependiendo de la causa podemos:
corregir un workload CPU-intensive;
optimizar código;
incrementar CPU requests;
modificar CPU limits;
agregar réplicas;
ajustar HPA;
separar procesos background;
investigar noisy neighbors;
revisar capacidad del nodo.
Simplemente eliminar todos los límites tampoco debería ser una respuesta automática.
Los límites pueden ayudar a controlar workloads que de otro modo podrían monopolizar recursos compartidos.
La configuración correcta depende del comportamiento del workload y de los objetivos de performance y aislamiento.
3. Alto consumo de memoria
Comenzamos nuevamente con:
kubectl top pods -A
Y:
kubectl top pod <pod> \
-n <namespace> \
--containers
Pero una lectura puntual no nos dice si estamos frente a:
Normal high usage
Memory
^
| ____________
| /
|______/
time
o:
Possible leak
Memory
^
| /
| /
| /
| /
|_____/
time
Por eso para memoria necesitamos especialmente datos históricos.
Preguntas para investigar memoria
High Memory
|
+--- ¿El uso está creciendo continuamente?
|
+--- ¿Se estabiliza?
|
+--- ¿Coincide con aumento de tráfico?
|
+--- ¿Cambió recientemente la aplicación?
|
+--- ¿Existe caching?
|
+--- ¿Existe un memory leak?
|
+--- ¿Está creciendo una cola?
|
+--- ¿El runtime tiene heap configurado?
|
+--- ¿Estamos cerca del memory limit?
Revisa requests y limits:
kubectl get pod <pod> \
-n <namespace> \
-o yaml
Por ejemplo:
resources:
requests:
memory: "512Mi"
limits:
memory: "1Gi"
Después compara:
Memory usage
vs
Memory request
vs
Memory limit
4. OOMKilled
Uno de los síntomas más claros que podemos encontrar es:
Reason: OOMKilled
Primero:
kubectl describe pod <pod> \
-n <namespace>
También podemos consultar el estado anterior:
kubectl get pod <pod> \
-n <namespace> \
-o jsonpath='{.status.containerStatuses[*].lastState.terminated}'
Y los logs anteriores:
kubectl logs <pod> \
-n <namespace> \
--previous
OOMKilled vs Exit Code 137
Es importante no confundirlos.
Podemos encontrar:
Exit Code: 137
137 normalmente representa:
128 + 9 = 137
signal 9 = SIGKILL
Pero:
Exit Code 137por sí solo no demuestra que Kubernetes haya matado el proceso por OOM.
Un proceso puede recibir SIGKILL por otras razones.
Por eso debemos revisar también:
Reason: OOMKilled
junto con:
container state;
Events;
métricas de memoria;
logs;
condiciones del nodo.
Container OOM vs Node memory pressure
Otra distinción importante:
Memory problem
|
+-------+-------+
| |
v v
Container OOM Node Pressure
| |
Memory limit Node running
/ cgroup OOM low on memory
| |
v v
OOMKilled Possible eviction
No son el mismo problema.
Container OOM
Por ejemplo:
limits:
memory: "512Mi"
La aplicación intenta utilizar más memoria de la que puede disponer dentro de ese cgroup y el kernel puede matar un proceso.
Node pressure
También podemos tener múltiples workloads consumiendo recursos hasta que el nodo empieza a quedarse sin memoria.
Revisa:
kubectl describe node <node>
Busca:
Conditions:
MemoryPressure:
Y:
kubectl top node <node>
¿Cómo resolver OOMKilled?
Aumentar el memory limit puede ser correcto.
Pero primero debemos responder:
¿Por qué la aplicación necesita más memoria?
Podemos tener:
OOMKilled
|
+--- Memory leak
|
+--- Limit demasiado bajo
|
+--- Tráfico mayor al esperado
|
+--- Cache sin límite
|
+--- Heap mal configurado
|
+--- Query o procesamiento demasiado grande
|
+--- Worker concurrency demasiado alta
|
+--- memory-backed volume
Una solución correcta podría ser cambiar:
resources:
requests:
memory: "512Mi"
limits:
memory: "1Gi"
a:
resources:
requests:
memory: "1Gi"
limits:
memory: "2Gi"
pero únicamente si los datos muestran que ese consumo es legítimo.
Si el proceso tiene un memory leak, simplemente aumentar el límite probablemente solo retrasará el siguiente OOM.
5. Pod Evictions
Ahora encontramos un Pod con:
STATUS: Evicted
Empieza con:
kubectl describe pod <pod> \
-n <namespace>
Y revisa el nodo:
kubectl describe node <node>
Kubernetes puede realizar node-pressure eviction cuando un nodo alcanza determinados niveles de presión de recursos.
El kubelet monitoriza señales relacionadas con recursos como:
Memory
Disk
Filesystem inodes
y puede terminar Pods para recuperar recursos y evitar que el nodo quede completamente degradado.
Node conditions
Un buen punto de partida es:
kubectl get nodes
seguido de:
kubectl describe node <node>
Busca:
MemoryPressure
DiskPressure
PIDPressure
Y revisa consumo:
kubectl top node <node>
Para memoria y CPU de todos los Pods del nodo:
kubectl get pods -A \
--field-selector spec.nodeName=<node> \
-o wide
Luego podemos revisar individualmente los workloads relevantes.
Eviction no es OOMKilled
Es fácil confundir ambos síntomas.
OOMKilled
|
Process/container killed
because of an OOM condition
!=
Evicted
|
kubelet removes a Pod
to reclaim constrained
node resources
La investigación es diferente.
Con un OOMKilled normalmente empezamos investigando el contenedor y su memoria.
Con Evicted debemos prestar especial atención al nodo y sus recursos.
6. QoS Classes
Kubernetes asigna una Quality of Service class a los Pods dependiendo de cómo estén definidos CPU y memoria.
Podemos verla con:
kubectl get pod <pod> \
-n <namespace> \
-o jsonpath='{.status.qosClass}'
Las clases son:
Guaranteed
Burstable
BestEffort
Guaranteed
En la configuración tradicional a nivel de contenedor, un Pod puede ser Guaranteed cuando todos sus contenedores tienen requests y limits para CPU y memoria, y request y limit son iguales para cada recurso.
Ejemplo:
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "500m"
memory: "512Mi"
Burstable
Un Pod que tiene algún request o limit de CPU/memoria, pero no cumple las condiciones de Guaranteed, normalmente pertenece a Burstable.
BestEffort
Un Pod sin requests ni limits de CPU/memoria queda normalmente como:
BestEffort
Ejemplo:
resources: {}
QoS y eviction
Cuando existe node pressure, la clase QoS es relevante, pero no es el único factor.
Kubernetes también considera factores como el consumo del workload respecto a sus requests y la prioridad del Pod al seleccionar candidatos para eviction.
Por eso no deberíamos resumir el comportamiento como:
BestEffort -> Burstable -> Guaranteed
y asumir que ese orden explica cualquier eviction.
QoS forma parte de una decisión más amplia.
7. Requests mal dimensionados
Existe otro problema frecuente.
Supongamos que tenemos:
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "2"
memory: "2Gi"
pero normalmente el workload consume:
CPU: 800m
Memory: 900Mi
El Pod podría funcionar.
Pero los requests le están diciendo al scheduler que el workload necesita mucho menos de lo que realmente suele consumir.
En muchos Pods esto puede contribuir a:
Node appears to have capacity
|
Scheduler places more Pods
|
Actual usage grows
|
Node becomes saturated
|
Resource pressure
Por eso requests no deberían escogerse únicamente para lograr que los Pods puedan ser scheduled.
Deberían reflejar de manera razonable el comportamiento real del workload.
8. ¿Qué pasa si los requests son demasiado altos?
El problema inverso también existe.
requests:
cpu: "4"
memory: "8Gi"
para un workload que normalmente utiliza:
CPU: 200m
Memory: 300Mi
puede desperdiciar capacidad de scheduling.
Aunque:
kubectl top nodes
muestre poca utilización, el scheduler puede no aceptar más Pods porque utiliza los recursos solicitados para determinar placement.
Para revisar allocation:
kubectl describe node <node>
Busca:
Allocated resources:
Esto permite comparar:
Actual utilization
vs
Requested resources
9. Métricas útiles con Prometheus
kubectl top es excelente para obtener una vista rápida.
Para un incidente real necesitamos normalmente observar el comportamiento en el tiempo.
Algunas métricas frecuentemente disponibles mediante kubelet/cAdvisor incluyen:
container_cpu_usage_seconds_total
container_memory_working_set_bytes
container_cpu_cfs_throttled_periods_total
container_cpu_cfs_periods_total
container_cpu_cfs_throttled_seconds_total
Y mediante kube-state-metrics podemos obtener información declarativa de Kubernetes, como requests y limits.
La disponibilidad exacta y los labels dependen de nuestro stack de monitoring.
CPU usage
Ejemplo:
sum by (namespace, pod) (
rate(
container_cpu_usage_seconds_total{
container!="",
container!="POD"
}[5m]
)
)
Memory working set
sum by (namespace, pod) (
container_memory_working_set_bytes{
container!="",
container!="POD"
}
)
Para entender un incidente no queremos únicamente:
current value
Queremos ver:
30 minutes before
|
incident begins
|
resource growth
|
failure
|
recovery
10. Correlacionar recursos con la aplicación
Supongamos:
14:00 CPU normal
14:05 Deployment
14:10 CPU increases
14:12 Latency increases
14:15 CPU throttling increases
14:20 5xx increases
Ahora tenemos una historia mucho más útil que:
"CPU is high."
Podemos correlacionar:
Deployment
|
v
CPU growth
|
v
Throttling
|
v
Latency
|
v
Errors
El siguiente paso podría ser revisar logs o traces para determinar qué parte del nuevo código está utilizando más CPU.
11. Revisar desde el nodo
Cuando sospechamos que el problema afecta al nodo completo:
kubectl describe node <node>
y:
kubectl top node <node>
son buenos puntos de partida.
Si necesitamos investigar más profundamente y tenemos permisos:
kubectl debug node/<node> \
-it \
--image=ubuntu
Dentro del entorno de debugging podemos inspeccionar el host según los permisos y configuración disponibles.
Por ejemplo:
free -h
df -h
df -i
Y para problemas del kubelet, en entornos donde tengamos acceso administrativo al host:
journalctl -u kubelet
Flujo de troubleshooting
Podemos resumir nuestra investigación así:
Performance issue
|
v
Define impact
|
v
kubectl top / metrics
|
+---------+---------+
| |
v v
CPU Memory
| |
+------+------+ +-----+------+
| | | |
High usage Throttling Growth OOMKilled
| | | |
+------+------+ +-----+------+
| |
v v
Requests/limits Requests/limits
| |
+---------+---------+
|
v
Check node
|
+------------+-------------+
| | |
CPU pressure MemoryPressure DiskPressure
| | |
+------------+-------------+
|
v
Correlate application
|
v
Logs / Metrics / Traces
|
v
Root cause
Command reference
# Current usage
kubectl top pods -A
kubectl top nodes
# Container-level usage
kubectl top pod <pod> \
-n <namespace> \
--containers
# Sort by usage
kubectl top pods \
-n <namespace> \
--sort-by=cpu
kubectl top pods \
-n <namespace> \
--sort-by=memory
# Pod state
kubectl describe pod <pod> \
-n <namespace>
# Previous container state
kubectl get pod <pod> \
-n <namespace> \
-o jsonpath='{.status.containerStatuses[*].lastState.terminated}'
# Previous logs
kubectl logs <pod> \
-n <namespace> \
--previous
# Resource configuration
kubectl get deployment <deployment> \
-n <namespace> \
-o yaml
# Node investigation
kubectl describe node <node>
kubectl top node <node>
# Pods on a node
kubectl get pods -A \
--field-selector spec.nodeName=<node> \
-o wide
# QoS
kubectl get pod <pod> \
-n <namespace> \
-o jsonpath='{.status.qosClass}'
# Node debugging
kubectl debug node/<node> \
-it \
--image=ubuntu
Qué no hacer
Durante un incidente evita respuestas automáticas como:
OOMKilled
-> increase memory
High CPU
-> increase CPU
Evicted
-> add another node
Esas acciones podrían mitigar temporalmente el problema, pero no necesariamente resolver su causa.
En su lugar:
Symptom
|
v
Measure
|
v
Correlate
|
v
Find constraint
|
v
Understand workload
|
v
Change
|
v
Validate
Después de realizar un cambio, vuelve a revisar:
CPU;
memory;
throttling;
restart count;
latency;
errors;
node pressure;
application behavior.
Conclusión
Los problemas de recursos en Kubernetes ocurren en diferentes niveles.
Application
|
Container
|
Pod
|
Node
|
Cluster
Un Pod OOMKilled no significa automáticamente que el nodo se quedó sin memoria.
CPU alto no significa automáticamente que necesitamos más CPUs.
Y un Pod evicted no es lo mismo que un proceso eliminado por el OOM killer.
Para solucionar estos incidentes debemos conectar:
Requests + Limits
+
Actual Usage
+
Container State
+
Node Conditions
+
Historical Metrics
+
Application behavior
|
v
Root Cause
En el siguiente artículo de esta serie vamos a investigar networking en Kubernetes:
Services;
EndpointSlices;
DNS;
Ingress y Gateway API;
NetworkPolicy;
CNI;
conectividad entre Pods;
conectividad hacia servicios externos.
Seguiremos la conexión paquete por paquete para encontrar exactamente dónde se rompe la comunicación.
Comments