TT Lab
はじめる
学ぶ 学習パス コース

証明書は更新されたのに、ブラウザは古いものを表示した

DNS に伝播はない、キャッシュと TTL があるだけ

TT Labで続きを見る

一言でいうと

名前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で短い名前がどんな問い合わせを作り出すのかを、サーバーログで確認します。