Inicio / Artículos

Kubernetes autoreparable en acción: tres fallos, señales de SigNoz y soluciones aprobadas

Kubernetes autoreparable con OpenTelemetry y SigNoz: un bucle de fallos, una eliminación por falta de memoria y un aumento repentino de tokens en Kind, qué indica cada señal y las soluciones de IA aprobadas.

Editor: REACTAPP.TOP11 min de lectura2499 palabras

Fuentes: Self-Healing Kubernetes: Wiring OpenTelemetry, SigNoz, and a Groq-Powered Remediation Agent (PART 7)

La demostración cuenta con varios componentes en movimiento. Un clúster local de Kind alberga a SigNoz para los registros y logs, además de una aplicación en Python cuyos tres puntos finales están dañados intencionadamente. Un agente de corrección busca anomalías y solicita un diagnóstico a Groq, mientras que un panel de control de React permite al operador aprobar o rechazar la solución sugerida antes de que se ejecute cualquier cosa. Una vez que todo esto está implementado, lo interesante es observar su comportamiento. Esta prueba activa cada fallo con toda la infraestructura en funcionamiento, examina lo que registra SigNoz para cada incidente y enumera los cambios necesarios antes de que alguien pueda confiar en esta configuración más allá de un ordenador portátil.

Confirmar que la infraestructura está activa

Deben estar en ejecución cinco elementos antes de que se active algún fallo. La primera verificación es asegurarse de que cada pod esté en buen estado en ambos namespaces.

# All pods should show Running
kubectl get pods -n observability
kubectl get pods -n apps

Los reenvíos de puertos para SigNoz, el servicio de resumen y el panel de control requieren cada uno su propia pestaña de terminal, y la última orden del bloque sigue los registros del agente.

# Port-forwards (run each in its own tab if not already open)
# Tab A: kubectl port-forward -n observability svc/signoz 8080:8080
# Tab B: kubectl port-forward -n apps svc/summarizer 8000:80
# Tab C: kubectl port-forward -n apps svc/approval-dashboard 9000:9000
# Tab D: kubectl port-forward -n apps svc/approval-frontend-svc 3000:3000# Confirm the agent is polling
kubectl logs -n apps deployment/remediation-agent --tail=5

Las líneas de estado “all-clear” recientes en esa salida indican que el agente está en buen estado. Un pod que no esté en estado “Running” debe ser inspeccionado con kubectl describe y reparado primero. Mantenga visibles dos pestañas del navegador: SigNoz en el puerto 8080 y el panel de control en el puerto 3000.

Escenario uno: CrashLoopBackOff

Al establecer una variable de entorno KILL_ME en la implementación del summarizer y luego acceder a su punto de finalización por fallo, el proceso muere a mitad de la solicitud.

kubectl set env deployment/summarizer -n apps KILL_ME=true
curl -s http://localhost:8000/crash

La llamada a curl puede no devolver nada o quedarse atascada, ya que el proceso se cierra mientras está en ejecución. Al monitorear los pods se observa que el contador de reinicios aumenta y el estado cambia entre Running y CrashLoopBackOff; dos o tres ciclos son suficientes para ello.

kubectl get pods -n apps -w

En SigNoz. El resumen del servicio de resumen muestra que la tasa de errores sube al 100% en cuanto el pod comienza a fallar, mientras que la tasa de solicitudes desciende a cero cada vez que el pod está inactivo entre reinicios. El tráfico, luego nada, y de nuevo tráfico es la señal visual de un bucle de fallos. Una implementación gradual se ve de manera diferente, con un cambio limpio y sin aumento repentino de errores, y una partición de red también se presenta de forma distinta, con una tasa de errores en aumento mientras el pod sigue en estado de ejecución.

La pestaña de Registros, filtrada para el resumen, contiene la línea crítica que la aplicación emitió justo antes de detenerse.

CRITICAL summarizer — KILL_ME is true, exiting process

Ese registro contiene el ID de seguimiento de la solicitud que lo activó, por lo que con un solo clic se pasa de la línea del registro al seguimiento y a la solicitud HTTP exacta que finalizó el proceso.

El agente reacciona. Dentro de los 30 segundos del primer fallo, se inicia el ciclo de monitoreo. Detecta una tasa de errores de 1.0 frente a un umbral de 0.10, agrupa la señal y llama a Groq. Los registros del agente muestran esta secuencia.

kubectl logs -n apps deployment/remediation-agent --tail=20 -f
WARNING  agent — crash loop signal: error_rate=1.00
INFO     agent — anomaly detected: crash_loop — calling Groq for diagnosis
INFO     agent — diagnosis: type=crash_loop severity=critical confidence=0.91
           fix=kubectl set env deployment/summarizer -n apps KILL_ME-
