Articles

Debugging war story: The mystery Of NXDOMAIN

följande blogginlägg beskriver ett felsökningsäventyr på Cloudflares Mesos-baserade kluster. Detta interna kluster används främst för att bearbeta loggfilinformation så att Cloudflare-kunder har analyser och för våra system som upptäcker och svarar på attacker.

problemet som uppstod hade ingen effekt på våra kunder, men fick ingenjörer att skrapa huvudet…

problemet

vid någon tidpunkt i ett av våra kluster började vi se fel som detta (en NXDOMÄN för en befintlig domän på vår interna DNS):

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

detta verkade väldigt konstigt, eftersom domänen verkligen fanns. Det var en av våra interna domäner! Ingenjörer hade nämnt att de hade sett detta beteende, så vi bestämde oss för att undersöka djupare. Frågor som utlöste detta fel varierade och varierade från dynamiska SRV-poster som hanterades av mesos-dns till externa domäner som tittade upp inifrån klustret.

vårt första naiva försök var att köra följande i en slinga:

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

att köra detta ett tag på en server reproducerade inte problemet: alla sökningar lyckades. Sedan tog vi våra serviceloggar för en dag och gjorde en grep för ”ingen sådan värd” och liknande meddelanden. Fel inträffade sporadiskt. Det fanns timmar mellan fel och inget uppenbart mönster som kunde leda oss till någon slutsats. Vår undersökning kasserade möjligheten att felet låg i Go, som vi använder för många av våra tjänster, eftersom fel också kom från Java-tjänster.

in i kaninhålet

vi brukade köra obundet på en enda IP över några maskiner för vår kluster DNS-resolver. BGP ansvarar sedan för att tillkännage interna Rutter från maskinerna till routern. Vi bestämde oss för att försöka hitta ett mönster genom att skicka massor av förfrågningar från olika maskiner och inspelningsfel. Så här såg vårt lasttestprogram först ut:

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

Vi kör net.LookupHost I en slinga med små pauser och loggfel; det är det. Att paketera detta i en Docker-behållare och springa på Maraton var ett självklart val för oss, eftersom det är så vi kör andra tjänster ändå. Loggar skickas till Kafka och sedan till Kibana, där vi kan analysera dem. Att köra detta program på 65 maskiner som gör sökningar var 50 ms visar följande felfördelning (från hög till låg) över värdar:

vi såg ingen stark korrelation till rack eller specifika maskiner. Fel hände på många värdar, men inte på dem alla och i olika tider händer windows-fel på olika maskiner. Att sätta tid på X-axeln och antalet fel på Y-axeln visade följande:

för att se om någon viss DNS-återkommande hade blivit galen, stoppade vi alla lastgeneratorer på vanliga maskiner och startade lastgenereringsverktyget på själva återkommande. Det fanns inga fel på några timmar, vilket antydde att obundet var helt friskt.

vi började misstänka att paketförlust var problemet, men varför skulle ”ingen sådan värd” inträffa? Det borde bara hända när ett NXDOMAIN-fel finns i ett DNS-svar, men vår teori var att Svar inte kom tillbaka alls.

den saknade

för att testa hypotesen att förlora paket kan leda till ett ”ingen sådan värd” – fel försökte vi först blockera utgående trafik på port 53:

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

i det här fallet gräver och liknande verktyg bara time out, men returnerar inte ”ingen sådan värd”:

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

Go är lite smartare och berättar mer om vad som händer, men returnerar inte ”ingen sådan värd” heller:

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

eftersom Linux-kärnan berättar avsändaren att den tappade paket, var vi tvungna att peka namnservern på något svart hål i nätverket som inte gör något med paket för att efterlikna paketförlust. Fortfarande ingen tur:

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

för att fortsätta skylla på nätverket var vi tvungna att stödja våra antaganden på något sätt, så vi lade till tidsinformation till våra sökningar:

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

för att vara ärlig började vi med tidsfel och lade till framgångstid senare. Fel inträffade efter 10-talet, relativt många framgångsrika svar kom efter 5-talet. Det ser ut som paketförlust, men berättar fortfarande inte varför ”ingen sådan värd” händer.

eftersom vi nu var på plats när vi visste vilka värdar som var mer benägna att påverkas av detta, körde vi följande två kommandon parallellt i två screen sessioner:

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

