Articles

Debugging War Story: das Geheimnis von NXDOMAIN

Der folgende Blogbeitrag beschreibt ein Debugging-Abenteuer auf dem Mesos-basierten Cluster von Cloudflare. Dieser interne Cluster wird hauptsächlich zur Verarbeitung von Protokolldateiinformationen verwendet, damit Cloudflare-Kunden über Analysen verfügen, und für unsere Systeme, die Angriffe erkennen und darauf reagieren.

Das aufgetretene Problem hatte keine Auswirkungen auf unsere Kunden, aber die Ingenieure kratzten sich am Kopf…

Das Problem

Irgendwann in einem unserer Cluster sahen wir Fehler wie diesen (eine NXDOMAIN für eine vorhandene Domain in unserem internen DNS):

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

Das schien sehr seltsam, da die Domain tatsächlich existierte. Es war eine unserer internen Domains! Ingenieure hatten erwähnt, dass sie dieses Verhalten gesehen hatten, also beschlossen wir, tiefer zu untersuchen. Die Abfragen, die diesen Fehler auslösten, waren unterschiedlich und reichten von dynamischen SRV-Einträgen, die von mesos-dns verwaltet wurden, bis hin zu externen Domänen, die innerhalb des Clusters nachgeschlagen wurden.

Unser erster naiver Versuch bestand darin, Folgendes in einer Schleife auszuführen:

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

Wenn Sie dies eine Weile auf einem Server ausführen, wird das Problem nicht reproduziert: Alle Lookups waren erfolgreich. Dann nahmen wir unsere Serviceprotokolle für einen Tag und machten einen Grep für „no such host“ und ähnliche Nachrichten. Fehler traten sporadisch auf. Es gab Stunden zwischen Fehlern und kein offensichtliches Muster, das uns zu einer Schlussfolgerung führen könnte. Unsere Untersuchung verwarf die Möglichkeit, dass der Fehler in Go lag, das wir für viele unserer Dienste verwenden, da Fehler auch von Java-Diensten stammten.

Into the rabbit hole

Wir haben für unseren Cluster-DNS-Resolver auf einer einzelnen IP auf einigen Computern ungebunden ausgeführt. BGP ist dann für die Ankündigung interner Routen von den Maschinen zum Router verantwortlich. Wir haben uns entschlossen, ein Muster zu finden, indem wir viele Anfragen von verschiedenen Maschinen gesendet und Fehler aufgezeichnet haben. So sah unser Lasttestprogramm zuerst aus:

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

Wir führen net.LookupHost in einer Schleife mit kleinen Pausen und Protokollfehlern aus; das war’s. Dies in einen Docker-Container zu packen und darauf zu laufen, war für uns eine offensichtliche Wahl, da wir auf diese Weise ohnehin andere Dienste ausführen. Protokolle werden an Kafka und dann an Kibana gesendet, wo wir sie analysieren können. Wenn Sie dieses Programm auf 65 Computern ausführen, die alle 50 ms nachschlagen, wird die folgende Fehlerverteilung (von hoch nach niedrig) auf Hosts angezeigt:

Wir haben keine starke Korrelation zu Racks oder bestimmten Computern festgestellt. Fehler sind auf vielen Hosts aufgetreten, aber nicht auf allen, und in verschiedenen Zeitfenstern treten Fehler auf verschiedenen Computern auf. Die Zeit auf der X-Achse und die Anzahl der Fehler auf der Y-Achse zeigten Folgendes:

Um zu sehen, ob ein bestimmter DNS-Recursor verrückt geworden war, stoppten wir alle Lastgeneratoren auf regulären Maschinen und starteten das Lastgenerierungstool auf den Recursors selbst. Innerhalb weniger Stunden gab es keine Fehler, was darauf hindeutete, dass Unbound vollkommen gesund war.

Wir begannen zu vermuten, dass Paketverlust das Problem war, aber warum sollte „kein solcher Host“ auftreten? Es sollte nur passieren, wenn ein NXDOMAIN-Fehler in einer DNS-Antwort enthalten ist, aber unsere Theorie war, dass Antworten überhaupt nicht zurückkamen.

Das Fehlende

Um die Hypothese zu testen, dass der Verlust von Paketen zu einem „no such host“ -Fehler führen kann, haben wir zuerst versucht, den ausgehenden Datenverkehr auf Port 53 zu blockieren:

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

In diesem Fall ist dig und ähnliche Tools nur eine Zeitüberschreitung, geben aber nicht „no such host“ zurück:

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

it schlauer und erzählt Ihnen mehr darüber, was los ist, gibt aber auch nicht „no such host“ zurück:

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

Da der Linux-Kernel dem Absender mitteilt, dass er Pakete fallen gelassen hat, mussten wir den Nameserver auf ein schwarzes Loch im Netzwerk verweisen, das nichts mit Paketen zu tun hat, um Paketverlust nachzuahmen. Immer noch kein Glück:

Um dem Netzwerk weiterhin die Schuld zu geben, mussten wir unsere Annahmen irgendwie stützen, also fügten wir unseren Lookups Timing-Informationen hinzu:

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

Um ehrlich zu sein, begannen wir mit Timing-Fehlern und fügten später das Erfolgstiming hinzu. Fehler traten nach 10s auf, vergleichsweise viele erfolgreiche Antworten kamen nach 5s. Es sieht nach Paketverlust aus, sagt uns aber immer noch nicht, warum „kein solcher Host“ passiert.

Da wir jetzt an einem Ort waren, an dem wir wussten, welche Hosts eher davon betroffen waren, führten wir die folgenden zwei Befehle parallel in zwei screen Sitzungen aus:

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

