docs(docs-portal): reflejar fixes de MTU, gitea-runner y cosign digest/rekor
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
This commit is contained in:
1 parent
86fcc88a1e
commit
b0b832f142
6 files changed
+200
-81
No files matched your search
@@ -0,0 +1,123 @@
|
||||
# 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.
|
||||
Reference in new issue
Block a user