LFCS — Linux Foundation認定システム管理者
名前が住所になる道、そして住所が変わった日
一言でいうと
ネットワークの問題は「つながらない」と報告されますが、実際には、名前解決・ルーティング・ソケットの3つの区間のどれかで終わります。どの区間かを言わない診断は、診断ではありません。
なぜ必要なのか
curlが失敗しました。ここからすぐにDNSを疑うのは、半分しか正しくありません。Linuxホストでの名前解決は、DNSだけの仕事ではありません。まず/etc/nsswitch.confのhosts:の行が、どの順序でどこに尋ねるかを決めます。通常はfiles dnsで、そのため/etc/hostsがDNSより先に答えます。そのあとで初めて、/etc/resolv.confが、どのサーバーに尋ねるか、どの検索ドメインを付けるかを決めます。
この構造のために、診断ツールの選び方が決まります。getent hostsはこの経路全体を通り、digはDNSだけを見ます。2つの結果が違うなら、問題はDNSサーバーではなく、スタブリゾルバーの区間にあります。この1行が、障害対応の会議を5分短縮します。
/etc/resolv.confで覚えておくべき値があります。nameserverは最大3つまでしか使われません。options timeoutはデフォルトが5秒、options attemptsはデフォルトが2回です。そのため、最初のネームサーバーが停止していると、ユーザーはデフォルトの場合、5秒をそのまま体感します。冗長化をしてあるのに遅い理由です。options ndotsは、名前にドットがいくつ以上あれば、検索ドメインを付けずに絶対名で先に問い合わせるかのしきい値で、デフォルトは1です。コンテナ環境でこの値が大きく設定されていると、外部の名前1つを解決するのに、失敗するクエリが何回も先行します。
どう動くのか
ルーティングテーブルは、上から下へ走査する一覧ではなく、最も長いプレフィックスが勝つ検索です。10.0.0.0/24と0.0.0.0/0の両方があれば、10.0.0.5へ向かうパケットは、いつも前者を通ります。デフォルトルートは、プレフィックス長が0のもの、つまり「何も一致しないとき」の経路です。ip routeの出力で、viaは次のホップ、devは出ていくデバイス、srcは送信元アドレスとして使う値です。
ソケットの状態は、それ自体が診断情報です。
| 状態 | 意味 | 多く見えたら |
|---|---|---|
LISTEN |
サーバーが接続を待っている | 正常 |
ESTABLISHED |
双方向の接続が成立 | 正常。個数が上限に達したら問題 |
SYN-SENT |
接続要求を送って応答を待っている | 相手がいないか、途中で破棄されている |
TIME-WAIT |
能動的に閉じた側が最後のパケットを待っている | 短い接続を大量に開閉している |
CLOSE-WAIT |
相手が閉じたのに、こちらが閉じていない | アプリケーションのバグのサイン |
TIME-WAITが多いのは、たいてい正常です。接続を先に切った側に残る状態で、遅れて届いたパケットが新しい接続を汚染しないようにする仕組みです。一方、CLOSE-WAITが溜まるのは、コードがソケットを閉じていないという意味なので、性格がまったく違います。
現場での姿
筆者のホームラボのクラスターが、ある日まるごと応答しなくなりました。ログにはdial tcp 10.0.0.111:6443: connect: no route to hostだけが繰り返されました。診断は5分で終わりました。ip -4 addr showを見ると、コントロールプレーンノードのアドレスが.111から.120に変わっていて、出力にscope global dynamicが付いていました。DHCPのリース更新の過程で、別のアドレスを受け取ったのです。
決定的な証拠は、証明書にありました。apiserverの証明書のSANの一覧には、10.0.0.111だけがあって、.120がありませんでした。そのため、etcdとkube-apiserverは、存在しない.111にバインドしようとして失敗し、再起動ループに陥り、kube-schedulerとcontroller-managerは127.0.0.1にバインドするので、プロセスは生きているのに何もできない状態になりました。アドレス1つが変わっただけで、7ノードのプラットフォーム全体が止まったのです。
再発防止は、netplanで静的アドレスを書き込むことでした。インターフェースにdhcp4: falseを与え、アドレスとデフォルトルート、ネームサーバーを直接指定します。ところが、それだけでは足りませんでした。cloud-initが起動時にネットワーク設定を再生成して上書きしてしまうので、次の1行も一緒に入れる必要がありました。
echo 'network: {config: disabled}' > /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg
これに加えて、ルーター側でも、該当のMACにDHCPの予約を設定するか、そのアドレスをプールから外す必要があります。ホストだけを固定しても、別の機器が同じアドレスをリースして、衝突することがあるからです。3つすべてを行って初めて「固定」です。適用後、ip addrの出力からdynamicの表示が消えたことが、確認のサインでした。
教訓がもう1つあります。このクラスターは、controlPlaneEndpointがVIPやDNS名ではなく、特定のノードの物理IPで書き込まれていました。そのため、あとでコントロールプレーンを3台に増やしてetcdのクォーラムを備えたのに、その1台が停止すると、残りの2台が無事でも、すべてのクライアントが入口を見つけられない状態になりました。アドレスを間接化しないと、冗長化はデータの可用性を上げるだけで、アクセスの可用性はそのままです。
次のラボですること
netplanの静的アドレスのファイルと、cloud-initの無効化ファイルを、自分で書きます。/etc/hostsにエントリを入れて、getentでその経路が実際に使われるかを確認し、/etc/resolv.confと/etc/nsswitch.confを読んで要約を作ります。リスナーを1つ起動してssでソケットを観察し、ip routeでデフォルトルートをパースします。最後に、ファイアウォールのルールを文書として設計しながら、ルールの順序がなぜ生死を分けるかを確認します。この環境では、iptablesとpingが塞がれているので、ルールの適用と到達性の確認は、概念として扱います。