Es ging darum, einen Netzwerkdump mit fehlgeschlagenen Resolves zu erhalten. Dort sahen wir die folgenden Abfragen:

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

Zwei Abfragen enden ohne Antwort, aber die dritte hat Glück und ist erfolgreich. Natürlich haben wir Cloudflare nicht.com in unserer internen Domäne, so ungebunden gibt zu Recht NXDOMAIN als Antwort, 10s nachdem die Suche initiiert wurde.

Bingo

Schauen wir uns /etc/resolv an.conf um mehr zu verstehen:

nameserver 10.1.0.9search in.our.internal.domain

Mit dem Suchbegriff können wir kurze Hostnamen anstelle von FQDN verwenden, wodurch myhost transparent äquivalent zu myhost.in.our.internal.domain .

Für den DNS-Resolver bedeutet dies Folgendes: Für jede DNS-Abfrage fragen Sie den Nameserver 10.1.0.9, wenn dies fehlschlägt, hängen Sie .in.our.internal.domain an die Abfrage an und versuchen Sie es erneut. Es spielt keine Rolle, welcher Fehler bei der ursprünglichen DNS-Abfrage auftritt. Normalerweise ist es NXDOMAIN , aber in unserem Fall ist es ein Lese-Timeout aufgrund von Paketverlust.

Die folgenden Ereignisse schienen auftreten zu müssen, damit ein Fehler „Kein solcher Host“ angezeigt wird:

  1. Die ursprüngliche DNS-Anfrage muss verloren gehen
  2. Die Wiederholung, die nach 5 Sekunden gesendet wird, muss verloren gehen
  3. Die nachfolgende Abfrage für die interne Domäne (verursacht durch die Option search) muss erfolgreich sein und NXDOMAIN zurückgeben

Um andererseits eine zeitlich begrenzte DNS-Abfrage anstelle von NXDOMAIN zu beobachten, müssen Sie vier Pakete verlieren, die 5 sekunden nacheinander (2 für die ursprüngliche Abfrage und 2 für die interne Version Ihrer Domain), was eine viel geringere Wahrscheinlichkeit darstellt. Tatsächlich haben wir eine NXDOMAIN nach 15 Sekunden nur einmal gesehen und nach 20 Sekunden nie einen Fehler.

Um diese Annahme zu bestätigen, haben wir einen Proof-of-Concept-DNS-Server erstellt, der alle Anforderungen cloudflare.com , aber sendet eine NXDOMAIN für vorhandene Domains:

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

Schließlich fanden wir heraus, was los war, und hatten eine Möglichkeit, dieses Verhalten zuverlässig zu replizieren.

Lösungen

Lassen Sie uns darüber nachdenken, wie wir unseren Client verbessern können, um diese vorübergehenden Netzwerkprobleme besser zu bewältigen und ihn widerstandsfähiger zu machen. Die Manpage für resolv.conf sagt Ihnen, dass Sie zwei Knöpfe haben: die timeout und retries Optionen. Die Standardwerte sind 5 bzw. 2.

Wenn Sie Ihren DNS-Server nicht sehr beschäftigt halten, ist es sehr unwahrscheinlich, dass die Antwort länger als 1 Sekunde dauert. In der Tat, wenn Sie ein Netzwerkgerät auf dem Mond haben, können Sie erwarten, dass es in 3 Sekunden antwortet. Wenn sich Ihr Nameserver im nächsten Rack befindet und über ein Hochgeschwindigkeitsnetzwerk erreichbar ist, können Sie davon ausgehen, dass Ihr DNS-Server Ihre Anfrage nicht erhalten hat, wenn nach 1 Sekunde keine Antwort erfolgt. Wenn Sie weniger seltsame „no such domain“ -Fehler haben möchten, bei denen Sie sich am Kopf kratzen, können Sie auch die Wiederholungsversuche erhöhen. Je öfter Sie es mit einem vorübergehenden Paketverlust wiederholen, desto geringer ist die Wahrscheinlichkeit eines Fehlers. Je öfter Sie es erneut versuchen, desto höher sind die Chancen, schneller fertig zu werden.

Stellen Sie sich vor, Sie haben einen wirklich zufälligen Paketverlust von 1%.

  • 2 wiederholungen, 5s timeout: max 10s warten vor fehler, 0.001% chance von ausfall
  • 5 wiederholungen, 1s timeout: max 5s warten vor fehler, 0.000001% chance von ausfall

In echt leben, die verteilung wäre anders aufgrund der tatsache, dass paket verlust ist nicht zufällig, aber sie können erwarten zu warten viel weniger für DNS zu antworten mit dieser art von ändern.

Wie Sie wissen, sind viele Systembibliotheken, die DNS-Auflösung bereitstellen, wie glibc, nscd, systemd-resolved, nicht gehärtet, um im Internet oder in einer Umgebung mit Paketverlusten zu sein. Im Laufe unseres Wachstums standen wir mehrmals vor der Herausforderung, eine zuverlässige und schnelle DNS-Auflösungsumgebung zu erstellen, um später festzustellen, dass die Lösung nicht perfekt ist.

Zu Ihnen

Wie würden Sie angesichts dessen, was Sie in diesem Artikel über Paketverlust und Split-DNS / Private-Namespace gelesen haben, ein schnelles und zuverlässiges Auflösungs-Setup entwerfen? Welche Software würden Sie verwenden und warum? Welche Tuning-Änderungen gegenüber der Standardkonfiguration würden Sie verwenden?