Articles

Debugging war story: il mistero di NXDOMAIN

Il seguente post sul blog descrive un’avventura di debug sul cluster basato su Mesos di Cloudflare. Questo cluster interno viene utilizzato principalmente per elaborare le informazioni sui file di log in modo che i clienti Cloudflare dispongano di analisi e per i nostri sistemi in grado di rilevare e rispondere agli attacchi.

Il problema riscontrato non ha avuto alcun effetto sui nostri clienti, ma ha avuto ingegneri grattarsi la testa…

Il problema

Ad un certo punto in uno dei nostri cluster abbiamo iniziato a vedere errori come questo (un NXDOMAIN per un dominio esistente sul nostro DNS interno):

lookup some.existing.internal.host on 10.1.0.9:53: no such host

Questo sembrava molto strano, dal momento che il dominio esisteva davvero. Era uno dei nostri domini interni! Gli ingegneri avevano detto di aver visto questo comportamento, quindi abbiamo deciso di indagare più a fondo. Le query che attivavano questo errore erano varie e variavano da record SRV dinamici gestiti da mesos-dns a domini esterni cercati dall’interno del cluster.

Il nostro primo tentativo ingenuo è stato quello di eseguire quanto segue in un ciclo:

while true; do dig some.existing.internal.host > /tmp/dig.txt || break; done

L’esecuzione di questo per un po ‘ su un server non ha riprodotto il problema: tutte le ricerche hanno avuto successo. Poi abbiamo preso i nostri registri di servizio per un giorno e abbiamo fatto un grep per “no such host” e messaggi simili. Gli errori stavano accadendo sporadicamente. Ci sono state ore tra errori e nessun modello ovvio che potrebbe portarci a qualsiasi conclusione. La nostra indagine ha scartato la possibilità che l’errore si trovasse in Go, che usiamo per molti dei nostri servizi, poiché anche gli errori provenivano dai servizi Java.

Nella tana del coniglio

Abbiamo usato per eseguire Unbound su un singolo IP su alcune macchine per il nostro resolver DNS cluster. BGP è quindi responsabile dell’annuncio dei percorsi interni dalle macchine al router. Abbiamo deciso di provare a trovare un modello inviando molte richieste da macchine diverse e registrando errori. Ecco come era il nostro programma di test di carico all’inizio:

package mainimport ("flag""fmt""net""os""time")func main() {n := flag.String("n", "", "domain to look up")p := flag.Duration("p", time.Millisecond*10, "pause between lookups")flag.Parse()if *n == "" {flag.PrintDefaults()os.Exit(1)}for {_, err := net.LookupHost(*n)if err != nil {fmt.Println("error:", err)}time.Sleep(*p)}}

Eseguiamonet.LookupHost in un ciclo con piccole pause e errori di log; questo è tutto. Imballare questo in un contenitore Docker e correre su Marathon è stata una scelta ovvia per noi, dal momento che è così che gestiamo comunque altri servizi. I registri vengono spediti a Kafka e poi a Kibana, dove possiamo analizzarli. L’esecuzione di questo programma su 65 macchine che eseguono ricerche ogni 50 ms mostra la seguente distribuzione degli errori (da alta a bassa) tra gli host:

Non abbiamo visto una forte correlazione con rack o macchine specifiche. Gli errori si sono verificati su molti host, ma non su tutti e in diverse finestre temporali gli errori si verificano su macchine diverse. Mettere il tempo sull’asse X e il numero di errori sull’asse Y ha mostrato quanto segue:

Per vedere se qualche particolare ricorsore DNS era impazzito, abbiamo fermato tutti i generatori di carico sulle macchine normali e avviato lo strumento di generazione del carico sui recursori stessi. Non ci sono stati errori in poche ore, il che ha suggerito che Unbound era perfettamente sano.

Abbiamo iniziato a sospettare che la perdita di pacchetti fosse il problema, ma perché “nessun host di questo tipo” si verificherebbe? Dovrebbe accadere solo quando un errore NXDOMAIN si trova in una risposta DNS, ma la nostra teoria era che le risposte non tornassero affatto.

Mancante

Per testare l’ipotesi che la perdita di pacchetti può portare a un “host” errore, abbiamo provato per la prima volta il blocco del traffico in uscita sulla porta 53:

sudo iptables -A OUTPUT -p udp --dport 53 -j DROP

In questo caso, scavare e strumenti simili in timeout, ma non tornano “host”:

; <<>> DiG 9.9.5-9+deb8u3-Debian <<>> cloudflare.com;; global options: +cmd;; connection timed out; no servers could be reached

Go è un po ‘ più intelligente e ti dice di più su ciò che sta succedendo, ma non ritorno “host” o:

error: lookup cloudflare.com on 10.1.0.9:53: write udp 10.1.14.20:47442->10.1.0.9:53: write: operation not permitted

Poiché il kernel di Linux dice al mittente che ha rilasciato pacchetti, abbiamo dovuto puntare il nameserver su un buco nero nella rete che non fa nulla con i pacchetti per simulare la perdita di pacchetti. Ancora nessuna fortuna:

error: lookup cloudflare.com on 10.1.2.9:53: read udp 10.1.14.20:39046->10.1.2.9:53: i/o timeout

Per continuare a incolpare la rete abbiamo dovuto supportare le nostre ipotesi in qualche modo, quindi abbiamo aggiunto le informazioni sui tempi alle nostre ricerche:

s := time.Now()_, err := net.LookupHost(*n)e := time.Now().Sub(s).Seconds()if err != nil { log.Printf("error after %.4fs: %s", e, err)} else if e > 1 { log.Printf("success after %.4fs", e)}

