Articles

Debugging war story: misterul NXDOMAIN

următoarea postare pe blog descrie o aventură de depanare pe clusterul bazat pe Mesos al Cloudflare. Acest cluster intern este utilizat în principal pentru a procesa informații despre fișierele jurnal, astfel încât clienții Cloudflare să aibă analize și pentru sistemele noastre care detectează și răspund la atacuri.

problema întâlnită nu a avut nici un efect asupra clienților noștri, dar au avut ingineri zgarieturi capul lor…

problema

la un moment dat într-unul din clusterul nostru am început să vedem erori de genul acesta (un NXDOMAIN pentru un domeniu existent pe DNS-ul nostru intern):

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

Acest lucru părea foarte ciudat, deoarece domeniul exista într-adevăr. A fost unul dintre domeniile noastre interne! Inginerii au menționat că au văzut acest comportament, așa că am decis să investigăm mai profund. Interogările care au declanșat această eroare au fost variate și au variat de la înregistrări SRV dinamice gestionate de mesos-dns la domenii externe privite din interiorul clusterului.

prima noastră încercare naivă a fost de a rula următoarele într-o buclă:

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

rularea acestui lucru pentru o vreme pe un server nu a reprodus problema: toate căutările au avut succes. Apoi ne-am luat jurnalele de servicii pentru o zi și am făcut un grep pentru „nici o astfel de gazdă” și mesaje similare. Erorile s-au întâmplat sporadic. Au existat ore între erori și nici un model evident care ne-ar putea duce la orice concluzie. Investigația noastră a eliminat posibilitatea ca eroarea să se afle în Go, pe care o folosim pentru o mulțime de servicii, deoarece erorile proveneau și din serviciile Java.

în gaura de iepure

obișnuiam să rulăm nelegat pe un singur IP pe câteva mașini pentru rezolvarea DNS a clusterului nostru. BGP este apoi responsabil pentru anunțarea rutelor interne de la mașini la router. Am decis să încercăm să găsim un model trimițând o mulțime de solicitări de la diferite mașini și erori de înregistrare. Iată cum arăta programul nostru de testare a încărcării la început:

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)}}

rulămnet.LookupHost într-o buclă cu pauze mici și erori de jurnal; asta este. Ambalarea acestuia într-un container Docker și alergarea pe maraton a fost o alegere evidentă pentru noi, deoarece oricum conducem alte servicii. Jurnalele sunt trimise la Kafka și apoi la Kibana, unde le putem analiza. Rularea acestui program pe 65 de mașini care fac căutări la fiecare 50ms arată următoarea distribuție a erorilor (de la mare la mic) pe gazde:

nu am văzut nicio corelație puternică cu rafturile sau mașinile specifice. Erori s-au întâmplat pe multe gazde, dar nu pe toate și în diferite erori de timp Windows se întâmplă pe diferite mașini. Punerea timpului pe axa X și numărul de erori pe axa Y au arătat următoarele:

pentru a vedea dacă un anumit recursor DNS a înnebunit, am oprit toate generatoarele de sarcină pe mașinile obișnuite și am pornit instrumentul de generare a sarcinii pe recursorii înșiși. Nu au existat erori în câteva ore, ceea ce a sugerat că nelegat era perfect sănătos.

am început să suspectăm că pierderea pachetelor a fost problema, dar de ce nu ar apărea „o astfel de gazdă”? Ar trebui să se întâmple numai atunci când o eroare NXDOMAIN este într-un răspuns DNS, dar teoria noastră a fost că răspunsurile nu au revenit deloc.

lipsa

pentru a testa ipoteza că pierderea pachetelor poate duce la o eroare „fără o astfel de gazdă”, am încercat mai întâi să blocăm traficul de ieșire pe portul 53:

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

În acest caz, dig și instrumente similare doar time out, dar nu returnați „nici o astfel de gazdă”:

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

Go este un pic mai inteligent și vă spune mai multe despre ceea ce se întâmplă, dar nu se întoarce „nici o astfel de gazdă:

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

deoarece nucleul Linux îi spune expeditorului că a scăpat pachete, a trebuit să îndreptăm serverul de nume către o gaură neagră din rețea care nu face nimic cu pachetele pentru a imita pierderea pachetelor. Încă nu am avut noroc:

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

pentru a continua să dăm vina pe rețea, a trebuit să ne susținem ipotezele într-un fel, așa că am adăugat informații de sincronizare la căutările noastre:

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)}

pentru a fi sincer, am început prin erori de sincronizare și am adăugat calendarul de succes mai târziu. Erori au avut loc după 10s, comparativ multe răspunsuri de succes au venit după 5s. Se pare ca pierderea de pachete, dar încă nu ne spune de ce „nici o astfel de gazdă” se întâmplă.

deoarece acum eram într-un loc în care știam care gazde erau mai susceptibile de a fi afectate de acest lucru, am rulat următoarele două comenzi în paralel în douăscreensesiuni:

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

