15
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

glibc, musl libc, go の resolver の違い

Last updated at Posted at 2021-01-13

先日、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

timeoutattempts は関連しているため一緒に扱います。また、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)
15
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?