Späť na blog

Traffic Ide na Mŕtve Pody: Conntrack Zastaralé NAT Mapovanie

|
| kubernetes, networking, conntrack, debugging, deployment, nat

Tento bug bol ako duch: nove pody sa nepripojili, stare ano. “Každý deploy spôsobuje presne 2 minúty 503 chýb.” Pridali sme preStop hooks, zvýšili terminationGracePeriod, vyladili readiness probes—nič nepomohlo. Vinník bol conntrack držiaci NAT mapovanie na mŕtve pod IP.

Prostredie: Kubernetes 1.27, NodePort služby, vysoko zaťažené stateless API

Problém

Strašidelný Vzor

Každý jednotlivý deployment:

T+0:00  Nové pody ready, staré pody terminujú
T+0:00  Endpointy aktualizované (kube-proxy syncuje)
T+0:00  503 chyby začínajú
T+0:05  503 rate: 5%
T+0:30  503 rate: 3%
T+1:00  503 rate: 2%
T+2:00  503 rate: 0% (konečne!)

Vždy presne 2 minúty.
Rovnaký vzor zakaždým.
Žiadne výnimky.

Prečo Štandardné Fixe Nefungujú

# Skúšali sme všetko:

# Dlhší termination grace period - nepomohlo
terminationGracePeriodSeconds: 120

# preStop hook delay - nepomohlo
lifecycle:
  preStop:
    exec:
      command: ["sleep", "30"]

# Agresívna readiness probe - nepomohla
readinessProbe:
  periodSeconds: 1
  failureThreshold: 1

# Problém nie je pod lifecycle
# Je to conntrack tabuľka nodu!

Príčina

Ako Conntrack Funguje

Normálny request flow s NodePort:

Klient → Node:30080 → iptables DNAT → Pod:8080

conntrack záznam vytvorený:
┌─────────────────────────────────────────────────────┐
│ tcp  src=client:54321 dst=node:30080                │
│      src=pod:8080     dst=client:54321 [ASSURED]    │
│      timeout=432000 (5 dní!)                        │
└─────────────────────────────────────────────────────┘

Tento záznam pamätá: "traffic z client:54321 ide na pod:8080"

Problém Počas Deploymentu

Časová os:

T+0:00  Staré pod IP: 10.1.1.100
        Nové pod IP: 10.1.1.200

        Kubernetes: "Endpoint 10.1.1.100 odstránený!"
        kube-proxy: "iptables pravidlá aktualizované!"

        Ale conntrack tabuľka stále má:
        ┌─────────────────────────────────────────┐
        │ tcp  src=client:54321 dst=node:30080    │
        │      src=10.1.1.100:8080 dst=client     │  ← Ukazuje na MŔTVY pod!
        │      timeout=stale_zostava_cas          │
        └─────────────────────────────────────────┘

T+0:01  Klient pošle paket na rovnakom spojení
        → Conntrack: "Toto poznám! Pošli na 10.1.1.100"
        → Paket ide na mŕtvy pod
        → Connection reset / timeout
        → 503 chyba!

T+2:00  Conntrack záznamy konečne expirujú
        Nové spojenia dostanú nový NAT na 10.1.1.200
        Chyby prestanú

Prečo Je To Presne 2 Minúty

# Skontroluj conntrack timeout pre established TCP
cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established
# 432000 (5 dní - nie je relevantné)

# 2-minútový vzor pochádza z:
# 1. Klient-side keepalive/retry nastavení
# 2. HTTP klient connection pool timeout
# 3. Load balancer health check intervalov

# Conntrack záznam sám osebe by mohol žiť dni
# Ale klient nakoniec vzdá a vytvorí nové spojenie

Diagnostika

Krok 1: Sleduj Conntrack Počas Deploy

# Pred deploy, poznač si client IP
CLIENT_IP="203.0.113.10"

# Sleduj conntrack záznamy pre toho klienta počas deploy
watch -n 0.5 "conntrack -L -s $CLIENT_IP 2>/dev/null | grep -E '(ESTABLISHED|TIME_WAIT)'"

# Uvidíš záznamy ukazujúce na staré pod IP
# aj po tom čo pod zmizol

Krok 2: Over Traffic na Mŕtvy Pod

# tcpdump na node počas deploy
tcpdump -i any host 10.1.1.100  # staré pod IP

# Uvidíš pakety posielané na staré IP
# po tom čo pod zmizol

Krok 3: Spočítaj Zastaralé Záznamy

#!/bin/bash
# count-stale-conntrack.sh

# Ziskaj aktuálne endpoint IP
VALID_IPS=$(kubectl get endpoints my-service -o jsonpath='{.subsets[*].addresses[*].ip}')

# Spočítaj conntrack záznamy ukazujúce na neplatné IP
conntrack -L 2>/dev/null | while read line; do
  DST_IP=$(echo "$line" | grep -oP 'dst=\K[0-9.]+' | head -1)
  if [[ ! " $VALID_IPS " =~ " $DST_IP " ]]; then
    echo "STALE: $line"
  fi
done | wc -l

Riešenie