ideea a fost să obținem o descărcare de rețea cu rezolvări eșuate. Acolo, am văzut următoarele întrebări:

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

două interogări expiră fără niciun răspuns, dar al treilea are noroc și reușește. Desigur, nu avem cloudflare.com în domeniul nostru intern, astfel încât nelegat dă pe bună dreptate NXDOMAIN în răspuns, 10s după căutare a fost inițiată.

Bingo

să ne uităm la/etc / resolv.conf pentru a înțelege mai multe:

nameserver 10.1.0.9search in.our.internal.domain

utilizarea cuvântului cheie de căutare ne permite să folosim nume de gazdă scurte în loc de FQDN, făcândmyhost echivalent transparent cumyhost.in.our.internal.domain.

pentru rezolvarea DNS înseamnă următoarele: pentru orice interogare DNS întrebați serverul de nume 10.1.0.9, dacă acest lucru eșuează, adăugați.in.our.internal.domain la interogare și încercați din nou. Nu contează ce eșec apare pentru interogarea DNS originală. De obicei este NXDOMAIN, dar în cazul nostru este un timeout de citire din cauza pierderii pachetelor.

următoarele evenimente păreau să aibă loc pentru ca o eroare” nu există o astfel de gazdă ” să apară:

  1. cererea DNS originală trebuie pierdută
  2. reîncercarea care este trimisă după 5 secunde trebuie pierdută
  3. interogarea ulterioară pentru domeniul intern (cauzată de opțiunea search) trebuie să reușească și să returneze NXDOMAIN

pe de altă parte, pentru a observa o interogare DNS expirată în loc de NXDOMAIN, trebuie să pierdeți patru pachete trimise 5 secunde unul după altul (2 pentru interogarea originală și 2 pentru versiunea internă a domeniului dvs.), ceea ce reprezintă o probabilitate mult mai mică. De fapt, am văzut doar un NXDOMAIN după 15s o dată și nu am văzut niciodată o eroare după 20s.

pentru a valida această ipoteză, am construit un server DNS proof-of-concept care renunță la toate cererile pentru cloudflare.com, dar trimite un NXDOMAIN pentru domeniile existente:

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)}}

În cele din urmă, am găsit ce se întâmplă și am avut o modalitate de a reproduce în mod fiabil acel comportament.

soluții

să ne gândim cum putem îmbunătăți clientul nostru pentru a gestiona mai bine aceste probleme de rețea tranzitorii, făcându-l mai rezistent. Pagina de manual pentru resolv.conf vă spune că aveți două butoane:timeout șiretries opțiuni. Valorile implicite sunt 5 și respectiv 2.

dacă nu vă păstrați serverul DNS foarte ocupat, este foarte puțin probabil să dureze mai mult de 1 secundă pentru a răspunde. De fapt, dacă se întâmplă să aveți un dispozitiv de rețea pe lună, vă puteți aștepta să răspundă în 3 secunde. Dacă serverul dvs. de nume locuiește în următorul rack și este accesibil printr-o rețea de mare viteză, puteți presupune în siguranță că, dacă nu există răspuns după 1 secundă, serverul DNS nu a primit interogarea. Dacă doriți să aveți erori mai puțin ciudate „fără un astfel de domeniu” care vă fac să vă zgâriați capul, puteți crește și reîncercările. De mai multe ori încercați din nou cu pierderi de pachete tranzitorii, cu atât mai puține șanse de eșec. Cu cât încercați mai des, cu atât sunt mai mari șansele de a termina mai repede.

Imaginați-vă că aveți cu adevărat aleatoare 1% pierdere de pachete.

  • 2 reîncercări, 5S timeout: max 10S așteptați înainte de eroare, 0,001% șanse de eșec
  • 5 reîncercări, 1s timeout: max 5S așteptați înainte de eroare, 0,000001% șanse de eșec

în viața reală, distribuția ar fi diferită datorită faptului că pierderea pachetelor nu este aleatorie, dar vă puteți aștepta să așteptați mult mai puțin pentru ca DNS să răspundă cu acest tip de schimbare.

după cum știți multe biblioteci de sistem care oferă rezoluție DNS cum ar fi glibc, nscd, systemd-resolved nu sunt întărite să se ocupe de a fi pe internet sau într-un mediu cu pierderi de pachete. Ne-am confruntat cu provocarea de a crea un mediu de rezoluție DNS fiabil și rapid de mai multe ori pe măsură ce am crescut, doar pentru a descoperi ulterior că soluția nu este perfectă.

Pe la tine

având în vedere ceea ce ați citit în acest articol despre pierderea de pachete și split-DNS / privat-namespace, cum ai proiecta o configurare rezoluție rapidă și fiabilă? Ce software ați folosi și de ce? Ce modificări de tuning de la configurația standard ați folosi?