Listado de la etiqueta: ram

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:

  1. No quiero reiniciar todos los pods a la vez (evitar downtime).
  2. Solo quiero matar al pod que esté realmente en peligro (umbral del 75-80%).
  3. 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 top es tu mejor amigo cuando el Metrics Server está bien configurado.
  • RBAC es clave: No escatimes en configurar los verbos correctos (watch me 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 ---"