Articles

Debugging War story: The mystery Of NXDOMAIN

poniższy wpis na blogu opisuje przygodę z debugowaniem na klastrze Cloudflare opartym na Mesos. Ten wewnętrzny klaster służy przede wszystkim do przetwarzania informacji o plikach dziennika, dzięki czemu klienci Cloudflare mają możliwość analizy, a także do naszych systemów, które wykrywają ataki i reagują na nie.

napotkany problem nie miał żadnego wpływu na naszych klientów, ale inżynierowie drapali się po głowach…

Problem

w pewnym momencie w jednym z naszych klastrów zaczęliśmy widzieć takie błędy (NXDOMENA dla istniejącej domeny w naszym wewnętrznym DNS):

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

wydawało się to bardzo dziwne, ponieważ domena rzeczywiście istniała. To była jedna z naszych wewnętrznych domen! Inżynierowie wspomnieli, że widzieli to zachowanie, więc zdecydowaliśmy się zbadać głębiej. Zapytania wywołujące ten błąd były zróżnicowane i wahały się od dynamicznych rekordów SRV zarządzanych przez mesos-dns do zewnętrznych domen wyszukiwanych z wnętrza klastra.

naszą pierwszą naiwną próbą było uruchomienie następującej czynności w pętli:

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

uruchomienie tego przez jakiś czas na jednym serwerze nie powtórzyło problemu: wszystkie wyszukiwania zakończyły się sukcesem. Następnie wzięliśmy nasze dzienniki usług na dzień i zrobiliśmy grep dla” nie ma takiego hosta ” i podobnych wiadomości. Błędy zdarzały się sporadycznie. Były godziny między błędami i brak oczywistego wzorca, który mógłby doprowadzić nas do jakichkolwiek wniosków. Nasze dochodzenie odrzuciło możliwość, że błąd leżał w Go, którego używamy dla wielu naszych usług, ponieważ błędy również pochodziły z usług Java.

do króliczej nory

uruchamialiśmy Unbound na jednym IP na kilku maszynach dla naszego Cluster DNS resolver. BGP jest następnie odpowiedzialny za ogłaszanie wewnętrznych tras z maszyn do routera. Postanowiliśmy spróbować znaleźć wzór, wysyłając wiele zapytań z różnych maszyn i rejestrując błędy. Oto jak na początku wyglądał nasz program do testowania obciążenia:

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

uruchamiamynet.LookupHost w pętli z małymi przerwami i błędami logowania; to wszystko. Pakowanie tego do kontenera Dockera i bieganie na maratonie było dla nas oczywistym wyborem, ponieważ w ten sposób i tak prowadzimy inne usługi. Logi są wysyłane do Kafki, a następnie do Kibany, gdzie możemy je przeanalizować. Uruchomienie tego programu na 65 maszynach robiąc Wyszukiwanie co 50ms pokazuje następujący rozkład błędów (od wysokiego do niskiego) pomiędzy hostami:

nie widzieliśmy silnej korelacji z szafami lub konkretnymi maszynami. Błędy wystąpiły na wielu hostach, ale nie na wszystkich i w różnych oknach czasowych błędy zdarzają się na różnych maszynach. Umieszczenie czasu na osi X i liczby błędów na osi Y pokazało się następująco:

aby sprawdzić, czy jakiś konkretny recursor DNS zwariował, zatrzymaliśmy wszystkie generatory obciążenia na zwykłych maszynach i uruchomiliśmy narzędzie do generowania obciążenia na samych recursorach. W ciągu kilku godzin nie było błędów, co sugerowało, że Unbound jest całkowicie zdrowy.

zaczęliśmy podejrzewać, że problemem była utrata pakietów, ale dlaczego” nie ma takiego hosta”? Powinno się to zdarzyć tylko wtedy, gdy błąd NXDOMAIN jest w odpowiedzi DNS, ale nasza teoria była taka, że odpowiedzi w ogóle nie wróciły.

brakujący

aby przetestować hipotezę, że utrata pakietów może prowadzić do błędu „brak takiego hosta”, najpierw próbowaliśmy zablokować ruch wychodzący na porcie 53:

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

w tym przypadku narzędzia dig i podobne po prostu przestają działać, ale nie zwracają „brak takiego hosta”:

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

go jest nieco mądrzejszy i mówi więcej o tym, co się dzieje, ale nie zwraca „no such host” :

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

ponieważ jądro Linuksa mówi nadawcy, że upuściło Pakiety, musieliśmy skierować serwer nazw do jakiejś czarnej dziury w sieci, która nie robi nic z pakietami, aby naśladować utratę pakietów. Nadal bez powodzenia:

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

aby kontynuować obwinianie sieci, musieliśmy jakoś wspierać nasze założenia, więc dodaliśmy informacje o czasie do naszych wyszukiwań:

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

szczerze mówiąc, zaczęliśmy od błędów pomiaru czasu, a później dodaliśmy timing sukcesu. Błędy pojawiały się po 10s, stosunkowo wiele udanych odpowiedzi nadeszło po 5s. Wygląda to na utratę pakietów, ale nadal nie mówi nam, dlaczego” nie ma takiego hosta”.

ponieważ teraz byliśmy w miejscu, w którym wiedzieliśmy, które hosty są bardziej narażone na to, uruchomiliśmy następujące dwie komendy równolegle w dwóch sesjachscreen:

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

