Späť na blog

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:

  1. Vynúť pure Go resolver s GODEBUG=netdns=go
  2. Builduj bez cgo keď možné (CGO_ENABLED=0)
  3. Monitoruj počet threadov - mal by byť stabilný, nie rastúci
  4. Cachuj DNS na aplikačnej alebo infraštruktúrnej úrovni

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. "Go cgo DNS Resolution Thread Explózia: Keď net.LookupHost Spawne Tisíce Threadov". https://www.michal-drozd.com/sk/blog/go-cgo-dns-thread-explosion/ (Publikované 25. februára 2025).