Späť na blog

Kubernetes Headless Service DNS: Zastarané Záznamy Po Zmazaní Podu

Headless services su super, kym stale DNS nespravi cluster strasidelny. “Connection refused na pod ktorý by mal existovať.” Príčina: headless services vracajú individuálne pod IP, ale DNS caching znamená že klienti skúšajú zmazané pod IP minúty po terminácii.

Prostredie: Kubernetes 1.25+, headless services pre StatefulSets alebo priamy prístup k podom, client-side DNS caching

Problém

Incident So Zastaranými Spojeniami

Časová os:

T+0s    StatefulSet má 3 repliky: pod-0, pod-1, pod-2
        DNS: myapp-headless.ns.svc → [10.0.1.10, 10.0.1.11, 10.0.1.12]

T+10s   pod-2 je zmazaný (scale down alebo rolling update)
T+11s   Endpoints controller odstráni 10.0.1.12 z endpoints
T+12s   CoreDNS prijme aktualizované endpoints
T+13s   CoreDNS aktualizuje svoju cache

T+15s   Klient pod (s 30s DNS cache TTL) dotazuje myapp-headless
        Klientova cached odpoveď: [10.0.1.10, 10.0.1.11, 10.0.1.12]
        Klient skúsi 10.0.1.12 → "Connection refused"

T+45s   Klient DNS cache expiruje, dostane čerstvú odpoveď
        Konečne funguje správne

Okno nefunkčných spojení: ~30 sekúnd (DNS TTL)

Prečo Sú Headless Services Iné

# Regulárny ClusterIP service:
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
  ports:
  - port: 80
# DNS vráti: jednu ClusterIP (10.96.x.x)
# kube-proxy riadi routing na zdravé pody
# Zmazanie podu = neviditeľné pre klientov

# Headless service:
apiVersion: v1
kind: Service
metadata:
  name: myapp-headless
spec:
  clusterIP: None  # <-- Headless!
  selector:
    app: myapp
  ports:
  - port: 80
# DNS vráti: VŠETKY pod IP priamo
# Klient si vyberá na ktorý pod sa pripojí
# Zmazanie podu = klient môže mať zastaranú IP v cache

Príčina

Vrstvy DNS Cachovania

Cesta DNS dotazu a cachovanie:

┌─────────────────────────────────────────────────────────────┐
│ Aplikácia                                                   │
│ └─► Jazyková DNS cache (Go: neobmedzená, Java: 30s default)│
│     └─► glibc nscd cache (ak povolená)                     │
│         └─► Node-local DNS cache (ak nasadená)             │
│             └─► CoreDNS (pods cache: 30s default)          │
│                 └─► Endpoints API (zdroj pravdy)           │
└─────────────────────────────────────────────────────────────┘

Každá vrstva môže držať zastarané dáta!

Predvolené TTL:
- CoreDNS pods plugin: 5s (ale klienti cachujú dlhšie)
- CoreDNS cache plugin: 30s
- Java InetAddress: 30s (alebo navždy so security manager)
- Go net.Resolver: žiadna vstavaná cache, ale používa systémový resolver
- glibc s nscd: konfigurovateľné, často 60s+
- Node-local DNS: konfigurovateľné, často 30s

Race Condition

Časovanie zmazania podu:

T+0.000s  kubectl delete pod pod-2
T+0.050s  API server označí pod ako Terminating
T+0.100s  Endpoints controller SLEDUJE zmenu
T+0.150s  Endpoints objekt aktualizovaný (pod-2 IP odstránená)
T+0.200s  CoreDNS prijme Endpoints update cez watch
T+0.250s  CoreDNS invaliduje svoju cache entry

ALE: Klient dotazoval v T-5.000s
     Klientova cached DNS expiruje v T+25.000s
     Ešte 25 sekúnd klient používa zastaranú IP!

Pri zlyhaniach spojenia:
- TCP: okamžité "Connection refused" (pod IP nesmerovateľná)
- Ak IP pridelená novému podu: nesprávne dáta/nesprávny service!

Diagnostika

Skontroluj DNS TTL v Odpovediach

# Dotazuj CoreDNS priamo pre headless service
kubectl run -it --rm debug --image=alpine --restart=Never -- \
  nslookup -debug myapp-headless.default.svc.cluster.local

# Hľadaj TTL v odpovedi:
# myapp-headless.default.svc.cluster.local
#     origin = ns.dns.cluster.local
#     ttl = 30  <-- Toto je cachované client-side!

Skontroluj Časovanie Endpoints

# Sleduj zmeny endpoints
kubectl get endpoints myapp-headless -w

# Porovnaj s časovaním zmazania podu
kubectl get pods -w

# Skontroluj latenciu aktualizácie endpoints
kubectl get endpoints myapp-headless -o json | \
  jq '.subsets[].addresses[].ip'

Identifikuj Cachované Zastarané IP

# Z klient podu, skontroluj čo si myslí že sú IP
kubectl exec client-pod -- getent hosts myapp-headless.default.svc

# Porovnaj so skutočnými endpoints
kubectl get endpoints myapp-headless -o jsonpath='{.subsets[*].addresses[*].ip}'

