DNS に伝播はない、キャッシュと TTL があるだけ
一言でいうと
名前1つがアドレスになるまでには、/etc/hosts → リゾルバー設定 → キャッシュ → 権威サーバーという順序があり、その間のどこでも、古い答えがTTLの分だけ生き残ります。「DNSを変えたのに、なぜまだ古いアドレスに行くのか」の答えは、ほとんどいつも、この順序の中にあります。
なぜ必要なのか
サービスを新しいサーバーに移し、Aレコードを変えました。自分のノートパソコンでは新しいアドレスが出るのに、顧客の半分はまだ古いサーバーにつながっています。担当者は「伝播(propagation)に時間がかかる」と言いますが、その言葉は説明ではなく、あきらめです。DNSには伝播というものはありません。あるのはキャッシュとTTLだけで、各キャッシュは、自分が受け取っておいた答えを、TTLが尽きるまでそのまま返します。したがって「いつ終わるのか」という問いには、正確に答えられます。変更前のレコードのTTLが過ぎれば終わります。
反対方向の事故もあります。名前を作る前に誰かが先に問い合わせてしまうと、「ない」という答えもキャッシュされます。そのため、レコードを入れたあともしばらくNXDOMAINが出続け、この時間はAレコードのTTLではなく、ゾーンのSOAが決めます。この2つを知らないと、DNS障害を前にしてできることが、待つことだけになります。
どう動くのか
プログラムがgetaddrinfo()を呼ぶと、glibcはまず/etc/nsswitch.confのhosts:行を見ます。たいていfiles dnsの順なので、/etc/hostsにその名前があれば、DNSにはまったく問い合わせません。getent hosts <이름>はこの順序をそのままたどるため(プレースホルダーは名前です)、「プログラムが見る答え」を確認するときは、digよりgetentのほうが正確です。digは、リゾルバーライブラリを経由せず、DNSサーバーに直接問い合わせるツールです。
DNSに進むと、/etc/resolv.confが決まりです。nameserverは最大でMAXNS(現在は3)個まで書け、書かれた順に問い合わせます。searchリストとoptions ndots:n(既定は1)は、短い名前をどう補って試すかを決めます。名前のドットの数がndotsより少なければ、検索ドメインを1つずつ付けて先に問い合わせ、すべて失敗したら、元の名前で問い合わせます。Kubernetes Podのresolv.confは、公式ドキュメントのとおり、search <ns>.svc.cluster.local svc.cluster.local cluster.localにoptions ndots:5なので、Podの中でapi.example.comのようにドットが5個未満の外部の名前を呼ぶと、検索ドメインが付いた問い合わせが先に出て、NXDOMAINを3回受け取ったあとに、ようやく本当の名前を問い合わせます。そのため、Podから外部APIだけが特に遅いとき、最初に疑うのがndotsです。
リゾルバー(キャッシュ)は、RFC 1034が定めるとおり、再帰的に権威サーバーをたどって答えを受け取り、レコードごとに付いているTTLの秒数のあいだ、その答えを保存します。TTLは権威サーバーのゾーン(zone)が決め、キャッシュは残り時間を減らしながら返します。digで同じキャッシュに2回問い合わせると、2回目の答えのTTLが減っているのが、その証拠です。
$ dig @127.0.0.1 -p 5301 www.lab.internal A +noall +answer
www.lab.internal. 120 IN A 10.0.0.10
$ dig @127.0.0.1 -p 5301 www.lab.internal A +noall +answer # 3초 뒤
www.lab.internal. 117 IN A 10.0.0.10
CNAMEは「この名前は、あの名前の別名」です。リゾルバーはCNAMEに出会うと、チェーンを最後までたどり、CNAMEと最後のAを1つの答えに入れて返します。チェーンが長いほどキャッシュするレコードが増え、それぞれのTTLが別々に進みます。
存在しない名前はどうキャッシュされるのでしょうか。RFC 2308は、権威サーバーがNXDOMAIN(名前なし)やNODATA(名前はあるが、そのタイプがない)を返すとき、authorityセクションにゾーンのSOAを入れ、そのTTLをSOAのMINIMUMフィールドとSOA自体のTTLのうち小さいほうの値にするよう定めています。リゾルバーはその時間のあいだ「ない」を覚えています。ゾーンにレコードを追加する前に試しに問い合わせてしまったなら、追加したあとも、その時間のあいだは「ない」という答えを受け取ります。
$ dig @127.0.0.1 -p 5301 nope.lab.internal A +noall +comments +authority
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, ...
;; AUTHORITY SECTION:
lab.internal. 60 IN SOA ns1.lab.internal. admin.lab.internal. 2026091101 3600 600 86400 60
現場での姿
移行作業を計画するとき、熟練者がすることは1つです。変更の数日前に、先にTTLを下げます。TTLが3600のレコードを変えると、最悪の場合、1時間のあいだ2台のサーバーが一緒にトラフィックを受けます。あらかじめ60に下げておけば(その変更自体に、古いTTLの分だけ時間がかかります)、実際の移行は1分以内に終わります。終わったあとにTTLを元に戻すことも忘れません。低いTTLはキャッシュのヒット率を下げ、権威サーバーの負荷を増やします。
Kubernetesでは、ndots:5が作る追加の問い合わせが、CoreDNSの負荷とレイテンシとして現れます。外部の名前を、末尾にドットを付けた絶対名(api.example.com.)で書くか、PodのdnsConfigでndotsを下げるのが定石です。/etc/hostsも落とし穴になります。以前にデバッグしながら入れておいた1行が残っていると、そのマシンだけが別の場所に行きます。そのため、「このマシンでだけ違う」という報告を受けたら、getent hostsから叩きます。
次のラボですること
Podの中に、権威サーバーとキャッシュサーバーをPythonで立ち上げます。ゾーンファイルにTTL 120のAレコードとSOA(minimum 60)を置き、digでTTLが減っていくこと・CNAMEチェーン・NXDOMAINのSOAのTTLを読みます。次にゾーンを直して、権威サーバーは新しい答えを返すのに、キャッシュは古い答えを返す状態を再現し、キャッシュを空にしたあと、最悪の伝播時間を計算してレポートに書きます。最後に、+search +ndots=5で短い名前がどんな問い合わせを作り出すのかを、サーバーログで確認します。