Per essere onesti, abbiamo iniziato con errori di temporizzazione e aggiunto i tempi di successo in seguito. Gli errori stavano accadendo dopo 10s, comparativamente molte risposte di successo stavano arrivando dopo 5s. Sembra una perdita di pacchetti, ma ancora non ci dice perché “nessun host del genere” accade.

Poiché ora eravamo in un posto in cui sapevamo quali host erano più probabili essere influenzati da questo, abbiamo eseguito i seguenti due comandi in parallelo in due sessioniscreen:

while true; do dig cloudflare.com > /tmp/dig.log || break; done; date; sudo killall tcpdumpsudo tcpdump -w /state/wtf.cap port 53

Il punto era ottenere un dump di rete con risoluzioni fallite. Lì, abbiamo visto le seguenti query:

00.00s A cloudflare.com05.00s A cloudflare.com10.00s A cloudflare.com.in.our.internal.domain

Due query scadono senza alcuna risposta, ma la terza ottiene fortuna e riesce. Naturalmente, non abbiamo cloudflare.com nel nostro dominio interno, quindi Unbound dà giustamente NXDOMAIN in risposta, 10s dopo l’avvio della ricerca.

Bingo

Diamo un’occhiata a/etc / resolv.conf per capire di più:

nameserver 10.1.0.9search in.our.internal.domain

Utilizzando la parola chiave di ricerca ci permette di utilizzare nomi host brevi invece di FQDN, rendendo myhosttrasparente equivalente a myhost.in.our.internal.domain.

Per il resolver DNS significa quanto segue: per qualsiasi query DNS chiedere al nameserver 10.1.0.9, se questo non riesce, aggiungere .in.our.internal.domain alla query e riprovare. Non importa quale errore si verifica per la query DNS originale. Di solito è NXDOMAIN, ma nel nostro caso è un timeout di lettura a causa della perdita di pacchetti.

I seguenti eventi sembravano dover verificarsi per un errore” no such host ” per apparire:

  1. L’originale della richiesta DNS deve essere perso
  2. La riprova che viene inviato dopo 5 secondi ha perso
  3. La query successiva per il dominio interno (causato dall’ search opzione) ha successo e ritorno NXDOMAIN

d’altra parte, per osservare un timeout di query DNS invece di NXDOMAIN, devi perdere quattro pacchetti inviati 5 secondi l’uno dopo l’altro (2 per la query originale e 2 per la versione interna del dominio), che è molto minore probabilità. In effetti, abbiamo visto solo un NXDOMAIN dopo 15s una volta e non abbiamo mai visto un errore dopo 20s.

Per convalidare tale ipotesi, abbiamo creato un server DNS proof-of-concept che elimina tutte le richieste per cloudflare.com, ma invia un NXDOMAIN per i domini esistenti:

package mainimport ("github.com/miekg/dns""log")func main() {server := &dns.Server{Addr: ":53", Net: "udp"}dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) {m := &dns.Msg{}m.SetReply(r)for _, q := range r.Question {log.Printf("checking %s", q.Name)if q.Name == "cloudflare.com." {log.Printf("ignoring %s", q.Name)// just ignorereturn}}w.WriteMsg(m)})log.Printf("listening..")if err := server.ListenAndServe(); err != nil {log.Fatalf("error listening: %s", err)}}

Infine, abbiamo trovato cosa stava succedendo e abbiamo avuto un modo di replicare in modo affidabile quel comportamento.

Soluzioni

Pensiamo a come possiamo migliorare il nostro cliente per gestire meglio questi problemi di rete transitori, rendendolo più resiliente. La pagina man per resolv.conf ti dice che hai due manopole :le opzionitimeout eretries. I valori predefiniti sono rispettivamente 5 e 2.

A meno che tu non tenga molto occupato il tuo server DNS, è molto improbabile che ci voglia più di 1 secondo per rispondere. Infatti, se vi capita di avere un dispositivo di rete sulla Luna, ci si può aspettare di rispondere in 3 secondi. Se il tuo nameserver vive nel rack successivo ed è raggiungibile su una rete ad alta velocità, puoi tranquillamente supporre che se non c’è risposta dopo 1 secondo, il tuo server DNS non ha ricevuto la tua query. Se vuoi avere meno strani errori di “nessun dominio” che ti fanno grattare la testa, potresti anche aumentare i tentativi. Più volte si riprova con perdita di pacchetti transitoria, meno possibilità di errore. Più spesso riprovi, maggiori sono le possibilità di finire più velocemente.

Immagina di avere una perdita di pacchetti dell ‘ 1% veramente casuale.

  • 2 tentativi, 5s timeout: max 10s attendere prima di errore, 0,001% di possibilità di fallimento
  • 5 tentativi, 1s timeout: max 5s attendere prima di errore, 0.000001% di possibilità di fallimento

Nella vita reale, la distribuzione potrebbe anche essere diverso, dovuto al fatto che la perdita di pacchetti non è casuale, ma si può aspettare molto di meno per il DNS per rispondere con questo tipo di modifica.

Come sapete molte librerie di sistema che forniscono risoluzione DNS come glibc, nscd, systemd-resolved non sono indurite per gestire l’essere su Internet o in un ambiente con perdite di pacchetti. Abbiamo affrontato la sfida di creare un ambiente di risoluzione DNS affidabile e veloce un certo numero di volte come siamo cresciuti, solo per poi scoprire che la soluzione non è perfetta.

Oltre a te

Dato quello che hai letto in questo articolo sulla perdita di pacchetti e split-DNS / private-namespace, come progetteresti una configurazione di risoluzione veloce e affidabile? Quale software useresti e perché? Quali modifiche di messa a punto dalla configurazione standard usereste?