# Ak rozdielne, klient má zastaranú cache

Riešenie

Možnosť 1: Zníž DNS TTL

# CoreDNS ConfigMap - zníž cache TTL
apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {
        kubernetes cluster.local in-addr.arpa ip6.arpa {
           pods insecure
           ttl 5  # Zníž z 30s na 5s
           fallthrough in-addr.arpa ip6.arpa
        }
        cache 10  # Zníž aj cache TTL
        # ... zvyšok configu
    }

Možnosť 2: Client-Side TTL Konfigurácia

// Java: Vypni DNS caching (neodporúča sa pre všetky prípady)
java.security.Security.setProperty("networkaddress.cache.ttl", "5");
java.security.Security.setProperty("networkaddress.cache.negative.ttl", "1");
// Go: Použi custom resolver s krátkou cache
import "github.com/rs/dnscache"

resolver := &dnscache.Resolver{
    Timeout: 5 * time.Second,
}

// Refreshuj cache periodicky
go func() {
    t := time.NewTicker(5 * time.Second)
    for range t.C {
        resolver.Refresh(true)
    }
}()

Možnosť 3: Connection Retry s Re-resolve

// Retry zlyhané spojenia s DNS re-resolution
func connectWithRetry(service string) (net.Conn, error) {
    var lastErr error
    for attempt := 0; attempt < 3; attempt++ {
        // Vynúť DNS re-resolution pri každom pokuse
        ips, err := net.LookupHost(service)
        if err != nil {
            lastErr = err
            continue
        }

        for _, ip := range ips {
            conn, err := net.DialTimeout("tcp", ip+":80", 5*time.Second)
            if err == nil {
                return conn, nil
            }
            lastErr = err
        }

        time.Sleep(time.Duration(attempt+1) * time.Second)
    }
    return nil, fmt.Errorf("všetky pokusy zlyhali: %w", lastErr)
}

Možnosť 4: Použi Regulárny Service so Session Affinity

# Ak nepotrebuješ priame adresovanie podov,
# použi regulárny ClusterIP service namiesto toho
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  type: ClusterIP  # Nie headless
  selector:
    app: myapp
  ports:
  - port: 80
  sessionAffinity: ClientIP  # Ak potrebuješ sticky sessions

Možnosť 5: Graceful Shutdown s Oneskorením

# Daj DNS čas na propagáciu pred tým než pod prestane prijímať spojenia
spec:
  containers:
  - name: myapp
    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sh
          - -c
          - |
            # Prestaň prijímať nové spojenia
            touch /tmp/shutdown
            # Počkaj na propagáciu DNS
            sleep 10
            # Potom ukonči

    readinessProbe:
      exec:
        command:
        - /bin/sh
        - -c
        - "test ! -f /tmp/shutdown"
      periodSeconds: 2

Monitoring

groups:
  - name: headless-dns
    rules:
      - alert: HeadlessDNSStaleRecords
        expr: |
          rate(dns_lookup_failures_total{service_type="headless"}[5m]) > 0.1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Vysoké zlyhania DNS lookup pre headless services"

      - alert: EndpointsUpdateDelay
        expr: |
          histogram_quantile(0.99,
            rate(endpoint_update_latency_seconds_bucket[5m])
          ) > 1
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Endpoints aktualizácie trvajú > 1s"

Checklist

## Headless Service Zastaraná DNS

### Symptómy
- [ ] "Connection refused" na pody ktoré boli nedávno zmazané
- [ ] Intermitentné zlyhania počas rolling updates
- [ ] Problémy sa vyriešia po čakaní ~30 sekúnd
- [ ] Funguje s regulárnymi services, zlyháva s headless

### Diagnostika
- [ ] Skontroluj DNS TTL v CoreDNS configu
- [ ] Porovnaj klient cached IP so skutočnými endpoints
- [ ] Zmeraj propagáciu endpoints update
- [ ] Skontroluj client-side DNS caching nastavenia

### Riešenia
- [ ] Zníž CoreDNS TTL pre kubernetes plugin
- [ ] Nakonfiguruj klient DNS cache TTL
- [ ] Pridaj connection retry s re-resolution
- [ ] Použi preStop hook pre graceful shutdown
- [ ] Zváž regulárny ClusterIP ak priame adresovanie podov nie je potrebné

Záver

Lekcia: headless services vystavujú surové pod IP, čo znamená že DNS caching ťa vystavuje zastaraným endpoints. Regulárne ClusterIP services toto skrývajú za kube-proxy real-time routing.

Kľúčové princípy:

  1. DNS caching existuje na viacerých vrstvách - klient, node-local, CoreDNS
  2. Zníž TTL pre headless services - 5s namiesto 30s
  3. Implementuj client-side retry s re-resolution
  4. Použi graceful shutdown aby DNS malo čas na propagáciu

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 Headless Service DNS: Zastarané Záznamy Po Zmazaní Podu". https://www.michal-drozd.com/sk/blog/kubernetes-headless-service-stale-dns/ (Publikované 22. novembra 2024).