glibc, musl libc, go の resolver の違い

先日、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 などの対応状況

```:resolv.confの例
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 の動作確認で使ったコード

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