Späť na blog

Ghost Pod: Prečo váš Service stále posiela traffic na mŕtve endpointy

|
| kubernetes, networking, conntrack, debugging, kube-proxy, iptables

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:

  1. conntrack je per-node a pretrvá cez endpoint zmeny—existujúce spojenia nevidia kube-proxy aktualizácie
  2. Dlhodobé spojenia sú riziko—connection pooly, gRPC streamy, WebSockets môžu prežiť pody
  3. Zlyhania sú node-lokálne—rôzne nody majú rôzny conntrack stav, čo robí diagnózu mätúcou
  4. Nastav max connection lifetime < frekvencia deploymentov—recykluj spojenia pred tým ako sa stanú zastaralými
  5. 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

Súvisiace články

Citujte tento článok

Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.

Michal Drozd. "Ghost Pod: Prečo váš Service stále posiela traffic na mŕtve endpointy". https://www.michal-drozd.com/sk/blog/kubernetes-ghost-pod-conntrack/ (Publikované 5. januára 2025).