poängen var att få en nätverksdump med misslyckade lösningar. Där såg vi följande frågor:

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

två frågor timeout utan något svar, men den tredje blir lycklig och lyckas. Naturligtvis har vi inte cloudflare.com i vår interna domän, så obundet ger med rätta NXDOMAIN som svar, 10s efter att sökningen inleddes.

Bingo

Låt oss titta på/etc / resolv.conf för att förstå mer:

nameserver 10.1.0.9search in.our.internal.domain

med hjälp av sökordet kan vi använda korta värdnamn istället för FQDN, vilket gör myhost transparent motsvarande myhost.in.our.internal.domain.

för DNS-upplösaren betyder det följande: för alla DNS-frågor fråga namnservern 10.1.0.9, om detta misslyckas, Lägg till .in.our.internal.domain till frågan och försök igen. Det spelar ingen roll vilket fel som uppstår för den ursprungliga DNS-frågan. Vanligtvis är det NXDOMAIN, men i vårt fall är det en läsningstid på grund av paketförlust.

följande händelser tycktes behöva inträffa för att ett” no such host ” – fel skulle visas:

  1. den ursprungliga DNS-begäran måste gå förlorad
  2. försöket som skickas efter 5 sekunder måste gå förlorat
  3. den efterföljande frågan för den interna domänen (orsakad av search alternativet) måste lyckas och returnera NXDOMAIN

å andra sidan, för att observera en tidsbestämd DNS-fråga istället för NXDOMAIN, måste du förlora fyra paket skickade 5 sekunder efter varandra (2 för den ursprungliga frågan och 2 för den interna versionen av din domän), vilket är en mycket mindre sannolikhet. Faktum är att vi bara såg en NXDOMAIN efter 15-talet en gång och såg aldrig ett fel efter 20-talet.

för att validera det antagandet byggde vi en proof-of-concept DNS-server som släpper alla förfrågningar om cloudflare.com, men skickar en NXDOMÄN för befintliga domäner:

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

slutligen hittade vi vad som hände och hade ett sätt att på ett tillförlitligt sätt replikera det beteendet.

lösningar

Låt oss tänka på hur vi kan förbättra vår klient för att bättre hantera dessa övergående nätverksproblem, vilket gör det mer motståndskraftigt. Mannen sida för resolv.conf säger att du har två rattar: alternativen timeout och retries. Standardvärdena är 5 respektive 2.

Om du inte håller din DNS-server mycket upptagen är det mycket osannolikt att det skulle ta mer än 1 sekund att svara. Faktum är att om du råkar ha en nätverksenhet på månen kan du förvänta dig att den svarar på 3 sekunder. Om din namnserver bor i nästa rack och kan nås via ett höghastighetsnätverk kan du säkert anta att om det inte finns något svar efter 1 sekund fick din DNS-server inte din fråga. Om du vill ha mindre konstiga ”inga sådana domän” – fel som får dig att skrapa huvudet, kan du lika gärna öka försöken. Ju fler gånger du försöker igen med övergående paketförlust, desto mindre chans att misslyckas. Ju oftare du försöker igen, desto större chanser att avsluta snabbare.

Föreställ dig att du verkligen har slumpmässig 1% paketförlust.

  • 2 försök, 5S timeout: max 10s vänta före fel, 0.001% chans att misslyckas
  • 5 försök, 1s timeout: max 5s vänta före fel, 0.000001% chans att misslyckas

i verkligheten skulle distributionen vara annorlunda på grund av att paketförlusten inte är slumpmässig, men du kan förvänta dig att vänta mycket mindre för DNS att svara med denna typ av förändring.som du vet är många systembibliotek som tillhandahåller DNS-upplösning som glibc, nscd, systemd-resolved inte härdade för att hantera att vara på internet eller i en miljö med paketförluster. Vi har mött utmaningen att skapa en pålitlig och snabb DNS-upplösningsmiljö ett antal gånger när vi har vuxit, bara för att senare upptäcka att lösningen inte är perfekt.

över till dig

Med tanke på vad du har läst i den här artikeln om paketförlust och split-DNS/private-namespace, hur skulle du utforma en snabb och pålitlig upplösningsinställning? Vilken programvara skulle du använda och varför? Vilken inställning ändras från standardkonfiguration skulle du använda?