Go cgo DNS Resolution Thread Explózia: Keď net.LookupHost Spawne Tisíce Threadov
Ked som prvykrat videl tisice DNS threadov, myslel som si, ze Go je pokazene. “Naša Go služba má zrazu 10,000 OS threadov a je OOMKilled.” Príčina: DNS lookupy používajúce cgo resolver spawnujú reálne OS thready ktoré blokujú čakajúc na pomalé DNS odpovede, obchádzajúc Go’s efektívny goroutine scheduling.
Prostredie: Go 1.21, Kubernetes s CoreDNS, vysoko-konkurenčný HTTP klient, firemná sieť s pomalým DNS
Problém
Thread Explózia Timeline
Normálna prevádzka:
- 50 goroutines robiacich HTTP requesty
- 10 OS threadov (GOMAXPROCS + runtime thready)
- Pamäť: 100MB
Počas DNS spomalenia:
- 5000 goroutines robiacich HTTP requesty
- 5000+ OS threadov (!!)
- Pamäť: 2GB+ a rastie
- Nakoniec: OOMKilled
# Skontroluj počet threadov:
cat /proc/$(pgrep myapp)/status | grep Threads
# Threads: 5234 <- Príliš veľa!
Nevine Vyzerajúci Kód
// Jednoduchý HTTP klient - čo by mohlo zlyhať?
func fetchURL(url string) (*Response, error) {
// Toto robí DNS resolution interne
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// ...
}
// Vysoká konkurencia
for i := 0; i < 5000; i++ {
go fetchURL("https://api.example.com/data")
}
// Ak je DNS pomalé, každý z týchto môže spawnnúť OS thread!
Príčina
Go Má Dva DNS Resolvery
// Go môže resolvovať DNS dvoma spôsobmi:
// 1. Pure Go resolver (default na väčšine Linuxov)
// - Používa goroutines
// - Non-blocking
// - Rešpektuje GOMAXPROCS
// - Číta /etc/resolv.conf priamo
// 2. cgo resolver (používa systémový libc)
// - Spawnuje reálne OS thready
// - BLOKUJE thread kým DNS neodpovie
// - Môže spawnnúť neobmedzene threadov
// - Používa getaddrinfo()
// Skontroluj ktorý resolver sa používa:
// Nastav GODEBUG=netdns=2 pre logy
// "go package net: using cgo DNS resolver"
// "go package net: using Go DNS resolver"
Kedy Go Používa cgo Resolver?
// Go používa cgo resolver keď AKÉKOĽVEK z týchto je pravda:
// 1. CGO_ENABLED=1 a:
// - /etc/nsswitch.conf má komplexnú konfiguráciu
// - /etc/resolv.conf má nepodporované options
// - Systém používa mDNS alebo custom NSS moduly
// 2. Vynúť cgo s:
// export GODEBUG=netdns=cgo
// Bežné triggery:
// /etc/nsswitch.conf:
hosts: files mdns4_minimal [NOTFOUND=return] dns // mdns triggeruje cgo!
// /etc/resolv.conf:
options rotate // Niektoré options triggerujú cgo
Mechanizmus Thread Explózie
Pure Go resolver (bezpečný):
┌──────────────────────────────────────────────────┐
│ Goroutine 1 ──► DNS query ──► epoll wait │
│ Goroutine 2 ──► DNS query ──► epoll wait │
│ Goroutine 3 ──► DNS query ──► epoll wait │
│ │
│ Všetky zdieľajú rovnaké OS thready cez netpoller│
│ 5000 goroutines = stále ~10 OS threadov │
└──────────────────────────────────────────────────┘
cgo resolver (nebezpečný):
┌──────────────────────────────────────────────────┐
│ Goroutine 1 ──► cgo ──► getaddrinfo() ──► BLOCK │
│ (potrebuje vlastný OS │
│ thread!) │
│ Goroutine 2 ──► cgo ──► getaddrinfo() ──► BLOCK │
│ ... │
│ Goroutine 5000 ──► cgo ──► getaddrinfo() ──► ... │
│ │
│ 5000 goroutines = 5000+ OS threadov! │
│ Každý thread = ~8KB stack minimum │
│ 5000 × 8KB = 40MB len pre stacky │
└──────────────────────────────────────────────────┘
Diagnostika
Skontroluj Ktorý Resolver Sa Používa
# Spusti s debug logovaním
GODEBUG=netdns=2 ./myapp 2>&1 | head -20
# Hľadaj:
# "go package net: using Go's DNS resolver" <- Dobré
# "go package net: using cgo DNS resolver" <- Nebezpečenstvo!
# Skontroluj prečo bolo vybrané cgo:
GODEBUG=netdns=2 ./myapp 2>&1 | grep -i "cgo\|dns"
Monitoruj Počet Threadov
# Sleduj počet threadov v reálnom čase
watch -n 1 'cat /proc/$(pgrep myapp)/status | grep Threads'
# Alebo s pprof
curl http://localhost:6060/debug/pprof/threadcreate?debug=1
# Prometheus metrika
process_threads # Malo by byť stabilné, nie rastúce
Skontroluj DNS Resolution Čas
# Zvnútra containeru/podu
time nslookup api.example.com
# Ak DNS trvá > 1 sekundu, uvidíš thread explóziu
# pri vysokej konkurencii s cgo resolverom
# Skontroluj CoreDNS latenciu
kubectl logs -n kube-system -l k8s-app=kube-dns | grep -i slow
Riešenie
Možnosť 1: Vynúť Pure Go Resolver
# Environment variable (najspoľahlivejšie)
export GODEBUG=netdns=go
# Alebo pri builde (kompiluj bez cgo)
CGO_ENABLED=0 go build -o myapp .
# Kubernetes deployment
env:
- name: GODEBUG
value: "netdns=go"
Možnosť 2: Zjednoduš nsswitch.conf
# Skontroluj aktuálnu konfiguráciu
cat /etc/nsswitch.conf | grep hosts
# Problematické (triggeruje cgo):
hosts: files mdns4_minimal [NOTFOUND=return] dns myhostname
# Jednoduché (umožňuje pure Go):
hosts: files dns
# V containeri kontroluješ base image
# Alpine používa musl libc - iné správanie!
# Debian/Ubuntu so zjednodušeným nsswitch.conf funguje s pure Go
Možnosť 3: DNS Caching a Connection Pooling
// Zníž DNS lookupy správnou HTTP client konfiguráciou
import "net/http"
var client = &http.Client{
Transport: &http.Transport{
// Znovupoužívaj spojenia aby si sa vyhol opakovaným DNS lookupom
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 90 * time.Second,
// Custom dialer s DNS cachingom
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
Resolver: &net.Resolver{
PreferGo: true, // Vynúť pure Go resolver
},
}).DialContext,
},
}
Možnosť 4: Limituj Konkurenčné DNS Lookupy
// Použi semafor na limitovanie konkurenčných DNS operácií
var dnsSem = make(chan struct{}, 100) // Max 100 konkurenčných lookupov
func lookupWithLimit(host string) ([]net.IP, error) {
dnsSem <- struct{}{} // Získaj
defer func() { <-dnsSem }() // Uvoľni
return net.LookupIP(host)
}
Možnosť 5: Použi Externú DNS Cache
# Deploy node-local DNS cache (Kubernetes)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-local-dns
spec:
# ... NodeLocal DNSCache konfigurácia
# Výrazne znižuje DNS latenciu
Monitoring
groups:
- name: go-dns-threads
rules:
- alert: GoThreadExplosion
expr: |
process_threads{job="myapp"} > 500
for: 2m
labels:
severity: critical
annotations:
summary: "Go app má {{ $value }} threadov (očakávané < 100)"
- alert: HighDNSLatency
expr: |
histogram_quantile(0.99,
rate(dns_lookup_duration_seconds_bucket[5m])
) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "DNS lookup p99 > 1 sekunda"
Checklist
## Go cgo DNS Thread Explózia
### Symptómy
- [ ] Počet threadov neobmedzene rastie
- [ ] Spotreba pamäte rastie s počtom threadov
- [ ] OOMKilled napriek nízkemu goroutine heap usage
- [ ] Koreluje s DNS latenciou alebo vysokou konkurenciou
### Diagnostika
- [ ] Skontroluj resolver: GODEBUG=netdns=2
- [ ] Monitoruj počet threadov cez /proc alebo pprof
- [ ] Zmeraj DNS resolution latenciu
- [ ] Skontroluj /etc/nsswitch.conf pre cgo triggery
### Riešenia
- [ ] Nastav GODEBUG=netdns=go
- [ ] Alebo builduj s CGO_ENABLED=0
- [ ] Zjednoduš nsswitch.conf ak možné
- [ ] Implementuj DNS caching
- [ ] Limituj konkurenčné DNS lookupy
- [ ] Deploy node-local DNS cache
Záver
Lekcia: Go’s goroutine model je efektívny, ale cgo volania ho úplne obchádzajú. Keď DNS používa cgo a stane sa pomalým, každý blokovaný DNS lookup spawnuje reálny OS thread - a zrazu tvoja “ľahká” Go app má tisíce threadov.
Kľúčová obrana:
- Vynúť pure Go resolver s
GODEBUG=netdns=go - Builduj bez cgo keď možné (
CGO_ENABLED=0) - Monitoruj počet threadov - mal by byť stabilný, nie rastúci
- Cachuj DNS na aplikačnej alebo infraštruktúrnej úrovni
Súvisiace články
- Go Timer Heap Pressure - Ďalšia Go runtime pasca
- Java Native Memory OOMKilled - Podobné off-heap problémy
Súvisiace články
Java Profilovanie v Hardened Kubernetes: Keď Security Blokuje Tvoj Debugger
Nemôžeš pripojiť profiler k produkčnej JVM. seccomp blokuje perf_event_open, container dropol CAP_SYS_PTRACE a PodSecurityPolicy bráni privileged mode. Tu je ako profilovať aj tak.
Go p99 Latency Špičky: Vnorené context.WithTimeout Timer Búrky
Periodické latency špičky ktoré vyzerajú ako network jitter. Skutočná príčina: vnorené timeouty vytvárajú tisíce timerov ktoré zaťažujú Go runtime timer heap a spúšťajú GC scanning.
CoreDNS vs NodeLocal DNS Cache: Zníženie Kubernetes DNS Latencie 10x
Vaše pody robia 100 DNS queries per request. CoreDNS je bottleneck. Benchmarkujem NodeLocal DNS cache a ukážem konfiguráciu pre produkciu.
etcd Watch Replay Búrky: Keď Obrovské ConfigMapy Zabíjajú Control Plane
Apiserver je 'náhodne pomalý'. Príčina: veľké, často aktualizované ConfigMapy spúšťajú watch compaction, čo spôsobuje simultánny relist tisícov kontrolérov.
Citujte tento článok
Ak na článok odkazujete, pridajte pôvodnú URL a uveďte autora.