Nuevo playbook del blackhole MTU cross-node (diagnostico + fix + validacion). Actualiza el playbook de contencion de recursos Gitea/runner de 'pendiente' a 'aplicado'. Actualiza cosign.md: firma por digest ya implementada, Rekor como decision tomada (no TODO). Corrige 2 links que quedaron rotos al renombrar la seccion de limitaciones (validado con mkdocs build --strict). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
124 lines
6.2 KiB
Markdown
124 lines
6.2 KiB
Markdown
# Incidente: blackhole de red cross-node por MTU/PMTUD (2026-08)
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Ventana del incidente** | Detectado ~2026-08, causa raíz confirmada y resuelto 2026-08-16 |
|
|
| **Servicio afectado** | Cualquier pod que necesite hablar con `commerce-postgres-0` (StatefulSet, sin réplicas, fijo a un nodo) desde un pod agendado en otro nodo del cluster k3d |
|
|
| **Impacto** | `medusa-deploy` quedaba en `CrashLoopBackOff`/`Init` si el scheduler lo mandaba a un nodo distinto al de Postgres — ver [incidente de crashloop de Medusa](incidente-crashloop-medusa.md), donde este mismo blackhole ya había aparecido como sospechoso sin confirmar |
|
|
|
|
## Síntoma
|
|
|
|
El handshake TCP (SYN/ACK) entre un pod y `commerce-postgres-0` funciona
|
|
perfecto cuando quedan en nodos distintos — `nc -zv` conecta sin problema.
|
|
Pero el primer paquete de datos real de la sesión (protocolo Postgres,
|
|
`pg_isready` incluido) nunca llega. La conexión se cuelga hasta hacer
|
|
timeout, sin ningún error explícito de por medio.
|
|
|
|
## Diagnóstico
|
|
|
|
Aislando el nodo con `nodeName` en pods de prueba: **100% de fallos
|
|
(186/186 intentos)** agendando cross-node; 100% de éxito en el mismo nodo.
|
|
Reproducible al 100%, no es intermitente.
|
|
|
|
Un primer intento de diagnóstico con `ping` normal (sin bit DF) no mostró
|
|
pérdida de paquetes hasta payloads de 2000 bytes — porque sin el bit DF el
|
|
kernel simplemente fragmenta y todo "funciona", lo cual **enmascaró** el
|
|
problema real durante un tiempo. La pista real solo aparece forzando el bit
|
|
DF (`ping -M do`), que es exactamente lo que hace un socket TCP real con
|
|
Path MTU Discovery activo — y ahí es donde Postgres (y cualquier TCP con
|
|
payload grande) se cuelga en silencio.
|
|
|
|
!!! warning "Herramienta correcta importa"
|
|
El `ping` de BusyBox (el que trae la imagen de nodo de k3s,
|
|
`rancher/k3s`) **no soporta `-M do`** — no hay forma de reproducir esto
|
|
con un `kubectl exec` directo a cualquier pod. Hizo falta un pod de
|
|
debug con una imagen completa (`nicolaka/netshoot`, trae `ping` de
|
|
`iputils` con soporte real de DF) agendado explícitamente en un nodo
|
|
distinto al de Postgres.
|
|
|
|
Con esa herramienta, un sweep de payloads crecientes contra el pod de
|
|
Postgres dio el límite exacto:
|
|
|
|
| Payload interno | Resultado |
|
|
|---|---|
|
|
| 1420 bytes | 0% de pérdida |
|
|
| **1450 bytes** | **100% de pérdida, sin ningún ICMP "frag needed"** |
|
|
| 1472/1500 bytes | 100% de pérdida |
|
|
|
|
Ese límite coincide casi exactamente con el MTU que la propia interfaz
|
|
`flannel.1` (la interfaz VXLAN de flannel dentro de cada nodo) ya reportaba:
|
|
**1450**, calculado como `eth0` del nodo (1500) menos 50 bytes de overhead
|
|
VXLAN. La aritmética de flannel es correcta — pero en la práctica, el
|
|
último tramo de red no entrega el paquete VXLAN a tamaño completo, y en vez
|
|
de generar el ICMP "fragmentation needed" que permitiría a Path MTU
|
|
Discovery ajustarse solo, el paquete se pierde en silencio. Eso es
|
|
exactamente un **blackhole de PMTUD**: el mecanismo que debería avisar
|
|
"bajá el tamaño" está roto, no solo el límite en sí.
|
|
|
|
## Causa raíz
|
|
|
|
Un tramo de la red (probablemente el puente Docker externo que conecta los
|
|
contenedores de los distintos nodos k3d entre sí) tiene una capacidad real
|
|
menor a los 1500 bytes que todos los niveles superiores (Docker, flannel)
|
|
asumen — y ese tramo descarta los paquetes que no entran, en vez de
|
|
fragmentarlos o avisar por ICMP. flannel calcula su propio MTU
|
|
correctamente a partir de lo que Docker le reporta, pero lo que Docker le
|
|
reporta no refleja la capacidad real de punta a punta.
|
|
|
|
## Fix aplicado
|
|
|
|
`k3s` (usado en este lab) **embebe** flannel — no lo corre como un
|
|
DaemonSet/binario separado con su propio archivo de configuración editable
|
|
como en un cluster "vanilla". Confirmado con `k3s server --help`: no existe
|
|
un flag `--flannel-mtu` en esta versión. El único punto de ajuste real es
|
|
la interfaz `flannel.1` en vivo, dentro de cada contenedor de nodo:
|
|
|
|
```bash
|
|
docker exec <nodo> ip link set flannel.1 mtu 1400
|
|
```
|
|
|
|
1400 queda con margen de sobra debajo del límite real confirmado (1420
|
|
bueno / 1450 blackhole).
|
|
|
|
Este ajuste **no persiste** en ningún archivo — se pierde si el contenedor
|
|
del nodo se reinicia. Automatizado en `scripts/deploy-lab.sh`, función
|
|
`fix_flannel_mtu()`, llamada tanto en `create_cluster()` (cluster nuevo)
|
|
como en `start_cluster()` (cluster existente que arranca) — espera hasta
|
|
20s a que la interfaz `flannel.1` exista antes de ajustarla, por si el nodo
|
|
todavía no terminó de inicializar su red.
|
|
|
|
Bajar el MTU de `flannel.1` en el host **solo afecta pods nuevos** — la CNI
|
|
de flannel asigna el MTU del `veth` de cada pod al crearlo, leyendo el
|
|
`flannel.1` del nodo en ese momento. Los pods que ya estaban corriendo se
|
|
reciclaron con `kubectl rollout restart` sobre todo el namespace
|
|
`ecommerce` (`commerce-postgres`, `commerce-redis`, `commerce-minio`,
|
|
`medusa-deploy`, `catalog-api-deploy`, `frontend-deploy`) para que
|
|
tomaran el MTU nuevo de inmediato, en vez de esperar a que se recrearan
|
|
solos.
|
|
|
|
## Validación
|
|
|
|
El mismo sweep de `ping -M do` después del fix ya no se cuelga en
|
|
silencio — devuelve `Frag needed and DF set (mtu = 1400)`, la respuesta
|
|
ICMP correcta que le permite al kernel ajustar el tamaño de los paquetes
|
|
solo. Path MTU Discovery, en otras palabras, **ya funciona** en vez de
|
|
estar bloqueado.
|
|
|
|
Validación final con tráfico real, no solo `ping`: conexión TCP cross-node
|
|
exitosa hacia el puerto 5432 de Postgres, y una transferencia de 50 KB
|
|
(muy por encima del umbral que antes se colgaba) completada sin problema.
|
|
`shop.cruzcloud.net` y `commerce.cruzcloud.net/app` respondiendo `200`
|
|
después de reciclar todos los pods.
|
|
|
|
## Sobre el `podAffinity` en `medusa.yaml`
|
|
|
|
El workaround que existía antes de este fix —
|
|
`podAffinity`/`requiredDuringSchedulingIgnoredDuringExecution` forzando a
|
|
`medusa-deploy` a agendarse siempre en el mismo nodo que
|
|
`commerce-postgres-0` — **se deja tal cual está**, a propósito. Ya no es
|
|
necesario para evitar el blackhole, pero no tiene ningún costo real, y
|
|
sirve como red de contención si el MTU real del entorno vuelve a cambiar
|
|
en el futuro (por ejemplo, tras una actualización de ZimaOS o de Docker).
|
|
No se debe interpretar como deuda pendiente de "limpiar" — es una decisión
|
|
consciente.
|