celem było uzyskanie zrzutu sieci z nieudanymi rozwiązaniami. Tam zobaczyliśmy następujące zapytania:

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

dwa zapytania kończą się bez żadnej odpowiedzi, ale trzecie ma szczęście i się udaje. Oczywiście nie mamy cloudflare.com w naszej wewnętrznej domenie, więc Unbound słusznie daje NXDOMAIN w odpowiedzi, 10s po zainicjowaniu wyszukiwania.

Bingo

spójrzmy na/etc / resolv.conf, aby zrozumieć więcej:

nameserver 10.1.0.9search in.our.internal.domain

użycie słowa kluczowego wyszukiwania pozwala nam używać krótkich nazw hostów zamiast FQDN, dzięki czemumyhostjest przezroczystym odpowiednikiemmyhost.in.our.internal.domain.

dla resolvera DNS oznacza to, co następuje: dla dowolnego zapytania DNS zapytaj serwer nazw 10.1.0.9, jeśli to się nie powiedzie, Dołącz .in.our.internal.domain do zapytania i Ponów próbę. Nie ma znaczenia, jaka awaria wystąpi dla oryginalnego zapytania DNS. Zwykle jest to NXDOMAIN, ale w naszym przypadku jest to limit czasu odczytu z powodu utraty pakietów.

wydaje się, że następujące zdarzenia muszą wystąpić, aby pojawił się błąd „no such host”:

  1. pierwotne żądanie DNS musi zostać utracone
  2. ponowna próba wysłana po 5 sekundach musi zostać utracona
  3. kolejne zapytanie o domenę wewnętrzną (spowodowane opcją search) musi zakończyć się sukcesem i zwrócić NXDOMAIN

z drugiej strony, aby obserwować wysłane zapytanie DNS zamiast NXDOMAIN, musisz stracić cztery pakiety wysyłane 5 sekund jeden po drugim (2 dla oryginalnego zapytania i 2 dla wewnętrznej wersji domeny), co jest znacznie mniejsze prawdopodobieństwo. W rzeczywistości tylko raz widzieliśmy NXDOMENĘ po 15s i nigdy nie widzieliśmy błędu po 20s.

aby potwierdzić to założenie, zbudowaliśmy serwer DNS proof-of-concept, który odrzuca wszystkie żądania cloudflare.com, ale wysyła NXDOMENĘ dla istniejących domen:

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

W końcu odkryliśmy, co się dzieje i mieliśmy sposób na niezawodne odtworzenie tego zachowania.

rozwiązania

zastanówmy się, jak możemy ulepszyć naszego klienta, aby lepiej radził sobie z tymi przejściowymi problemami sieciowymi, czyniąc go bardziej odpornym. Strona podręcznika dla resolv.conf informuje, że masz dwa pokrętła: opcje timeout I retries. Wartości domyślne to odpowiednio 5 i 2.

o ile serwer DNS nie jest bardzo zajęty, jest mało prawdopodobne, aby odpowiedź zajęła więcej niż 1 sekundę. W rzeczywistości, jeśli zdarzy ci się mieć urządzenie sieciowe na Księżycu, możesz oczekiwać, że odpowie w ciągu 3 sekund. Jeśli serwer nazw znajduje się w następnej szafie i jest dostępny przez szybką sieć, możesz bezpiecznie założyć, że jeśli nie ma odpowiedzi po 1 sekundzie, serwer DNS nie otrzymał zapytania. Jeśli chcesz mieć mniej dziwnych błędów „brak takiej domeny”, które sprawiają, że drapiesz się po głowie, równie dobrze możesz zwiększyć liczbę powtórzeń. Im więcej razy spróbujesz ponownie z przejściową utratą pakietów, tym mniejsza szansa na niepowodzenie. Im częściej ponawiasz próbę, tym większe szanse na szybsze zakończenie.

wyobraź sobie, że masz naprawdę losową utratę pakietów 1%.

  • 2 powtórzenia, 5s timeout: max 10s czekać przed błędem, 0.001% szansa na niepowodzenie
  • 5 powtórzeń, 1s timeout: max 5s czekać przed błędem, 0.000001% szansa na niepowodzenie

w rzeczywistości Dystrybucja byłaby inna ze względu na fakt, że utrata pakietów nie jest losowa, ale możesz spodziewać się znacznie mniej czasu na odpowiedź DNS z tego typu zmianami.

jak wiesz, wiele bibliotek systemowych, które zapewniają rozdzielczość DNS, takich jak glibc, nscd, systemd-resolved, nie jest utwardzonych, aby poradzić sobie z przebywaniem w Internecie lub w środowisku z utratą pakietów. Wiele razy staliśmy przed wyzwaniem stworzenia niezawodnego i szybkiego środowiska DNS, aby później odkryć, że rozwiązanie nie jest idealne.

do ciebie

biorąc pod uwagę to, co przeczytałeś w tym artykule o utracie pakietów i Splicie-DNS / private-namespace, jak zaprojektowałbyś szybką i niezawodną konfigurację rozdzielczości? Jakiego oprogramowania używasz i dlaczego? Jakie zmiany tuningu w stosunku do standardowej konfiguracji byś zastosował?