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

Kubernetes運用実務

ノードが死んだあとの5分は誰が決めたのか

TT Labで続きを見る

一言でいうと

ノードに問題が起きると、ノードコントローラーがNoExecuteテイントを付け、その時点から、Podがいつ消えるかはPodのtolerationSecondsが決めます。ほとんどのPodはこの値を書いたことがなく、アドミッションが入れてくれた300秒で動いています。

なぜこの値を知っておく必要があるのか

drainは人が実行します。時刻も対象も、私たちが選びます。しかし、明け方に呼び出される事故は、たいていその逆です。誰もコマンドを実行していないのにPodが移動し始め、あるワークロードは移動が早すぎてローカルキャッシュをすべて失い、別のワークロードは死んだノードに長く居座りすぎて、トラフィックが空の場所へ流れます。この2つが同じクラスターで同時に起こる理由は単純です。2つのワークロードが同じ値を使っているからです。

その値は300秒です。KubernetesはPodを作成するとき、node.kubernetes.io/not-readyとnode.kubernetes.io/unreachableの2つのキーに対するNoExecuteトレラレーションを、tolerationSecondsを300に設定して自動的に付けます。明示的に指定しなかった場合にのみ付けられます。公式ドキュメントの表現のとおり、自動的に追加されたこのトレラレーションのせいで、Podは問題が検知されてから5分間、ノードに縛られることになります。

どう動くのか

まず、ノード側です。kubeletは定期的に自分のLeaseを更新して、生きていることを知らせます。この更新が途絶えると、ノードコントローラーがノードのReadyコンディションをUnknownに変え、それに対応するテイントを付けます。コンディションとテイントは、次のように対応しています。

ノードのコンディション 付与されるテイント デフォルトの効果
Ready = False node.kubernetes.io/not-ready NoExecute
Ready = Unknown node.kubernetes.io/unreachable NoExecute
MemoryPressure node.kubernetes.io/memory-pressure NoSchedule
DiskPressure node.kubernetes.io/disk-pressure NoSchedule
PIDPressure node.kubernetes.io/pid-pressure NoSchedule
NetworkUnavailable node.kubernetes.io/network-unavailable NoSchedule

スケジューラーがノードのコンディションではなくテイントを見るという点が重要です。コンディションを直接見させると、配置の判断がコンディションの種類の数だけ分かれますが、テイントに一度翻訳しておけば、配置もエビクションも1つのルールで処理されます。

次が、効果の違いです。NoScheduleは、新しく配置することだけを防ぎ、すでに起動しているPodには手を触れません。PreferNoScheduleは、その弱いバージョンです。NoExecuteだけが、すでに起動しているPodを追い出します。このとき、トレラレーションは3つに分かれます。許容するトレラレーションがなければ即座にエビクション、許容するがtolerationSecondsがなければ永遠に残り、秒数が書かれていればその秒数だけ耐えたあとにエビクションされます。テイントがその前に外れれば、エビクションは起こりません。

DaemonSetは例外です。DaemonSetコントローラーが作成するPodには、not-readyとunreachableの2つのキーに対するNoExecuteトレラレーションが、tolerationSecondsなしで付きます。ノードが不安定だからといってノードエージェントまで抜けてしまうと、そのノードを観測したり復旧したりする手段も一緒になくなるからです。

もう1つあります。エビクションが一度に集中するのを防ぐため、コントロールプレーンは、ノードに新しいテイントを付ける速度を制限します。大規模なネットワーク断で、クラスター全体が一瞬で再配置されるのを防ぐ仕組みです。そして1.29以降は、このエビクションの実装がノードコントローラーから切り離され、taint-eviction-controllerという別のコントローラーが担当します。

現場での姿

最もよくある事故は、短いネットワークの揺らぎで、ステートフルなワークロードがまるごと移動してしまうことです。40秒のスイッチの再起動なのに、300秒後にはPodがすでに別のノードで空のローカルディスクを見ながら、最初からデータを受け取っています。このようなワークロードには、tolerationSecondsを長く設定するほうがよいです。公式ドキュメントも、ローカル状態の多いアプリケーションを、ネットワーク断の状況でも長くノードに縛っておきたい場合があると述べ、その例として6000秒を挙げています。

逆方向の事故も、同じ値から生じます。ノードが本当に死んだのに、300秒のあいだサービスのレプリカが足りないまま動き続けます。フロントエンドのように、どこで起動しても同じワークロードなら、この5分がまるごと損失です。ただし、このようなところは、値を短くするだけでなく、レプリカを増やしてトポロジーを分散するほうが、たいてい優れています。値だけを短くすると、揺らぐたびに再配置が頻繁になり、かえって不安定になります。

3つ目は、テイントを外すのを忘れることです。点検のためにNoExecuteを付けてPodを退避させたのに、点検が終わったあとにテイントを削除しないと、そのノードはずっと空のままです。クラスターの容量が3分の1減ったまま何週間も過ぎてしまうことは、珍しくありません。

このラボ環境の限界

このクラスターのノードはkwokが作成した偽のノードなので、本物のkubeletがありません。そのため、ハートビートを止めて、ノードコントローラーが自らテイントを付けるようにすることはできません。さらに、ここで1つ、実際に計測してみました。Readyのノードにnode.kubernetes.io/unreachable:NoExecuteを手で付けても、数秒以内に消えます。この2つのキーのテイントは、ノードコントローラーがノードのコンディションを見て直接管理しているので、コンディションが正常なノードに付いているものは、誤った状態と見なして外してしまうからです(ノードのstatusを直接書き換えてReadyをFalseにしても、kwokがすぐに元に戻します)。そのため、ラボではカスタムキーで同じNoExecuteテイントを付けます。キーが違うだけで、エビクションのルールは文字どおり同じであり、その後に起こるエビクション、つまり誰がいつ消えるのかは、本物のコントローラーが行う本物の動作です。

また、kwokのPodはコンテナではないので、コンテナのログもexecもOOMもありません。Podが移動してデータを受け取り直す場面はこの環境では見られず、見られるのはオブジェクトが消える時刻です。このラボが扱うのは、まさにその時刻です。

次のラボですること

手を付けない比較群を1つのノードに立て、誰も書いていないデフォルトのトレラレーションを目で確認します。次に、許容する時間だけが異なるPodを2つ別のノードに置き、NoScheduleでは何も起こらず、NoExecuteでは1つが実際に消えるのを確認します。続いて、別のノードにリンク断を模したNoExecuteテイントを付け、20秒と3600秒の違いを計測し、ネームスペース全体のエビクション時刻表を計算するスクリプトを作成します。最後に、状態を持つワークロードに与える値を決めてクラスターに適用し、その根拠を残します。

参考ドキュメント: