Articles

Debugging war story: The mystery of nxdomain

A következő blogbejegyzés egy hibakeresési kalandot ír le a Cloudflare Mezos-alapú klaszterén. Ezt a belső fürtöt elsősorban a naplófájl-információk feldolgozására használják, hogy a Cloudflare ügyfelei analitikával rendelkezzenek, valamint a támadásokra reagáló rendszereink számára.

a felmerült probléma nem volt hatással ügyfeleinkre, de a mérnökök vakargatták a fejüket…

A probléma

az egyik Klaszterünk egy pontján ilyen hibákat kezdtünk látni (egy nxdomain egy meglévő domainhez a belső DNS-en):

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

Ez nagyon furcsának tűnt, mivel a domain valóban létezett. Ez volt az egyik belső domainünk! A mérnökök megemlítették, hogy látták ezt a viselkedést, ezért úgy döntöttünk, hogy mélyebben megvizsgáljuk. A hibát kiváltó lekérdezések változatosak voltak, a Mezos-dns által kezelt dinamikus SRV rekordoktól a fürt belsejéből felnézett külső tartományokig terjedtek.

első naiv kísérletünk a következő ciklus futtatása volt:

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

ennek egy ideig egy szerveren történő futtatása nem reprodukálta a problémát: az összes keresés sikeres volt. Aztán fogtuk a szolgáltatás naplók egy napra, és nem egy grep a” nincs ilyen host ” és hasonló üzeneteket. A hibák szórványosan történtek. Órák teltek el a hibák között, és nem volt nyilvánvaló minta, amely bármilyen következtetésre vezetne minket. Vizsgálatunk elvetette annak lehetőségét, hogy a hiba a Go-ban rejlik, amelyet sok szolgáltatásunkhoz használunk, mivel a hibák a Java szolgáltatásokból is származnak.

A nyúl lyukba

A klaszter DNS-feloldónk számára egyetlen IP-n keresztül futtattunk kötetlen futtatást. A BGP ezután felelős a belső útvonalak bejelentéséért a gépektől az útválasztóig. Úgy döntöttünk, hogy megpróbálunk mintát találni, sok kérést küldve különböző gépekről és rögzítési hibákat. Itt van, amit a terhelés Tesztelési program nézett ki, mint az első:

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

futtatjuk net.LookupHost egy hurok kis szünetek és naplózási hibák; ennyi. Ezt egy dokkoló konténerbe csomagolni és Maratonon futni nyilvánvaló választás volt számunkra, mivel egyébként is így működtetünk más szolgáltatásokat. A naplókat Kafkába, majd Kibanába szállítják, ahol elemezhetjük őket. Ha ezt a programot 65 gépen futtatja, 50 ms-enként kereséseket végez, a következő hibaeloszlást mutatja (magasról alacsonyra) a gazdagépek között:

nem láttunk szoros összefüggést a rackekkel vagy bizonyos gépekkel. A hibák sok gépen történtek, de nem mindegyiken, és különböző időpontokban a windows hibák különböző gépeken fordulnak elő. Az X tengelyen töltött idő és az Y tengelyen lévő hibák száma a következőket mutatta:

hogy lássuk, hogy egy adott DNS-rekurzor megőrült-e, leállítottuk az összes terhelésgenerátort a szokásos gépeken, és elindítottuk a terhelésgeneráló eszközt magukon a rekurzorokon. Néhány órán belül nem volt hiba, ami azt sugallta, hogy a kötetlen teljesen egészséges.

gyanítani kezdtük, hogy a csomagvesztés volt a probléma, de miért fordul elő “ilyen gazdagép”? Ez csak akkor történhet meg, ha EGY NXDOMAIN hiba van egy DNS-válaszban, de elméletünk szerint a válaszok egyáltalán nem jöttek vissza.

A hiányzó

annak a hipotézisnek a teszteléséhez, hogy a csomagok elvesztése “nincs ilyen gazdagép” hibához vezethet, először megpróbáltuk blokkolni a kimenő forgalmat az 53-as porton:

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

ebben az esetben a dig és hasonló eszközök csak időtúllépést jelentenek, de nem adnak vissza “nincs ilyen gazdagép”:

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

Go egy kicsit okosabb, és többet mond arról, hogy mi folyik itt, de nem tér vissza “nincs ilyen host” sem:

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

mivel a Linux kernel azt mondja a feladónak, hogy eldobta a csomagokat, a névszerverre kellett mutatnunk egy fekete lyukat a hálózatban, amely semmit sem tesz a csomagokkal, hogy utánozza a csomagvesztést. Még mindig nincs szerencsénk:

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

a hálózat hibáztatásához valahogy támogatnunk kellett feltételezéseinket, ezért időzítési információkat adtunk hozzá a kereséseinkhez:

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

hogy őszinte legyek, időzítési hibákkal kezdtük, majd később hozzáadtuk a siker időzítését. Hibák történtek 10-es évek után, viszonylag sok sikeres válasz érkezett 5-ös után. Úgy néz ki, mint a csomagvesztés, de még mindig nem mondja el nekünk, hogy miért “nincs ilyen gazdagép”.

