Kubernetes Ghost Connections: Zastarané Conntrack DNAT Záznamy
Stale DNAT entry je bug, s ktorym sa stretavas az vo vacsom scale. “Requesty náhodne zlyhávajú s connection refused po škálovaní podov.” Príčina: Linux conntrack tabuľka drží DNAT záznamy pre zmazané pody a kube-proxy ich okamžite nečistí.
Prostredie: Kubernetes 1.20+, iptables mode kube-proxy, Services s viacerými backendmi, časté škálovacie eventy
Problém
Intermitentné Zlyhania
Časová os katastrofy:
T+0:00 3 pody bežia pre service-a
Pod IP: 10.0.1.10, 10.0.1.11, 10.0.1.12
Conntrack záznamy vytvorené
T+0:30 Škálovanie na 2 pody
Pod 10.0.1.12 terminovaný
Endpointy aktualizované: 10.0.1.10, 10.0.1.11
T+0:31 Nový request prichádza od klienta
Conntrack lookup: "Poznám tento flow! → 10.0.1.12"
Packet poslaný na zmazanú pod IP
Connection refused / timeout
T+2:00 Conntrack záznam expiruje (default 120s)
Traffic konečne ide na správne pody
Prečo Conntrack Cachuje Routes
Linux Connection Tracking (conntrack):
┌─────────────────────────────────────────────────────────────┐
│ Prichádzajúci packet na Service VIP 10.96.0.100:80 │
│ │
│ Prvý packet (žiadny conntrack záznam): │
│ 1. iptables DNAT pravidlo vyberie backend: 10.0.1.12 │
│ 2. Conntrack vytvorí záznam: │
│ src=10.0.2.50:45678 dst=10.96.0.100:80 │
│ → DNAT na 10.0.1.12:8080 │
│ 3. Záznam cachovaný po dobu spojenia + timeout │
│ │
│ Ďalšie packety (conntrack hit): │
│ 1. Lookup v conntrack tabuľke │
│ 2. Aplikuje cachovaný DNAT: → 10.0.1.12:8080 │
│ 3. OBCHÁDZA iptables pravidlá úplne! │
│ │
│ Problém: Pod 10.0.1.12 zmazaný ale conntrack záznam žije │
└─────────────────────────────────────────────────────────────┘
Príčina
Životný Cyklus Conntrack
# Zobrazenie aktuálnych conntrack záznamov
conntrack -L -d 10.96.0.100
# Výstup ukazujúci zastaraný záznam:
tcp 6 117 TIME_WAIT
src=10.0.2.50 dst=10.96.0.100 sport=45678 dport=80
src=10.0.1.12 dst=10.0.2.50 sport=8080 dport=45678 [ASSURED]
mark=0 use=1
# Tento záznam bude smerovať traffic na 10.0.1.12 ďalších 117 sekúnd
# Aj keď je pod preč!
Keď Kube-Proxy Nepomáha
// kube-proxy správanie pri odstránení endpointu:
// 1. Aktualizuje iptables pravidlá (odstráni backend)
// 2. NEČISTÍ conntrack záznamy
// Prečo? Výkon - conntrack cleanup je drahý
// Tiež: Race condition - pod sa možno len reštartuje
// Problémové flows:
// - Dlhodobé spojenia (gRPC streamy, WebSockets)
// - UDP traffic (DNS, metriky)
// - Connection pools s keepalive
// Tieto udržiavajú conntrack záznamy neobmedzene
UDP Je Obzvlášť Zlé
UDP conntrack timeout: 30 sekúnd (kratší ale stále problémový)
UDP flow na CoreDNS:
T+0:00 DNS query na kube-dns service
Conntrack: src=pod → dst=10.96.0.10 (kube-dns VIP)
DNAT na: 10.0.1.50 (coredns pod)
T+0:10 CoreDNS pod preschedulovaný na 10.0.1.51
T+0:15 Ďalšia DNS query
Conntrack hit → stále smeruje na 10.0.1.50
DNS timeout! Pod nič neresolvuje!
# UDP nemá connection state na detekciu zlyhania
# Klient stále posiela na mŕtvy endpoint
Diagnostika
Kontrola Zastaraných Záznamov
# Vypísanie všetkých conntrack záznamov pre service
kubectl exec -n kube-system <kube-proxy-pod> -- \
conntrack -L -d <service-cluster-ip> 2>/dev/null
# Nájdenie záznamov smerujúcich na neexistujúce endpointy
# Porovnajte s aktuálnymi endpointami:
kubectl get endpoints <service-name> -o yaml
# Hľadajte DNAT targety ktoré nie sú v endpoint liste
Monitoring Conntrack Tabuľky
# Štatistiky conntrack tabuľky
conntrack -S
# Výstup:
# cpu=0 found=12847 invalid=32 insert=0 insert_failed=0 drop=0
# cpu=1 found=11923 invalid=28 insert=0 insert_failed=0 drop=0
# Sledujte insert_failed - tabuľka môže byť plná
# Sledujte vysoké invalid - problémy so zastaranými záznamami
# Veľkosť tabuľky a limity
cat /proc/sys/net/netfilter/nf_conntrack_count
cat /proc/sys/net/netfilter/nf_conntrack_max
Korelácia s Pod Eventmi
# Sledujte mazanie podov a zlyhania spojení spolu
kubectl get events -w --field-selector reason=Killing &
kubectl logs -f <client-pod> | grep -i "connection refused"
# Ak connection refused narastajú po terminácii podov
# → zastaraný conntrack je pravdepodobne príčina
Riešenie
Možnosť 1: Graceful Terminácia Podu
apiVersion: v1
kind: Pod
spec:
terminationGracePeriodSeconds: 60
containers:
- name: app
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- |
# Najprv odstránenie zo service endpointov
# Počkanie na expiráciu conntrack záznamov
# Potom skutočná terminácia
sleep 30
// Vo vašej aplikácii - drain spojení pred ukončením
func main() {
// Spracovanie SIGTERM
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGTERM)
go func() {
<-sigChan
// Zastavenie prijímania nových spojení
listener.Close()
// Počkanie na dokončenie existujúcich spojení
// Alebo timeout po grace periode
server.Shutdown(context.Background())
}()
}
Možnosť 2: Agresívne Čistenie Conntrack
# Pri mazaní podu vyčistite conntrack záznamy
# Pridajte do pod preStop hook alebo controllera
# Zmazanie všetkých conntrack záznamov pre pod IP
conntrack -D -d <pod-ip>
conntrack -D -s <pod-ip>
# Pre services špecificky
conntrack -D -d <service-cluster-ip> --dport <service-port>
# DaemonSet pre čistenie conntrack
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: conntrack-cleaner
spec:
template:
spec:
hostNetwork: true
containers:
- name: cleaner
image: your-cleaner-image
securityContext:
capabilities:
add: ["NET_ADMIN"]
# Sleduje endpoint zmeny a čistí conntrack
Možnosť 3: Použitie IPVS Mode
# kube-proxy konfigurácia pre IPVS mode
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"
ipvs:
strictARP: true
# IPVS má lepšie sledovanie spojení
# Automatické čistenie pri odstránení backendu
# Výhody IPVS:
# - Connection tabuľka per-service (nie globálny conntrack)
# - Automatické čistenie pri odstránení backendu
# - Lepší výkon pri scale
# Kontrola aktuálneho módu
kubectl -n kube-system get cm kube-proxy -o yaml | grep mode
Možnosť 4: Client-Side Retry Logika
// Implementujte retry s backoff pre prechodné zlyhania
func callService(ctx context.Context) error {
backoff := []time.Duration{10*time.Millisecond, 100*time.Millisecond, 1*time.Second}
var lastErr error
for i := 0; i <= len(backoff); i++ {
resp, err := http.Get("http://service-a/endpoint")
if err == nil {
return nil
}
// Kontrola či je to connection refused (symptóm stale conntrack)
if isConnectionRefused(err) && i < len(backoff) {
time.Sleep(backoff[i])
continue
}
lastErr = err
}
return lastErr
}
func isConnectionRefused(err error) bool {
var opErr *net.OpError
if errors.As(err, &opErr) {
var syscallErr *os.SyscallError
if errors.As(opErr.Err, &syscallErr) {
return syscallErr.Err == syscall.ECONNREFUSED
}
}
return false
}
Možnosť 5: Zníženie Conntrack Timeoutov
# Tuning conntrack timeoutov (na úrovni nodu)
# VAROVANIE: Ovplyvňuje všetky spojenia na node
# TCP established (default 432000 = 5 dní!)
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=3600
# TCP time_wait (default 120)
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
# UDP (default 30)
sysctl -w net.netfilter.nf_conntrack_udp_timeout=10
sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=30
# Aplikovanie cez DaemonSet
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: sysctl-tuner
spec:
template:
spec:
initContainers:
- name: sysctl
image: busybox
securityContext:
privileged: true
command:
- sysctl
- -w
- net.netfilter.nf_conntrack_tcp_timeout_time_wait=30
Monitoring
groups:
- name: conntrack
rules:
- alert: ConntrackTableNearFull
expr: |
node_nf_conntrack_entries /
node_nf_conntrack_entries_limit > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Conntrack tabuľka {{ $value | humanizePercentage }} plná"
- alert: HighConntrackInsertFailed
expr: |
rate(node_nf_conntrack_stat_insert_failed[5m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "Conntrack inserty zlyhávajú - tabuľka plná alebo hash kolízia"
- alert: ServiceEndpointChurn
expr: |
changes(kube_endpoint_address_available[5m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Vysoký endpoint churn - riziko stale conntrack"
Checklist
## Kubernetes Conntrack Stale DNAT
### Diagnostika
- [ ] Skontrolujte conntrack záznamy: conntrack -L -d <service-ip>
- [ ] Porovnajte záznamy s aktuálnymi endpointami
- [ ] Hľadajte connection refused po scale eventoch
- [ ] Skontrolujte využitie conntrack tabuľky
### Prevencia
- [ ] Implementujte graceful termináciu podu (preStop hook)
- [ ] Pridajte sleep pred ukončením podu (expirácia conntrack)
- [ ] Zvážte IPVS mód pre lepšie čistenie
- [ ] Implementujte client retry logiku
### Tuning
- [ ] Skontrolujte hodnoty conntrack timeout
- [ ] Monitorujte veľkosť conntrack tabuľky
- [ ] Alertujte na endpoint churn rate
Záver
Poučenie: Kubernetes Services závisia na Linux conntrack pre smerovanie spojení, ale conntrack prežíva životné cykly podov. Zastarané DNAT záznamy spôsobujú “ghost connections” na zmazané pody.
Kľúčové princípy:
- Conntrack cachuje DNAT rozhodnutia - obchádza iptables pravidlá
- Zmazanie podu nečistí conntrack - záznamy žijú do timeoutu
- UDP je horšie ako TCP - žiadny connection state na detekciu zlyhaní
- IPVS mód to rieši lepšie - per-service connection tabuľky
Súvisiace Články
- Kubernetes DNS Resolution Failures - CoreDNS a DNS problémy
- Kubernetes Headless Service Stale DNS - DNS caching problémy
Súvisiace články
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.
Ghost Pod: Prečo váš Service stále posiela traffic na mŕtve endpointy
Náhodné ECONNRESET na niektorých nodoch. Endpointy vyzerajú správne. Vinník: conntrack NAT záznamy držia dlhodobé spojenia pripnuté k podom, ktoré už neexistujú.
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.
Vyčerpanie Ephemeral Portov: Node Ktorý 'Pokazí'
Jeden Kubernetes node začne zlyhávať pripojenia k externým službám zatiaľ čo pody vyzerajú zdravé. Skrytá príčina: sidecar proxy vyčerpávajú ephemeral porty krátkodobými spojeniami.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.