Ghost Pod: Prečo váš Service stále posiela traffic na mŕtve endpointy
Stale sme videli traffic z podov, ktore uz neexistovali. “Prečo tento jeden node dostáva connection resety k databáze?” PostgreSQL pody boli zdravé. Service endpointy vyzerali správne. kubectl describe svc ukazoval správne backend pody. Ale jeden aplikačný node mal intermitentné zlyhania s ECONNRESET zatiaľ čo ostatné boli v poriadku. Strávili sme dva dni kontrolovaním aplikačného kódu, konfigurácie poolov, network policies—všetko vyzeralo perfektne.
Prelom prišiel keď som sa pripojil cez SSH na zlyhávajúci node a spustil conntrack -L | grep 5432. Tam to bolo: NAT mapovanie ukazujúce na 10.244.3.47—IP adresu ktorá patrila podu ukončenému počas včerajšieho rolling deploymentu. Connection pool otvoril toto spojenie pred deploymentom a conntrack stále verne prekladal pakety na destináciu ktorá už neexistovala. Kernel robil presne to čo mal; len mal zastaralý stav zo spojenia ktoré prežilo pod s ktorým komunikovalo.
Toto je jeden z najfrustrujúcejších Kubernetes networking problémov pretože všetko vyzerá v poriadku. Endpointy sú správne. kube-proxy pravidlá sú správne. Nové spojenia fungujú perfektne. Ale existujúce dlhodobé spojenia—databázové pooly, gRPC streamy, WebSocket spojenia—môžu zostať pripnuté k mŕtvym endpointom cez conntrack NAT mapovania ktoré pretrvávajú kým sa spojenie nezatvorí alebo nevyprší.
Základný problém je nesúlad medzi Kubernetes pohľadom na endpointy (aktualizovaný okamžite keď pod skončí) a kernelovou conntrack tabuľkou (zachováva NAT mapovania po dobu života spojenia). Keď kube-proxy aktualizuje iptables pravidlá aby odstránil endpoint, neinvaliduje—a nemôže—existujúce conntrack záznamy pre nadviazané spojenia. Tieto spojenia pokračujú v používaní starého NAT mapovania, posielajúc pakety do prázdna.
Prostredie: Kubernetes 1.28+, kube-proxy v iptables móde, dlhodobé TCP spojenia (connection pooly, gRPC, WebSockets)
Pochopenie mechanizmu
Ako kube-proxy a conntrack spolupracujú
Normálny Service flow:
Klient Pod (10.244.1.50:45678)
|
| SYN na ClusterIP (10.96.100.50:5432)
↓
iptables DNAT pravidlo (spravované kube-proxy)
|
| Preloží na backend: 10.244.3.47:5432
↓
conntrack vytvorí NAT mapovanie:
src=10.244.1.50:45678 → dst=10.96.100.50:5432
reply: src=10.244.3.47:5432 → dst=10.244.1.50:45678
|
↓
Backend Pod (10.244.3.47:5432)
Toto mapovanie pretrvá po celú dobu života spojenia.
Problém Ghost Podu
Časová os zlyhania:
T+0:00 Connection pool otvára spojenie na postgres-svc
conntrack záznam vytvorený: → 10.244.3.47:5432
Spojenie je ESTABLISHED, funguje dobre
T+1:00 Začína rolling deployment
Nový postgres pod: 10.244.3.48
Starý postgres pod (10.244.3.47) sa ukončuje
T+1:05 kube-proxy aktualizuje iptables pravidlá
Service teraz routuje na 10.244.3.48
NOVÉ spojenia idú na nový pod ✓
T+1:10 Starý pod úplne ukončený
IP 10.244.3.47 už neexistuje
ALE: conntrack záznam stále mapuje na ňu!
T+1:15 Aplikácia znovupoužije poolované spojenie
Paket ide na 10.244.3.47 (cez conntrack)
Žiadna destinácia → ECONNRESET alebo timeout
Výsledok: Niektoré spojenia zlyhávajú, iné fungujú
Zlyhanie je lokálne pre node (conntrack je per-node)
Nové spojenia vždy fungujú (používajú aktualizované pravidlá)
Prečo len niektoré nody?
conntrack je lokálny pre node:
Node A:
├── Pod 1 (otvoril spojenie v T+0)
│ └── conntrack: → starý pod 10.244.3.47 ← ZASTARALÝ
├── Pod 2 (otvoril spojenie v T+2)
│ └── conntrack: → nový pod 10.244.3.48 ← OK
└── Nové spojenia → 10.244.3.48 ✓
Node B:
├── Pod 3 (žiadne existujúce spojenia)
│ └── Všetky spojenia → 10.244.3.48 ✓
└── Žiadne zastaralé conntrack záznamy
Len Pod 1 na Node A vidí zlyhania!
To to robí ako bug v aplikácii, nie networking.
Diagnostika Ghost Podov
Kontrola conntrack záznamov
# SSH na postihnutý node
# Nájdi conntrack záznamy pre tvoj service
conntrack -L -p tcp --dport 5432 2>/dev/null | head -20
# Príklad výstupu ukazujúci zastaralý záznam:
# tcp 6 86393 ESTABLISHED src=10.244.1.50 dst=10.96.100.50 sport=45678 dport=5432
# src=10.244.3.47 dst=10.244.1.50 sport=5432 dport=45678 [ASSURED] mark=0
# Reply src=10.244.3.47 je skutočný backend
# Ak tá IP už neexistuje → ghost pod!
# Skontroluj či backend IP existuje
kubectl get pods -o wide | grep 10.244.3.47
# (žiadny výstup = ghost pod potvrdený)
# Spočítaj conntrack záznamy podľa destinácie
conntrack -L -p tcp 2>/dev/null | \
grep -oP 'src=\K[0-9.]+(?= dst)' | \
sort | uniq -c | sort -rn | head
Porovnaj Endpointy vs conntrack
# Získaj aktuálne endpointy
kubectl get endpoints postgres-svc -o jsonpath='{.subsets[*].addresses[*].ip}'
# Výstup: 10.244.3.48 10.244.3.49
# Získaj conntrack destinácie pre ten service
conntrack -L -p tcp --dport 5432 2>/dev/null | \
grep -oP 'reply src=\K[0-9.]+' | sort -u
# Výstup: 10.244.3.47 10.244.3.48 10.244.3.49
# 10.244.3.47 je v conntrack ale nie v endpoints = ghost!
Identifikuj postihnuté spojenia
# Nájdi všetky zastaralé conntrack záznamy (IP nie v endpoints)
ENDPOINTS=$(kubectl get endpoints postgres-svc -o jsonpath='{.subsets[*].addresses[*].ip}' | tr ' ' '|')
conntrack -L -p tcp --dport 5432 2>/dev/null | while read line; do
DEST=$(echo "$line" | grep -oP 'reply src=\K[0-9.]+')
if ! echo "$DEST" | grep -qE "^($ENDPOINTS)$"; then
echo "ZASTARALÝ: $line"
fi
done
Oprava
Možnosť 1: Vymaž zastaralé conntrack záznamy
# Nukleárna možnosť: vymaž všetky conntrack pre destination port
conntrack -D -p tcp --dport 5432
# Chirurgickejšie: vymaž záznamy pre konkrétnu mŕtvu IP
conntrack -D -p tcp --reply-src 10.244.3.47
# Automatizovaný cleanup skript
#!/bin/bash
# cleanup-ghost-conntrack.sh
SERVICE=$1
PORT=$2
# Získaj aktuálne platné endpointy
ENDPOINTS=$(kubectl get endpoints $SERVICE -o jsonpath='{.subsets[*].addresses[*].ip}' | tr ' ' '\n')
# Nájdi a vymaž zastaralé záznamy
conntrack -L -p tcp --dport $PORT 2>/dev/null | while read line; do
DEST=$(echo "$line" | grep -oP 'reply src=\K[0-9.]+')
if ! echo "$ENDPOINTS" | grep -qF "$DEST"; then
echo "Mažem zastaralý záznam na $DEST"
conntrack -D -p tcp --dport $PORT --reply-src $DEST
fi
done
Možnosť 2: Správny Connection Draining
# Pod spec so správnym termination handling
apiVersion: v1
kind: Pod
spec:
terminationGracePeriodSeconds: 60
containers:
- name: postgres
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
# Signál app aby prestala prijímať nové spojenia
pg_ctl stop -m smart -w -t 30
# Počkaj kým existujúce spojenia odídu
sleep 30
# Readiness probe zlyhá okamžite pri SIGTERM
readinessProbe:
exec:
command: ["pg_isready"]
periodSeconds: 5
Možnosť 3: Konfigurácia Connection Poolu
// HikariCP - Vynúť validáciu spojenia
HikariConfig config = new HikariConfig();
config.setConnectionTimeout(5000);
config.setValidationTimeout(3000);
config.setMaxLifetime(1800000); // 30 minút max
config.setKeepaliveTime(30000); // TCP keepalive každých 30s
config.setConnectionTestQuery("SELECT 1");
// Kľúčové: Obmedz životnosť spojenia
config.setMaxLifetime(300000); // 5 minút - kratšie ako typický deploy
// Go sql.DB - Nastav životnosť spojenia
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(1 * time.Minute)
// Pre gRPC - Zapni keepalive s krátkym timeoutom
conn, err := grpc.Dial(
target,
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 10 * time.Second,
Timeout: 3 * time.Second,
PermitWithoutStream: true,
}),
)
Možnosť 4: Prepni na IPVS mód
# kube-proxy config - IPVS má lepší connection tracking
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: ipvs
ipvs:
scheduler: lc # least connection
syncPeriod: 30s
minSyncPeriod: 5s
# IPVS môže gracefully drainovať spojenia
tcpTimeout: 900s
tcpFinTimeout: 30s
udpTimeout: 300s
Monitoring
Prometheus Alerty
groups:
- name: conntrack-ghost-pods
rules:
- alert: ZastareConntrackZaznamy
expr: |
conntrack_stale_entries > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Detekované zastaralé conntrack záznamy"
- alert: ConnectionResetSpike
expr: |
rate(tcp_connection_resets_total[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Vysoká miera TCP connection resetov"
- alert: ServiceEndpointChurn
expr: |
changes(kube_endpoint_address_available[10m]) > 5
for: 5m
labels:
severity: info
annotations:
summary: "Vysoký endpoint churn - skontroluj ghost pody"
Checklist
## Prevencia a reakcia na Ghost Pod
### Detekcia
- [ ] SSH na postihnutý node a skontroluj conntrack záznamy
- [ ] Porovnaj conntrack destinácie vs aktuálne endpointy
- [ ] Over že zlyhania sú node-lokálne (nie cluster-wide)
- [ ] Koreluj chyby s nedávnymi deploymentami
### Okamžitá oprava
- [ ] Vymaž zastaralé conntrack záznamy pre postihnutý service
- [ ] Reštartuj postihnuté pody pre čerstvé spojenia
- [ ] Over že nové spojenia fungujú správne
### Prevencia
- [ ] Nastav max lifetime connection poolu < frekvencia deploymentov
- [ ] Konfiguruj správne preStop hooky pre graceful drain
- [ ] Zapni TCP keepalive na dlhodobých spojeniach
- [ ] Zváž IPVS mód pre lepší connection tracking
### Monitoring
- [ ] Alert na connection reset spiky
- [ ] Monitoruj endpoint churn rate
- [ ] Sleduj rast conntrack tabuľky
- [ ] Loguj koreláciu medzi resetmi a deploymentami
Záver
Ghost pod problém je perfektný príklad ako vrstvená architektúra Kubernetes môže vytvárať subtílne failure módy. Kubernetes aktualizuje svoj endpoint register okamžite keď pod skončí. kube-proxy aktualizuje iptables pravidlá v priebehu sekúnd. Ale kernelová conntrack tabuľka—ktorá sleduje NAT stav pre existujúce spojenia—nevie a nestará sa o Kubernetes abstrakcie. Verne udržiava mapovania pre spojenia ktoré sú stále “established” z pohľadu TCP, aj keď destinácia bola zmazaná.
Frustrujúce je že debugovacie nástroje vám klamú. kubectl get endpoints ukazuje správny stav. iptables -L -t nat ukazuje správne pravidlá. Network policies sú v poriadku. Service je zdravý. Len keď sa ponoríte do conntrack -L na konkrétnych nodoch uvidíte zastaralé NAT mapovania spôsobujúce zlyhania.
Kľúčové princípy:
- conntrack je per-node a pretrvá cez endpoint zmeny—existujúce spojenia nevidia kube-proxy aktualizácie
- Dlhodobé spojenia sú riziko—connection pooly, gRPC streamy, WebSockets môžu prežiť pody
- Zlyhania sú node-lokálne—rôzne nody majú rôzny conntrack stav, čo robí diagnózu mätúcou
- Nastav max connection lifetime < frekvencia deploymentov—recykluj spojenia pred tým ako sa stanú zastaralými
- Kontroluj conntrack najprv keď vidíš intermitentné ECONNRESET k Services ktoré vyzerajú zdravo
Ghost pod možno práve teraz straší váš cluster. Skontrolujte conntrack -L na node po vašom ďalšom deploymene.
Súvisiace články
- Kubernetes Conntrack Table Exhaustion - Keď sa conntrack zaplní
- gRPC Keepalive konfigurácia - Keepalive pre dlhodobé spojenia
Súvisiace články
kube-proxy Mikro-Výpadky: Problém xtables Lock Contencie
Náhodné 1-3 sekundové výpadky spojení počas deploymentov. CPU vyzerá v poriadku, pamäť stabilná. Skrytá príčina: iptables-restore drží xtables lock počas endpoint churnu.
Kubernetes Ghost Connections: Zastarané Conntrack DNAT Záznamy
Service vracia zlé pod IP po škálovaní. Príčina: Linux conntrack drží DNAT záznamy dlhšie ako existujú pody, smeruje traffic na zmazané endpointy.
Traffic Ide na Mŕtve Pody: Conntrack Zastaralé NAT Mapovanie
Deploy spôsobuje 503 presne 2 minúty. Problém: conntrack drží NAT mapovanie na staré pod IP aj po tom čo Kubernetes odstráni endpointy.
Kubernetes conntrack Vyčerpanie: Tichý Zabijak Paketov
Náhodné DNS timeouty, dropped spojenia, služby timeout-ujú. Vaša nf_conntrack tabuľka je plná. Ukážem ako diagnostikovať, monitorovať a opraviť tento K8s networking problém.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.