El parche perfecto: Cómo domé una fuga de memoria en Kubernetes con un CronJob inteligente
El Problema: Una API con hambre de RAM
No importa cuánta experiencia tengas, a veces el código te juega una mala pasada. Recientemente, me encontré con un desafío clásico pero persistente: una de nuestras APIs críticas desarrollada en .NET presentaba una fuga de memoria (memory leak).
En nuestro entorno On-Premise, corriendo sobre un cluster de SUSE Linux Enterprise (HA) con solo dos nodos, no podíamos permitir que un pod consumiera recursos hasta asfixiar al nodo o provocar un reinicio descontrolado por el OOMKiller. Si el pod llegaba al límite del Deployment (1500 Mi), la latencia subía y la experiencia del usuario se degradaba.
La Idea: Si no puedes curarlo (aún), manténlo limpio
Mientras el equipo de desarrollo investigaba el root cause en el código, necesitaba una solución operativa. Podría haber configurado un auto-reinicio simple, pero quería algo más «quirúrgico».
Mi premisa fue:
- No quiero reiniciar todos los pods a la vez (evitar downtime).
- Solo quiero matar al pod que esté realmente en peligro (umbral del 75-80%).
- Debo asegurar que el nuevo pod esté saludable antes de pasar al siguiente.
Como mi día a día es en la terminal con Zsh y mi fiel alias kub para kubectl, decidí automatizar mi propio flujo de trabajo.
La Implementación: RBAC y un Script de Bash
La solución no podía ser «toscamente» manual. Implementé un CronJob dentro del cluster que actúa como un recolector de basura inteligente.
1. Seguridad ante todo (RBAC)
No quería que el script corriera con permisos de administrador. Creé una ServiceAccount específica y un Role con los permisos mínimos necesarios: get, list y watch para pods y despliegues. Sin el permiso de watch, el script no podría esperar de forma segura a que la nueva réplica estuviera lista.
2. El «Cerebro» del Script
El script realiza un baile preciso:
- Consulta las métricas reales mediante
kubectl top pods. - Filtra los pods de mi API y calcula el porcentaje de uso basado en el límite real configurado en el Deployment.
- Si un pod supera el 75%, lo elimina.
- Inmediatamente ejecuta un
kubectl rollout status. Esto es vital: el script se pausa hasta que Kubernetes confirma que el nuevo pod pasó sus Health Checks.
3. El Toque On-Premise
Al estar en un cluster con OCFS2 (Oracle Cluster File System 2) y almacenamiento compartido RWX, la conmutación de pods es extremadamente fluida. El nuevo pod monta los volúmenes en segundos, ya sea en el nodea o en el nodeb, sin errores de bloqueo.
El Resultado: Estabilidad 24/7
Programé el CronJob para ejecutarse dos veces al día: a las 3 AM y a las 3 PM.
¿El resultado? El sistema ahora se «limpia» solo. En los logs de la última prueba, pude ver cómo el script identificaba un pod al 80% (1202 Mi), lo eliminaba y esperaba a que la nueva réplica estuviera al 100% antes de seguir. El resto de los pods, que estaban en niveles normales, no fueron tocados.
Esta solución me dio la tranquilidad de que, mientras llega el parche definitivo en el código, mi infraestructura sigue siendo robusta, predecible y, sobre todo, altamente disponible.
Lo que aprendí:
- Observabilidad:
kubectl topes tu mejor amigo cuando el Metrics Server está bien configurado. - RBAC es clave: No escatimes en configurar los verbos correctos (
watchme salvó la vida). - Automatiza tus parches: Un «parche» bien automatizado es una herramienta de ingeniería, no una chapuza.

kub create job --from=cronjob/memory-leak-patch test-mem-clean -n gag
job.batch/test-mem-clean created
❯ kub logs -f -l job-name=test-mem-clean -n gag
--- Iniciando chequeo de memoria ---
Umbral: 75% de 1500 Mi
>> Pod deploy-gag-api-prod-849d5cb498-z6trv excedido: 80% (1202 Mi). Matando...
pod "deploy-gag-api-prod-849d5cb498-z6trv" deleted from gag namespace
>> Esperando a que el reemplazo esté Ready...
Waiting for deployment "deploy-gag-api-prod" rollout to finish: 4 of 5 updated replicas are available...
deployment "deploy-gag-api-prod" successfully rolled out
>> Pod deploy-gag-api-prod-849d5cb498-xzxtb OK: 21% (326 Mi).
>> Pod deploy-gag-api-prod-849d5cb498-x8krt OK: 15% (235 Mi).
>> Pod deploy-gag-api-prod-849d5cb498-d22w4 OK: 10% (161 Mi).
>> Pod deploy-gag-api-prod-849d5cb498-wtlpr OK: 9% (144 Mi).
--- Saneamiento finalizado ---
Este es el yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-memory-cleaner
namespace: gag
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-memory-cleaner-role
namespace: gag
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "delete", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pod-memory-cleaner-binding
namespace: gag
subjects:
- kind: ServiceAccount
name: pod-memory-cleaner
namespace: gag
roleRef:
kind: Role
name: pod-memory-cleaner-role
apiGroup: rbac.authorization.k8s.io
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: memory-leak-patch
namespace: gag
spec:
# Se ejecuta a las 3:00 AM y 3:00 PM (15:00)
schedule: "0 3,15 * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
serviceAccountName: pod-memory-cleaner
restartPolicy: OnFailure
containers:
- name: patch-script
image: bitnami/kubectl:latest
command:
- /bin/sh
- -c
- |
NAMESPACE="gag"
DEPLOYMENT="deploy-gag-api-prod"
THRESHOLD=75
# Obtener límite de memoria del deployment
LIMIT=$(kubectl get deploy $DEPLOYMENT -n $NAMESPACE -o jsonpath='{.spec.template.spec.containers[0].resources.limits.memory}' | sed 's/Mi//')
echo "--- Iniciando saneamiento de memoria ---"
echo "Umbral configurado: $THRESHOLD% de $LIMIT Mi"
# Listar pods del deployment ordenados por mayor consumo de memoria
kubectl top pods -n $NAMESPACE --no-headers | grep "$DEPLOYMENT" | sort -k3 -rn | while read line; do
POD_NAME=$(echo $line | awk '{print $1}')
USAGE=$(echo $line | awk '{print $3}' | sed 's/Mi//')
# Cálculo de porcentaje
PERCENT=$(( USAGE * 100 / LIMIT ))
if [ "$PERCENT" -gt "$THRESHOLD" ]; then
echo ">> Pod $POD_NAME excedido: $PERCENT% ($USAGE Mi). Eliminando..."
kubectl delete pod $POD_NAME -n $NAMESPACE
echo ">> Esperando que la nueva réplica esté Ready (Rollout Status)..."
kubectl rollout status deployment/$DEPLOYMENT -n $NAMESPACE
# Margen para convergencia de red (MetalLB)
sleep 15
else
echo ">> Pod $POD_NAME OK: $PERCENT% ($USAGE Mi)."
fi
done
echo "--- Saneamiento finalizado ---"

Dejar un comentario
¿Quieres unirte a la conversación?Siéntete libre de contribuir!