INFO     incident_poster — incident posted: id=<uuid> type=crash_loop

Aprobación. El panel muestra la tarjeta en unos diez segundos, y la solución propuesta es la correcta, por lo que el operador hace clic en Aprobar. El backend verifica que la orden comience con kubectl set env y no contenga términos bloqueados, y luego la ejecuta. La tarjeta se actualiza para confirmar la aprobación y el cambio en el entorno.

✓ Approved by operator
deployment.apps/summarizer env updated

De vuelta en la pantalla de monitoreo del módulo, el siguiente arranque tiene éxito, el bucle de fallos termina y la tasa de errores de SigNoz vuelve a cero. Antes del siguiente escenario, una verificación rápida confirma que la variable ya no existe.

# The approve action already removed KILL_ME, but confirm:
kubectl get deployment summarizer -n apps -o jsonpath='{.spec.template.spec.containers[0].env}' | jq

KILL_ME no debe aparecer en el resultado.

Escenario dos: OOMKill

Un bucle de solicitudes POST al endpoint de fugas aumenta poco a poco un contenedor en la memoria.

for i in {1..30}; do
  curl -s -X POST http://localhost:8000/leak | jq -c '{leaked_mb,chunks}'
  sleep 0.5
done

La memoria aumenta mientras se monitorea la cápsula, y alrededor de la llamada número 25 la cápsula se detiene a mitad de ciclo, tras lo cual curl comienza a devolver errores de conexión.

En SigNoz. Esta vez la tasa de errores no aumenta de forma gradual. La detención por falta de memoria es abrupta, y la aplicación nunca devuelve una respuesta de error. En cambio, la tasa de solicitudes disminuye drásticamente: tráfico, parada brusca, un intervalo, y luego nuevamente tráfico una vez que la cápsula se reinicia.

Los registros contienen la historia real. Al filtrarlos con el resumidor, se observa que al intervalo le preceden advertencias relacionadas con un tamaño creciente del bucket.

WARNING summarizer — leak endpoint hit, bucket now holds 10 MB
WARNING summarizer — leak endpoint hit, bucket now holds 20 MB
...
WARNING summarizer — leak endpoint hit, bucket now holds 240 MB

After the last warning comes silence, with no exception, no stack trace and no graceful shutdown message, because the kernel's OOM killer gives the process no chance to log. A run of climbing warnings followed by nothing is the pattern worth recognizing.

The cluster confirms the cause from its side.

kubectl describe pod -n apps -l app=summarizer | grep -A 5 "Last State"
Last State:     Terminated
  Reason:       OOMKilled
  Exit Code:    137

El código de salida 137 es la firma del kernel: SIGKILL (señal 9) más 128. Esto permite distinguir una eliminación por falta de memoria de un fallo de la aplicación, que termina con el código 1, y de un fallo en la prueba de actividad, que también da 137 pero con una cadena de razón diferente en la salida de descripción.

El agente reacciona. Su verificación de falta de memoria busca en el índice de registros de SigNoz la cadena OOMKilled durante los últimos diez minutos. Si se encuentra, agrupa las muestras de registros y el evento de reinicio del pod en un mensaje, y se obtiene el diagnóstico correspondiente.

type=oom_kill severity=critical confidence=0.88
fix=kubectl set resources deployment/summarizer -n apps \
  --containers=summarizer --limits=memory=512Mi

La solución propuesta duplica el límite de memoria. Es una medida a corto plazo razonable, ya que permite ganar tiempo para implementar la corrección en la aplicación. Después de aprobarla, el backend aplica un límite de 512Mi, Kubernetes realiza un reinicio progresivo y el pod vuelve a funcionar con margen de maniobra.

La fuga en sí permanece en el código. El cambio solo eleva ese límite, y un incidente real requeriría una corrección en el código que elimine periódicamente el acumulador de fugas. La tarea del agente es detener la fuga, pero la causa raíz sigue siendo una tarea que debe realizar un humano.

Escenario tres: latencia y aumento repentino de tokens

Un bucle corto de llamadas al endpoint costoso provoca el tercer fallo.

for i in {1..5}; do
  curl -s -X POST http://localhost:8000/expensive \
    | jq '{output_tokens, length_chars}'
  sleep 2
done

Nada se cae ni se detiene, y el pod sigue en ejecución. Sin embargo, las respuestas tardan entre 8 y 15 segundos cada una, y el recuento de tokens se acerca al límite máximo de 2,048.

