← maurobernal.com.ar

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 ---"

Tags

tsql (27)mssql (26)sql (20)devops (20)dotnet (18)docker (15)performance (14)contenedores (11)dotnet10 (10)linux (9)csharp (8)microservicios (7)angular (7)angular21 (7)sql server (6)issabel (6)docker-compose (6)typescript (6)mysql (5).NET (5)