Možnosť 1: Flush Conntrack Počas Deploy

# Pridaj do deploymentu ako preStop na STARÉ pody
lifecycle:
  preStop:
    exec:
      command:
        - /bin/sh
        - -c
        - |
          # Počkaj na propagáciu endpoint removal
          sleep 5
          # Ziskaj IP tohto podu
          POD_IP=$(hostname -i)
          # Flush conntrack záznamy ukazujúce na tento pod
          # Vyžaduje NET_ADMIN capability
          conntrack -D -d $POD_IP || true
# Udeľ NET_ADMIN capability
securityContext:
  capabilities:
    add: ["NET_ADMIN"]

Možnosť 2: Node-Level Conntrack Flush

# DaemonSet ktorý sleduje endpoint zmeny
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: conntrack-flusher
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: conntrack-flusher
  template:
    metadata:
      labels:
        app: conntrack-flusher
    spec:
      hostNetwork: true
      serviceAccountName: conntrack-flusher
      containers:
        - name: flusher
          image: alpine
          securityContext:
            privileged: true
          command:
            - /bin/sh
            - -c
            - |
              apk add --no-cache conntrack-tools curl
              while true; do
                # Sleduj endpoint deletions cez API
                # Flush conntrack keď sú pody odstránené
                sleep 10
              done

Možnosť 3: Použi Headless Service (Vyhni Sa NAT)

# Headless service = priame pod IP, žiadny NAT, žiadny conntrack problém
apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  clusterIP: None  # Headless!
  selector:
    app: my-app
  ports:
    - port: 8080

# Klienti sa pripájajú priamo na pod IP
# Žiadny DNAT = žiadne zastaralé conntrack záznamy
# Ale vyžaduje client-side load balancing

Možnosť 4: Zníž Conntrack Timeouty

# Zníž established connection timeout (opatrne!)
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=120

# Zníž FIN_WAIT timeout
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_fin_wait=30

# Zníž TIME_WAIT timeout
sysctl -w net.netfilter.nf_conntrack_tcp_timeout_time_wait=30

# Toto ovplyvňuje VŠETKY spojenia, nie len zastaralé
# Môže rozbiť long-lived spojenia

Možnosť 5: Graceful Connection Draining

// V aplikácii: drain spojenia pred shutdown
func gracefulShutdown(srv *http.Server) {
    // Signalizuj že sa vypíname
    // Prestaň akceptovať nové spojenia na health endpointe
    healthStatus.Store(false)

    // Počkaj kým load balancer prestane posielať traffic
    time.Sleep(10 * time.Second)

    // Teraz gracefully shutdown existujúce spojenia
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    srv.Shutdown(ctx)
}

Monitoring

Prometheus Metriky

groups:
  - name: conntrack
    rules:
      - alert: StaleConntrackEntries
        expr: |
          node_nf_conntrack_entries > 50000 AND
          rate(node_nf_conntrack_entries[5m]) < 0
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Možné zastaralé conntrack záznamy na {{ $labels.instance }}"

      - alert: DeploymentErrors
        expr: |
          sum(rate(http_requests_total{status=~"5.."}[1m])) BY (deployment) /
          sum(rate(http_requests_total[1m])) BY (deployment) > 0.01
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "Zvýšená 5xx miera počas deploymentu"

Checklist

## Conntrack Zastaralé NAT Mapovanie

### Symptómy
- [ ] Chyby trvajú presne 2+ minúty po deploy
- [ ] Rovnaký vzor každý deployment
- [ ] Dlhší preStop/terminationGrace nepomáha
- [ ] Používa NodePort alebo LoadBalancer service

### Diagnostika
- [ ] Sleduj conntrack počas deploy
- [ ] Porovnaj conntrack dst IP vs aktuálne endpointy
- [ ] tcpdump traffic na staré pod IP

### Riešenia
- [ ] Flush conntrack záznamy pre umierajúce pody
- [ ] Použi headless service (ak možné)
- [ ] Implementuj proper connection draining
- [ ] Zníž conntrack timeouty (opatrne)

### Prevencia
- [ ] Pridaj NET_ADMIN capability pre conntrack flush
- [ ] Implementuj graceful shutdown v app
- [ ] Zváž service mesh (rieši toto automaticky)

Záver

Tento problém je frustrujúci pretože:

  1. Štandardné pod lifecycle fixe nepomáhajú - problém je na node úrovni
  2. Vzor je strašidelne konzistentný - presne 2 minúty zakaždým
  3. Vyžaduje kernel-level pochopenie - conntrack nie je viditeľný pre K8s
  4. Fix vyžaduje elevated privileges - NET_ADMIN alebo privileged

Fundamentálny problém: Kubernetes endpoint updaty sa dejú na inej vrstve než Linux conntrack. Nekomunikujú spolu, takže zastaralé NAT mapovanie pretrvávajú.


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. "Traffic Ide na Mŕtve Pody: Conntrack Zastaralé NAT Mapovanie". https://www.michal-drozd.com/sk/blog/conntrack-stale-nat-mapovanie/ (Publikované 14. novembra 2024).