Späť na blog

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:

  1. Conntrack cachuje DNAT rozhodnutia - obchádza iptables pravidlá
  2. Zmazanie podu nečistí conntrack - záznamy žijú do timeoutu
  3. UDP je horšie ako TCP - žiadny connection state na detekciu zlyhaní
  4. IPVS mód to rieši lepšie - per-service connection tabuľky

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. "Kubernetes Ghost Connections: Zastarané Conntrack DNAT Záznamy". https://www.michal-drozd.com/sk/blog/kubernetes-conntrack-stale-dnat/ (Publikované 5. februára 2025).