Skip to content

usage gate: el reintento de /check puede reintentar contra el mismo endpoint muerto #28

Description

@sre-helmcode

Qué pasa

Cuando un nodo desaparece de golpe, los pods de platform-api que vivían en él siguen en el EndpointSlice hasta que el control-plane los declara perdidos. Durante esa ventana, el usage hook de LiteLLM balancea hacia una réplica inalcanzable, se come el timeout y falla abierto: la petición se sirve sin comprobar el cap.

El fix de 2026-08-07 (project_nan_usage_gate_fail_open_fix) cubre el caso ordenado: preStop.sleep de 5s, maxUnavailable: 0, y CHECK_RETRIES=1. Eso resuelve los rollouts, donde el pod avisa antes de morir. No cubre la pérdida abrupta de un nodo, donde no hay preStop que valga.

Y el reintento no lo salva: CHECK_RETRIES=1 reintenta una vez, pero si el balanceo le devuelve el mismo endpoint muerto, el reintento se pierde igual.

Evidencia (2026-08-19)

19 eventos de nan_usage_gate_fail_open_total, todos con cause="platform_api_unreachable", en dos ventanas y ninguna otra:

Momento (CEST) Eventos Qué pasaba
19:13 3 reinicio de nan-eu001, que alojaba una réplica
20:38-20:39 16 nan-eu007 aislado por firewall, alojaba otra réplica

En ambos casos los eventos cesaron en cuanto el nodo volvió. nan_usage_gate_check_retry_total se quedó en 5 repartidos, muy por debajo de los 19 fail-open, lo que confirma que el reintento no está absorbiendo estos casos: si estuviera funcionando veríamos los dos contadores subir juntos.

Propuesta

Que el reintento excluya el endpoint que acaba de fallar en vez de rebalancear a ciegas. Opciones, de menos a más invasiva:

  1. Resolver los endpoints y reintentar contra otro explícitamente, en vez de volver a pegarle al Service y confiar en el balanceo.
  2. Subir CHECK_RETRIES a 2 con backoff corto. Más probable acertar un endpoint vivo, pero sigue siendo probabilístico y sube el peor caso de latencia.
  3. Bajar el timeout de conexión (distinto del de lectura) para que un endpoint muerto se descarte rápido y quede tiempo para el reintento dentro del presupuesto actual.

La (1) es la correcta; la (3) es la más barata y ya ayudaría.

Sea cual sea, el test que lo demuestra es el mismo: matar un nodo con una réplica de platform-api y comprobar que fail_open_total no se mueve. Hoy sí se mueve.

Contexto

  • El fail-open es deliberado y correcto: preferimos servir sin capar a cortar el servicio. El problema no es la política, es que la ventana es más ancha de lo necesario.
  • Afecta solo a modelos capados de pago por token (glm5.2, deepseek-v4-flash, mimo-v2.5), o sea gasto sin comprobar.
  • La alerta nan-usage-hook-fail-open-trickle usa increase(...[6h]) > 15, así que un pico puntual la deja disparada 6 horas. Eso es correcto por diseño, pero conviene saberlo al triarla: mira primero cuándo ocurrieron los eventos, no solo el total.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions