「さっき失敗したAPI、再実行したら普通に繋がった」

外部APIへのリクエストが、1日に数回だけタイムアウトする。ログを見ても異常なし。再実行すると何事もなかったように成功する——K3sを本番運用しているインフラエンジニアなら、一度はこのDNS間欠障害に心当たりがあるはずです。
厄介なのは、この症状が単一の原因では説明できないことです。多くのチームは「CoreDNSが重いのでは」と当たりをつけて時間を溶かしますが、実際に効いているのはndots:5によるDNSクエリの増幅とUDP conntrackのカーネルレースという、性質の異なる2つの問題の「掛け算」です。さらに、その対策として導入したはずのNodeLocal DNSCacheが、条件次第で新たな間欠障害の発生源になることもあります。
この記事では、K3sクラスタで実際に起きるDNS間欠障害を、原因の層ごとに切り分けて解説します。こうした罠は本来、運用チームが個別に踏みながら学ぶのではなく、クラスタの設計段階で潰しておきたいところです(記事の最後で、マネージドK8sサービスKuboがどう解決しているかにも触れます)。
1回の名前解決のつもりが、実は4回のクエリになっている
まず疑うべきなのは、Podの /etc/resolv.conf に自動設定される ndots:5 というオプションです。Kubenatives の解説によれば、この設定は「名前に含まれるドットが5個未満なら、まず検索ドメインを付加してから名前解決を試みる」という挙動をリゾルバに指示するものです。
Pod内から api.stripe.com(ドットは2個)のような外部ドメインに接続しようとすると、実際には次の順序でクエリが飛びます。
api.stripe.com.default.svc.cluster.local→ NXDOMAINapi.stripe.com.svc.cluster.local→ NXDOMAINapi.stripe.com.cluster.local→ NXDOMAINapi.stripe.com→ 成功
つまり1回の名前解決のつもりが、実際には4回のクエリが発生し、そのうち3回は必ず失敗する設計になっています。トラフィックの少ないクラスタでは気づきにくいですが、Podの数が増えるほど無駄なクエリが積み重なり、CoreDNSの負荷とレイテンシの両方を押し上げます。
対策: FQDNの末尾にドットを付ける、またはndots自体を下げる
外部ドメインへのアクセスが多いアプリケーションでは、次のいずれかで無駄なクエリを削減できます。
- 接続先を
api.stripe.com.(末尾にドット)と明示的なFQDNとして指定する。これにより「検索ドメインを付加しない完全な名前である」とリゾルバに伝えられます - Podの
dnsConfigでndotsの値を"2"程度に下げる(内部サービス名の解決に影響が出ないか事前に検証が必要です)
もう一つの罠 — UDP conntrackのカーネルレースが「5秒待ち」を生む

ndots:5によるクエリ増幅だけでも十分やっかいですが、K3s/Kubernetesクラスタではもう一つ、カーネルレベルの問題が重なります。weaveworks/weave の Issue #3287 で報告されているように、これはカーネルレベルのSNATレースコンディションです。
多くのリゾルバはA(IPv4)レコードとAAAA(IPv6)レコードを同時に問い合わせますが、この2つのUDPパケットが同じ送信元ポートを使ってNAT変換される際に、Linuxカーネルのconntrackテーブルで衝突が起きることがあります。片方のパケットが失われると、アプリケーション側はタイムアウト(多くの場合5秒)を待ってからリトライし、そこでようやく成功します。これはWeave・Flannelなど特定のCNIに限らず起きる既知の問題で、Kubernetes公式のNodeLocalDNSドキュメントも kubernetes/kubernetes#56903 として言及しています。
公式ドキュメントはさらに、UDPのDNSクエリがconntrackエントリを生成し続けること自体もリスクだと指摘しており、「UDPからTCPへの移行がパケットロスやDNSタイムアウト(最大30秒)に起因するテール・レイテンシを削減する」としています。UDPのconntrackエントリはタイムアウトするまでテーブルに残るため、DNSクエリの多いクラスタほど圧迫されやすくなります。
対策: single-request-reopenでA/AAAAクエリを直列化する
OneUptime のデバッグガイドでは、カーネル5.9より前の環境向けに、次のPod設定でA/AAAAクエリを同時ではなく順番に送るワークアラウンドが紹介されています。これにより同一ソケットでの競合が起きにくくなり、5秒待ちの発生率を下げられます。
dnsConfig:
options:
- name: single-request-reopen
対策のはずが罠に — NodeLocal DNSCacheが新しい間欠障害を生む瞬間

ここまでの2つの問題への定番の処方箋が NodeLocal DNSCache です。各ノードにDaemonSetとしてキャッシュエージェントを配置し、リンクローカルアドレス(169.254.0.0/16 範囲、Kubernetes公式ドキュメントによる)で待ち受けることで、キャッシュヒット時はCoreDNSへのネットワークホップをスキップできます。ある運用ブログでは、大量のPodが同時起動してDNS問い合わせが集中した障害と、導入後のレイテンシ改善(数百ミリ秒単位の応答が数ミリ秒台まで短縮)が実測値として報告されています。
しかし、ここで終わりではありません。NodeLocal DNSCache自体もDaemonSetである以上、それ自身が新たな間欠障害の発生源になり得ることを見落としがちです。ADHDecode の解説は次の2つのケースを指摘しています。
- ローリング更新中: NodeLocal DNSCacheのDaemonSetを更新すると、一時的にキャッシュPodが存在しないノードが生じる
- ノードのリソース逼迫時: CPU/メモリ/ディスクの逼迫でPodがEvictされる際、NodeLocal DNSCacheのPodも対象になり得る
さらに興味深いのは、この状態で起きる症状のパターンです。「内部のKubernetesサービス名は解決できるのに、外部ドメインだけ時々タイムアウトする」——これは、内部名の解決経路と外部名の解決経路が異なるために起きる、一見矛盾した現象です。「NodeLocalDNSを入れたのに、まだ時々失敗する」と感じたら、まずキャッシュPod自体の稼働状況を疑う価値があります。
このDaemonSetのローリング更新タイミングやノードのリソース逼迫を運用者が常に監視し続けるのは、地味に工数がかかる作業です。Kubo はK3sベースのマネージドKubernetesとして、NodeLocal DNSCacheを含む標準コンポーネントの健全性をCaptain UIで可視化し、更新時の切り替えも運用側で管理する設計になっています。
K3s固有の落とし穴 — kubectl edit configmap corednsが消える理由

ここまでの3つの問題を理解していても、K3sならではの落とし穴がもう一つあります。K3sはCoreDNSをバンドルされたAddonとして管理しており、K3s公式ドキュメントには「CoreDNSの設定をカスタマイズするには、kube-system namespaceにcoredns-customという名前のConfigMapを作成する」と明記されています。
つまり、CoreDNSの設定を直接 kubectl edit configmap coredns で編集しても、K3sの起動時にAddonとして再デプロイされ、その変更は消えてしまいます。OneUptime のK3s向け解説は、正しい永続化の手順を次のように整理しています。
coredns-customという名前のConfigMapを作成する*.overrideに書いたキーは既定の:.53Server Blockに取り込まれる*.serverに書いたキーは追加のServer Blockとして扱われる- このConfigMapのマニフェストを
/var/lib/rancher/k3s/server/manifests/coredns-custom.yamlに配置しておくことで、K3sの再起動・アップグレードをまたいで設定が消えない
K3s公式ドキュメントはさらに、--resolv-conf オプションでシステムの設定ファイルを書き換えずに代替のresolver設定を渡せることも案内しています。DNS周りのカスタマイズをK3sクラスタで行う際は、この永続化の作法を最初に押さえておく必要があります。
DNSの間欠障害は「1つの原因」を探しても見つからない
K3sクラスタでのDNS間欠障害は、単一の犯人がいるわけではありません。ndots:5が名前解決を4倍のクエリに増幅し、UDP conntrackのカーネルレースがパケットロス→5秒待ちを生み、その対策であるNodeLocal DNSCache自体がローリング更新やノード逼迫時に新たな障害源になり、K3s固有のAddon管理が素朴な設定変更を再起動のたびに消し去る——4つのレイヤーが互いに独立していながら組み合わさって現れるため、「これを直せば解決」という単純な対処では再発しやすいのが実情です。
こうした罠を一つひとつ踏みながら学んでいく状態は、本来インフラの専門性を割くべき場所ではないはずです。Kubo はK3sベースのマネージドKubernetesサービスとして、NodeLocal DNSCacheの適切な設定やCoreDNSカスタマイズの永続化パターンをあらかじめ組み込んだ状態でクラスタを提供します。DNSスタックの設計・検証を一から自分たちで積み上げる前に、料金プランも含めて比較してみる価値はあるはずです。EKS/AKSのようなフルマネージドの利便性と、K3sならではのコスト効率を両立できるよう設計されています。オンプレミスでのデータ主権が要件になる場合はKubo On-Premiseも選択肢です。DNSスタックを含むクラスタ設計の相談はお問い合わせからどうぞ。