Articles

デバッグ戦争ストーリー:NXDOMAINの謎

次のブログ記事では、CloudflareのMesosベースのクラスターでのデバッグの冒険について説明します。 この内部クラスターは、主にCloudflareのお客様が分析できるようにログファイル情報を処理し、攻撃を検出して対応するシステムに使用されます。

発生した問題は、お客様に影響を与えませんでしたが、エンジニアが頭を悩ませました。..

問題

クラスターのある時点で、次のようなエラーが表示され始めました(内部DNS上の既存のドメインのNXDOMAIN)。

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

ドメインが実際に存在していたので、これは非常に奇妙に見えました。 それは私たちの内部ドメインの一つでした! エンジニアは、彼らがこの動作を見たと述べていたので、私たちはより深く調査することにしました。 このエラーをトリガーするクエリは、mesos-dnsによって管理される動的SRVレコードから、クラスター内から検索される外部ドメインまで様々でした。

私たちの最初の素朴な試みは、ループで次のことを実行することでした。

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

これをあるサーバーでしばらく実行しても問題は再現されませんでした:すべての検索が成功しました。 その後、1日サービスログを取得し、「no such host」と同様のメッセージのgrepを実行しました。 エラーは散発的に起こっていた。 エラーと任意の結論に私たちを導くことができる明白なパターンの間に時間がありました。 私たちの調査では、エラーがJavaサービスからも発生していたため、多くのサービスで使用しているGoにエラーが発生する可能性は捨てられました。

ウサギの穴に

私たちは、クラスタDNSリゾルバのためにいくつかのマシン間で単一のIP上でUnboundを実行するために使用されます。 その後、BGPはマシンからルータへの内部ルートの通知を担当します。 私たちは、別のマシンから多くの要求を送信し、エラーを記録することによってパターンを見つけようとすることにしました。 ここでは、ロードテストプログラムが最初のように見えたものです:

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

小さな一時停止とログエラーを持つループでnet.LookupHostを実行します。 これをDockerコンテナにパッケージ化し、Marathonで実行することは、とにかく他のサービスを実行する方法であるため、私たちにとって明らかな選択でした。 ログはKafkaに出荷され、次にKibanaに出荷され、そこで分析することができます。 50msごとにルックアップを行う65台のマシンでこのプログラムを実行すると、ホスト間で次のエラー分布(高から低へ)が表示されます。

ラックや特定のマシンとの強い相関は見られませんでした。 エラーは多くのホストで発生しましたが、それらのすべてではなく、異なる時間にwindowsエラーが異なるマシンで発生します。 X軸に時間を置き、y軸にエラーの数を置くと、次のことが示されました。

特定のDNSリサーサが狂っているかどうかを確認するために、通常のマシンオンのすべてのロードジェネレータを停止し、リサーサ自体のロード生成ツールを開始しました。 数時間でエラーはなく、Unboundは完全に健康であることが示唆されました。私たちはパケット損失が問題であると疑い始めましたが、なぜ”そのようなホストはありません”が発生するのでしょうか? NXDOMAINエラーがDNS応答にある場合にのみ発生するはずですが、私たちの理論は、応答がまったく戻ってこないということでした。

欠落している

パケットを失うと”no such host”エラーが発生するという仮説をテストするために、まずポート53で発信トラフィックをブロックしようとしました。

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

この場合、digと同様のツールはタイムアウトしますが、”no such host”を返さないでください。

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

Goは少し賢く、何が起こっているのかについての詳細を教えてくれますが、”そのようなホストはありません”も返されません:

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

Linuxカーネルはパケットを落としたことを送信者に伝えるので、パケット損失を模倣するためにパケットを何もしないネットワーク内のブラックホールをネームサーバーに向ける必要がありました。 まだ運がありません:

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

ネットワークを非難し続けるために、私たちは何とか仮定をサポートしなければならなかったので、ルックアップにタイ エラーは10秒後に起こっていたが、比較的多くの成功した応答は5秒後に来ていた。 それはパケット損失のように見えますが、なぜ「そのようなホストがいない」のかはまだわかりません。

