先日、resolv.conf で timeout を調整したいなと思うことがありました、しかし、Docker だの Kubernetes だのといった時代です。Linux しか使っていなかったとしても resolver が glibc のそれだとは限らないわけですね。そこで glibc, musl libc (alpine のやつです), go の resolver の違いを調べてみました。
環境
それぞれ docker container を使い、問い合わせの状況は tcpdump で確認しました。
macOS Catalina (10.15.7)
docker desktop 3.0.3 (51017)
container image は
alpine:3.12.3 / musl-1.1.24-r10
debian:buster-20201209 (10.7) / glibc 2.28-10
Go は mac 上で 1.15.6 を使い、クロスコンパイルして上記の alpine 上で実行しました。
クロスコンパイルなのでデフォルトで CGO_ENABLED=0
のはずですが、明示してビルドしました。
resolv.conf の options などの対応状況
nameserver 8.8.8.8
nameserver 8.8.4.4
search default.svc.cluster.local svc.cluster.local cluster.local asia-northeast1-b.c.project-id.internal c.project-id.internal google.internal
options ndots:5 timeout:5 attempts:2
resolv.conf の options にはいろいろありますが、今回の 3 つの resolver 全てで対応されているのは
ndots
timeout
attempts
の3つでした。
nameserver
は勿論ですが search
にも対応しています。
昔の musl libc は search
(domain 補完) には対応していませんでしたが 1.1.13 から対応していました。
nameserver
は複数回指定可能ですが、使われるのは3つまでです。4つ以上指定しても3つまでしか使われません。
それでは機能の違いを見ていきましょう
ndots
まずは ndots
です、Kubernetes を使うようになってはじめて知りました。
指定するのは問い合わせるドメインに含まれる dot (.
) の数です、Kubernetes のデフォルト設定では 5 (ndots:5
) となっています。
例えば ping www.google.com
とした場合、dot の数は 2 です。
dot の数が ndots 以上の場合
dot の数が ndots
で指定した以上であればそれは FQDN であろうと判断し、search
で指定したドメインを補完せずに DNS サーバーに問い合わせます。ここで、NXDOMAIN
(そんなドメイン存在しないよ) が返ってきたら glibc と go は search
で指定したドメインを補完して問い合わせます。しかし、musl libc は補完した問い合わせを行わないでそんなドメインは存在しないよということで終了してしまいます。
その昔、musl はそもそもドメイン補完に対応していなかったので当時から使っている人は常に FQDN で指定しているでしょうから問題ないでしょうが、知らないで ndots
を小さくすると解決できるはずのドメインが解決できないかもしれません。
dot の数が ndots 未満の場合
dot の数が ndots
未満であった場合はまず search
で指定した複数のドメインを順番に補完して問い合わせます、見つかるまで繰り返すため(これの上限を確認するの忘れた)、順序は重要ですかね。ただ、順序以上に ndots
で指定する数が重要です、5
というのは結構多いです。Kubernetes でデフォルト設定だと service.namespace.svc.cluster.local
という FQDN を指定したとしても 4
なわけです。search
に並んでいる全てのドメインの補完を試した後に service.namespace.svc.cluster.local
をそのまま問い合わせてやっと欲しい結果が得られるわけです、DNS サーバー側にネガティブキャッシュがあるとはいえ、DNS サーバーとの通信はその回数行われるわけですから、かなりの無駄です。GKE のデフォルト設定では6つもドメインが列挙されています。EKS は4つ。ndots
を小さくして名前解決ができないドメインが発生してしまうよりも、無駄な問い合わせが増えたとしても名前解決できる値がデフォルトなっているわけですね。ndots
はチューニングの余地がありそうです。
ちなみに、末尾に .
をつけた場合はそれは FQDN だぞということを意味するので ping www.google.com.
などとした場合はどの resolver でもドメインの補完は行いません。
nameserver
複数サーバーを指定した場合の扱いが glibc と musl で大きく異なるため、まず説明しておきます。
glibc と Go は一つ目のサーバーに問い合わせて、timeout 秒待っても応答がない場合に、次のサーバーに問い合わせます。
musl libc は複数の nameserver に同時にリクエストを送って最初に返ってきた応答を使います。3つのネームサーバーが指定されていたら3倍の問い合わせが発生してしまうわけですね。
timeout と attempts
timeout
と attempts
は関連しているため一緒に扱います。また、nameserver
の数にもよるためそれごとにまとめます。
nameserver が1つの場合
glibc と Go は問い合わせを投げては timeout
秒待ち、応答がなければ再度問い合わせて、合計 attempts
回問い合わせます。
musl libc は timeout
秒を attempts
で割った秒数をそれぞれの問い合わせで待ちます。つまり、リトライを含めた合計で最大 timeout
秒待つということです。 timeout:5 attempts:2
というデフォルト設定では 2.5 秒ずつ待ちます。
nameserver が2つの場合
glibc と Go は1つ目の nameserver
に問い合わせて timeout
秒待ち、次は2つ目の nameserver
に問い合わせます。これが attempts
の1回分です。timeout:5 attempts:2
というデフォルト設定ではそれぞれの nameserver
に対して2回ずつ、合計4回問い合わせます。その都度5秒待ちます。
musl libc は nameserver
の項で書いた通り、複数の nameserver
に対して同時に問い合わせるため、nameserver
が1つの場合と同じです。
nameserver が3つの場合
nameserver
が3つになると glibc の timeout
の振る舞いが少し変わります。1つ目の nameserver
に対しては timeout
で指定した秒数待ちますが、2つ目は timeout
で指定したのよりも短くなり、3つ目は timeout
で指定したものより長くなります。
timeout:5
の場合は5秒、3秒、6秒となります。このセットを attempts
回繰り返します。
Go は常に timeout
秒待ちます。
musl libc はこれまでと同じです。
timeout 時のドメイン補完
何度問い合わせても timeout するような場合はもういろいろダメなのであまり重要ではないですが、動作に違いがあったので書いておきます。
timeout が続く状態で、2つ目以降の search ドメインを補完するのかどうか、glibc は2つ目以降のドメイン補完は行いませんが、補完なしの問い合わせを行います。Go は律儀に全てのドメイン補完を試しますし、補完なしでの問い合わせもします。musl libc は1つ目の補完の問い合わせは行いますが、それで終わりです。補完なしの問い合わせもありません。
Go の動作確認で使ったコード
package main
import (
"context"
"net"
"os"
"log"
)
func main() {
ctx := context.Background()
resolver := &net.Resolver{}
for _, v := range os.Args[1:] {
log.Printf("Resolving %s\n", v)
names, err := resolver.LookupHost(ctx, v)
if err != nil {
log.Fatal(err)
}
for _, name := range names {
log.Printf("%s\n", name)
}
}
}
ndots:5 の凶悪さを可視化する
文章でつらつら書いても分かりづらいので、 GKE 環境で storage.googleapis.com
にアクセスしようとした場合にどんな問い合わせが発生するかを見てみましょう。default
namespace に立てた Pod で ping storage.googleapis.com
を実行した際の tcpdump の出力を加工しました。(横幅を抑えるために)
Cloud Storage の API にアクセスしようとする度にこの数の問い合わせが発生すると思うとゾッとしますね。マイクロサービスで Keep-Alive などをしていない場合は大量の内部通信でもこれが発生しているかもしれません。JVM などのように DNS のキャッシュをしてくれれば良いですけどね。
ndots:2
として常に FQDN で指定するということが徹底できると良いのかな。
14:53:34.706909 ▶︎ A? storage.googleapis.com.default.svc.cluster.local. (66)
14:53:34.707130 ▶︎ AAAA? storage.googleapis.com.default.svc.cluster.local. (66)
14:53:34.708881 ◁ NXDomain 0/1/0 (159)
14:53:34.708940 ◁ NXDomain 0/1/0 (159)
14:53:34.709051 ▶︎ A? storage.googleapis.com.svc.cluster.local. (58)
14:53:34.709133 ▶︎ AAAA? storage.googleapis.com.svc.cluster.local. (58)
14:53:34.709615 ◁ NXDomain 0/1/0 (151)
14:53:34.709689 ◁ NXDomain 0/1/0 (151)
14:53:34.709771 ▶︎ A? storage.googleapis.com.cluster.local. (54)
14:53:34.709816 ▶︎ AAAA? storage.googleapis.com.cluster.local. (54)
14:53:34.712211 ◁ NXDomain 0/1/0 (147)
14:53:34.712280 ◁ NXDomain 0/1/0 (147)
14:53:34.712387 ▶︎ A? storage.googleapis.com.asia-northeast1-b.c.my-project-id.internal. (83)
14:53:34.712479 ▶︎ AAAA? storage.googleapis.com.asia-northeast1-b.c.my-project-id.internal. (83)
14:53:34.716561 ◁ NXDomain 0/1/0 (189)
14:53:34.716623 ◁ NXDomain 0/1/0 (189)
14:53:34.716718 ▶︎ A? storage.googleapis.com.c.my-project-id.internal. (65)
14:53:34.716760 ▶︎ AAAA? storage.googleapis.com.c.my-project-id.internal. (65)
14:53:34.719891 ◁ NXDomain 0/1/0 (162)
14:53:34.720191 ◁ NXDomain 0/1/0 (162)
14:53:34.720304 ▶︎ A? storage.googleapis.com.google.internal. (56)
14:53:34.720390 ▶︎ AAAA? storage.googleapis.com.google.internal. (56)
14:53:34.724145 ◁ NXDomain 0/1/0 (145)
14:53:34.724352 ◁ NXDomain 0/1/0 (145)
14:53:34.724458 ▶︎ A? storage.googleapis.com. (40)
14:53:34.724500 ▶︎ AAAA? storage.googleapis.com. (40)
14:53:34.726930 ◁ 4/0/0 AAAA 2404:6800:4004:813::2010, AAAA 2404:6800:4004:81c::2010, AAAA 2404:6800:4004:81d::2010, AAAA 2404:6800:4004:81e::2010 (152)
14:53:34.726957 ◁ 16/0/0 A 172.217.161.80, A 172.217.175.16, A 172.217.175.48, A 172.217.175.80, A 172.217.175.112, A 216.58.197.144, A 172.217.25.208, A 172.217.25.240, A 172.217.26.48, A 172.217.31.176, A 172.217.161.48, A 172.217.174.112, A 172.217.175.240, A 216.58.220.112, A 216.58.197.208, A 216.58.197.240 (296)