mivel most olyan helyen voltunk, amikor tudtuk, hogy mely gazdagépeket érinti ez nagyobb valószínűséggel, a következő két parancsot futtattuk párhuzamosan két screen munkamenetben:

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

a lényeg az volt, hogy egy hálózati dump-ot kapjunk sikertelen megoldásokkal. Ott a következő lekérdezéseket láttuk:

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

két lekérdezés válasz nélkül időtúllépés, de a harmadik szerencsés és sikeres lesz. Természetesen nincs cloudflare.com a belső domainünkben, így a kötetlen jogosan ad választ az NXDOMAIN-nek, 10s a Keresés megkezdése után.

Bingo

nézzük meg az/etc / resolv fájlt.conf, hogy jobban megértsük:

nameserver 10.1.0.9search in.our.internal.domain

a keresési kulcsszó használatával rövid hostneveket használhatunk az FQDN helyett, így a myhost átláthatóan egyenértékű a myhost.in.our.internal.domain.

a DNS-feloldó esetében ez a következőket jelenti: bármely DNS-lekérdezés esetén kérdezze meg a 10.1.0.9 névkiszolgálót, ha ez nem sikerül, fűzze hozzá a.in.our.internal.domain lekérdezést, majd próbálja meg újra. Nem számít, milyen hiba történik az eredeti DNS-lekérdezésnél. Általában NXDOMAIN, de a mi esetünkben ez egy olvasási időtúllépés a csomagvesztés miatt.

úgy tűnt, hogy a következő eseményeknek kell bekövetkezniük a” no such host ” hiba megjelenéséhez:

  1. az eredeti DNS-kérésnek el kell vesznie
  2. az 5 másodperc után elküldött újrapróbálkozásnak el kell vesznie
  3. a belső tartomány következő lekérdezésének (amelyet a search opció okoz) sikerülnie kell, és vissza kell adnia az NXDOMAIN-t

másrészt az nxdomain helyett egy időzített DNS-lekérdezés megfigyeléséhez el kell veszítenie az 5 másodperc után küldött négy csomagot másodpercek egymás után (2 az eredeti lekérdezéshez és 2 a tartomány belső verziójához), ami sokkal kisebb valószínűség. Valójában csak egyszer láttunk egy NXDOMAIN-t 15 év után, és soha nem láttunk hibát 20 év után.

annak igazolására, hogy a feltételezés, építettünk egy proof-of-concept DNS szerver, amely csepp minden kérést cloudflare.com, de küld egy nxdomain-t a meglévő domainekhez:

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

végül megtaláltuk, mi folyik itt, és sikerült megbízhatóan lemásolni ezt a viselkedést.

megoldások

gondoljunk arra, hogyan javíthatjuk ügyfelünket ezen átmeneti hálózati problémák jobb kezelésében, rugalmasabbá téve azt. A resolv man oldala.a conf azt mondja, hogy két gombod van: a timeout és a retries opciók. Az alapértelmezett értékek 5, illetve 2.

hacsak nem tartja nagyon elfoglalva a DNS-kiszolgálót, nagyon valószínűtlen, hogy 1 másodpercnél tovább tartana a válaszadás. Valójában, ha van egy hálózati eszköz a Holdon, akkor számíthat arra, hogy 3 másodpercen belül válaszol. Ha a névszerver a következő rackben él, és nagy sebességű hálózaton keresztül érhető el, akkor nyugodtan feltételezhetjük, hogy ha 1 másodperc után nem érkezik válasz, a DNS-kiszolgáló nem kapta meg a lekérdezést. Ha azt szeretné, hogy kevésbé furcsa” nincs ilyen domain ” hibák, amelyek megkarcolják a fejét, akkor is növelheti az újrapróbálkozásokat. Minél többször próbálkozik újra átmeneti csomagvesztéssel, annál kisebb a kudarc esélye. Minél gyakrabban próbálkozik újra, annál nagyobb az esélye a gyorsabb befejezésnek.

képzelje el, hogy valóban véletlenszerű 1% – os csomagvesztesége van.

  • 2 újrapróbálkozás, 5S timeout: max 10S várakozás a hiba előtt, 0.001% hiba esélye
  • 5 újrapróbálkozás, 1s timeout: max 5s várakozás a hiba előtt, 0.000001% hiba esélye

a való életben az eloszlás más lenne, mivel a csomagvesztés nem véletlenszerű, de számíthat arra, hogy sokkal kevesebbet vár, amíg a DNS válaszol az ilyen típusú változásokra.

mint tudod, sok olyan rendszerkönyvtár, amely DNS-felbontást biztosít, mint például a glibc, az nscd, a systemd-resolved, nem edzett az interneten vagy a csomagveszteséggel rendelkező környezetben. Számos alkalommal szembesültünk azzal a kihívással, hogy megbízható és gyors DNS-felbontási környezetet hozzunk létre, ahogy nőttünk, csak később rájöttünk, hogy a megoldás nem tökéletes.

át neked

tekintettel arra, amit ebben a cikkben olvastál a csomagvesztésről és a split-DNS/private-namespace-ről, hogyan terveznél gyors és megbízható felbontást? Milyen szoftvert használnál és miért? Milyen hangolás változik a szokásos konfigurációhoz képest?