どのホストがこの影響を受ける可能性が高いかを知っていたので、次の二つのコマンドを二つのscreenセッションで並列に実行しました。

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

ポイントは、解決に失敗したネットワークダンプを取得することでした。 そこでは、次のクエリが表示されました。

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

二つのクエリは何の答えもなくタイムアウトしますが、三つ目は幸運になり成功します。 当然のことながら、cloudflareはありません。comは私たちの内部ドメインにあるので、Unboundは正当にnxdomainを返信し、検索が開始されてから10秒後にnxdomainを返します。/etc/resolvを見てみましょう。conf詳細を理解するには:

nameserver 10.1.0.9search in.our.internal.domain

検索キーワードを使用すると、FQDNの代わりに短いホスト名を使用することができ、myhostmyhost.in.our.internal.domainと同等になります。

DNSリゾルバーの場合は、次のことを意味します。.in.our.internal.domainをクエリに追加して再試行します。 元のDNSクエリでどのような失敗が発生するかは問題ではありません。 通常はNXDOMAINですが、私たちの場合はパケット損失による読み取りタイムアウトです。

“no such host”エラーが表示されるには、次のイベントが発生する必要があるようです:

  1. 元のDNS要求が失われる必要があります
  2. 5秒後に送信される再試行が失われる必要があります
  3. 内部ドメインの後続のクエリ(searchオプ元のクエリの場合は2、ドメインの内部バージョンの場合は2)、これははるかに小さい確率です。 実際には、15秒後にNXDOMAINを一度だけ見て、20秒後にエラーを見たことはありませんでした。この仮定を検証するために、以下のすべての要求を削除する概念実証DNSサーバーを構築しました。

    cloudflare.comしかし、既存のドメインのNXDOMAINを送信します:

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

    最後に、何が起こっているのかを見つけ、その動作を確実に複製する方法がありました。

    ソリューション

    クライアントを改善して、これらの一時的なネットワークの問題をよりよく処理し、より弾力性のあるものにする方法を考 Resolvのmanページ。confは、timeoutretriesオプションの二つのノブがあることを示します。 デフォルト値はそれぞれ5と2です。DNSサーバーを非常にビジー状態にしない限り、返信に1秒以上かかることはほとんどありません。

    実際、月にネットワークデバイスがある場合は、3秒で応答することが期待できます。 ネームサーバーが次のラックにあり、高速ネットワーク経由で到達可能な場合は、1秒後に応答がない場合、DNSサーバーがクエリを取得しなかったと安全に想定で あなたの頭を傷つけるような奇妙な「そのようなドメインなし」エラーが少なくしたい場合は、再試行を増やすこともできます。 一時的なパケット損失で再試行する回数が多いほど、失敗の可能性は低くなります。 より頻繁に再試行するほど、より速く終了する可能性が高くなります。あなたは本当にランダムな1%のパケット損失を持っていると想像してみてくださ

    • 2回の再試行、5秒のタイムアウト:最大10秒エラーの前に待機、0.001%の失敗の可能性
    • 5回の再試行、1秒のタイムアウト:最大5秒エラーの前に待機、0.000001%の失敗の可能性

    実際の生活では、パケット損失がランダムではないという事実のために分布が異なるでしょうが、DNSがこのタイプの変更で応答するのをはるかに待つことを期待することができます。あなたが知っているように、GLIBC、nscd、systemd-resolvedのようなDNS解決を提供する多くのシステムライブラリは、インターネット上またはパケット損失のある環境で処理する 私たちは、成長するにつれて、信頼性の高い高速なDNS解決環境を作成するという課題に何度も直面してきました。パケット損失とsplit-DNS/private-namespaceについてのこの記事で読んだことを考えると、高速で信頼性の高い解決設定をどのように設計しますか? どのようなソフトウェアを使用し、なぜですか? 標準構成からどのようなチューニングの変更を使用しますか?