En SigNoz. Es en estos fallos silenciosos donde la capa de observabilidad demuestra su valor. La línea de latencia p95, que permanece estable cerca de 1.5 segundos durante el tráfico normal de resumen, aumenta cuando se realizan las llamadas costosas; su forma refleja un cambio brusco y no una desviación gradual, ya que cada llamada alcanza el límite máximo. Al ordenar las trazas por duración, esas llamadas aparecen en la parte superior, con un tiempo de 8 a 15 segundos cada una.

Al abrir un seguimiento lento en el gráfico de llamas se observa casi todo el tiempo dentro de un único span hijo correspondiente a la solicitud HTTP saliente hacia api.groq.com. El span del propio manejador del endpoint es muy breve, ya que el manejador de FastAPI prácticamente no hace nada, y el tiempo dedicado a las comunicaciones con el LLM domina. La longitud del contenido de respuesta de ese span es mucho mayor que en una llamada normal de resumen, lo que se refleja en el conteo de tokens de salida mostrado en bytes en la transmisión. Una línea plana, un aumento, y de nuevo línea plana cuando termina el bucle: ese es el perfil de un endpoint mal configurado, no de una falla en la infraestructura.

El agente reacciona. La verificación de latencia compara el valor p95 por ruta. La ruta más costosa arroja un tiempo de aproximadamente 11,000 ms, superando el umbral de 5,000 ms, y el agente agrupa los datos correspondientes.

{
  "check": "latency_spike",
  "spikes": { "/expensive": 11240.0 },
  "p95_ms_by_route": {
    "/summarize": 1820.0,
    "/expensive": 11240.0,
    "/healthz": 4.0
  }
}

Groq lo lee y genera un diagnóstico preciso.

type=token_spike severity=high confidence=0.85
root_cause: The /expensive endpoint has no reasonable max_tokens budget
            and a prompt that invites long outputs, causing p95 latency
            to spike to 11s versus 1.8s on the normal /summarize path.
fix=kubectl set env deployment/summarizer -n apps MAX_TOKENS_EXPENSIVE=256

La solución propuesta establece una variable de entorno que la aplicación puede leer para limitar el presupuesto de tokens en ese endpoint. Después de aprobarla, se configura la variable. En una implementación real, la aplicación también leería MAX_TOKENS_EXPENSIVE desde el entorno. El agente propone utilizar una configuración en lugar de realizar cambios en el código, lo cual es el alcance adecuado para una solución a nivel kubectl.

Tres fallos en un mismo período de tiempo

Al ejecutar los tres escenarios en secuencia, el resumen obtenido en un período de dos horas muestra tres patrones distintos.

  • El bucle de caídas es una forma en diente de sierra en la tasa de errores, que aumenta al 100%, desciende a cero y vuelve a aumentar, con correspondientes disminuciones en el rendimiento.
  • La interrupción por falta de memoria es un descenso brusco en la tasa de solicitudes sin aumento en los errores, ya que el proceso nunca llegó a devolver un error.
  • El aumento repentino en los tokens representa un aumento claro en la latencia p95, mientras que la tasa de errores se mantiene estable, ya que el pod nunca falló y solo se volvió lento y costoso.

Tres modos de fallo con tres formas reconocibles aparecen en un único panel de control, sin necesidad de reglas de alerta personalizadas ni análisis manual de registros. Ese es el beneficio de los rastros, métricas y registros correlacionados.

Cambios necesarios para producción

La demostración funciona en una laptop y omite aspectos importantes en otros entornos.

Sustituir el almacenamiento temporal de incidentes en memoria. La lista de incidentes es un diccionario de Python dentro del pod backend, por lo que un reinicio hace que se pierdan todos los datos. Usar SQLite a través de SQLAlchemy implica un cambio de una sola línea que no requiere ningún servicio adicional, y Redis es adecuado cuando el panel de control debe escalar entre varias réplicas backend. La estructura de la API permanece igual y solo cambia el almacenamiento que la respalda.

Añadir notificaciones de Slack. Actualmente, es necesario que alguien esté vigilando el panel de control para darse cuenta de que ha aparecido una tarjeta. Una llamada a webhook de Slack dentro del receptor de incidentes del backend soluciona este problema.

import httpx
SLACK_WEBHOOK = os.environ.get("SLACK_WEBHOOK_URL")def notify_slack(card: dict):
    if not SLACK_WEBHOOK:
        return
    d = card["diagnosis"]
    httpx.post(SLACK_WEBHOOK, json={
        "text": (
            f"*{d['severity'].upper()} — {d['incident_type']}* on `{card['service']}`\n"
            f"Root cause: {d['root_cause']}\n"
            f"Proposed fix: `{d['proposed_fix']}`\n"
            f"Confidence: {int(d['confidence']*100)}%\n"
            f"<http://your-dashboard-url|Review on dashboard>"
        )
    })

