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:
- DNS caching existuje na viacerých vrstvách - klient, node-local, CoreDNS
- Zníž TTL pre headless services - 5s namiesto 30s
- Implementuj client-side retry s re-resolution
- Použi graceful shutdown aby DNS malo čas na propagáciu
Súvisiace články
- Kubernetes DNS Caching ndots - DNS konfiguračné úskalia
- Gossip Ghost Nodes IP Reuse - Podobné problémy so zastaranými IP
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.
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.
PMTU Blackholes: Keď Iba Veľké Odpovede Visia
Malé API odpovede fungujú, veľké visia navždy. Príčina: ICMP 'Fragmentation Needed' správy filtrované firewallmi, rozbíjajú Path MTU Discovery v overlay sieťach.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.