Llamar al notificador al final del receptor implica que cada nuevo incidente envía una señal al canal de atención, y el aprobador ya no necesita mantener abierta una pestaña del navegador.

Maneje correctamente las credenciales del clúster. El backend utiliza kubectl, lo cual está bien cuando el entorno del nodo lo configura así. En un clúster real, el pod del backend necesita una ServiceAccount con permisos RBAC adecuados y el cliente de Python de Kubernetes configurado para utilizar credenciales dentro del clúster.

from kubernetes import client, config
config.load_incluster_config()
apps_v1 = client.AppsV1Api()
core_v1 = client.CoreV1Api()

Cada tipo de comando aprobado se convierte entonces en una llamada directa al SDK y no en una cadena de shell. Esto es más verboso, pero permite un manejo estructurado de errores, registro de auditoría en el servidor de API y no existe riesgo de inyección de shell.

Restringa más la lista de permisos. La demostración acepta cualquier comando kubectl set env. En entornos de producción, esto debería limitarse a despliegues y nombres de espacio con nombre específico.

ALLOWED_TARGETS = {
    "deployment/summarizer -n apps",
    "deployment/worker -n apps",
}
def validate_command(cmd: str) -> bool:
    if not any(cmd.startswith(p) for p in ALLOWED_PREFIXES):
        return False
    if any(term in cmd for term in BLOCKED_TERMS):
        return False
    if not any(target in cmd for target in ALLOWED_TARGETS):
        return False
    return True

Un modelo no debe poder aplicar una solución a ningún espacio de nombres o despliegue fuera de la lista que se le haya proporcionado.

Proyectos que vale la pena estudiar a continuación

La demostración se creó desde cero para mostrar cómo se conectan las diferentes partes, mientras que su uso en entornos reales se basaría en proyectos existentes.

  • KubeStellar es útil cuando las cargas de trabajo abarcan varios clústeres. Absorbe las tareas del plano de control que complican la corrección en múltiples clústeres, por ejemplo, identificando en qué clúster se encuentra un incidente antes de proponer cualquier solución.
  • OpenTelemetry va mucho más allá de lo necesario para la demostración. El Collector cuenta con procesadores y exportadores que pueden añadir metadatos de Kubernetes a los spans de forma automática, por lo que no es necesario agregar manualmente los atributos de recursos en los manifiestos de la aplicación.
  • KEDA abarca la categoría de escalado automático: esta solución corrige los ajustes que a veces propone el agente y que no deberían ser aprobados manualmente uno por uno. Si la solución consiste siempre en escalar cuando la profundidad de la cola supera un cierto valor, eso debe gestionarse mediante un KEDA ScaledObject y no mediante aprobación humana.
  • Robusta y HolmesGPT son versiones de nivel de producción relacionadas con lo que se desarrolló aquí. Después de observar la capa de consultas, la estructura de los prompts y el mecanismo de aprobación, resulta fácil seguir su código.

Qué contiene toda la implementación

El sistema final es un clúster Kind de tres nodos con una topología de programación multi-nodo realista, y SigNoz desplegado a través de Helm que ingiere rastros, métricas y registros mediante OpenTelemetry Collector. Un servicio Python FastAPI se instrumenta automáticamente al iniciar, de modo que cada solicitud HTTP y cada llamada al LLM se convierte en un rastro, y cada línea de registro se correlaciona sin necesidad de código de instrumentación en la aplicación.

Tres puntos finales dañados provocan los tres fallos más comunes en Kubernetes: una caída del proceso, agotamiento de memoria y un empeoramiento en los costos y la latencia. Un agente de corrección consulta SigNoz cada 30 segundos, aplica verificaciones de umbrales, agrupa las señales en una solicitud estructurada, solicita un diagnóstico a un LLM y publica una tarjeta de incidente. Un backend de FastAPI recibe estas tarjetas, valida las soluciones propuestas contra una lista de permisos y ejecuta los comandos aprobados mediante kubectl. El panel de control de React incluye controles de aprobación y rechazo, un distintivo que muestra la confianza del modelo, un banner para diagnósticos poco fiables y un registro de cada acción realizada.

Todo funciona localmente con APIs de nivel gratuito y no cuesta nada. El código es lo suficientemente pequeño, con unas 600 líneas de Python, una página HTML y unos pocos archivos YAML, como para que cada parte pueda ser comprendida de principio a fin.

Leer más

  • Un panel de aprobación para un agente de remediación de Kubernetes: FastAPI y React — Crea un backend con FastAPI que utilice una lista de permisos kubectl y un panel de aprobación en React, de modo que un agente de remediación basado en LLM pueda solucionar incidentes solo después de que una persona haga clic en Aprobar.