🩺

Kubernetes の Probe の仕組みと考慮点

2023/04/10に公開

Kubernetes には、ワークロードの正常性を確認するための Probe という仕組みがあり、Liveness / Readiness / Startup Probe が用意されています。kubelet (Kubernetes のノード上で動作するエージェント) は、ワークロードに対して TCP Socket / HTTP GET / gRPC / Exec の中から指定されたチェックを定期的に実行します。

それぞれの Probe の特性を理解して使い分けないとサービスに影響することがあります。本記事では、Probe の仕組みと考慮すべき点について紹介します。

Probe の仕組み

Exec と TCP Socket / HTTP GET / gRPC の Probe の仕組みは異なります。

Exec Probe

kubelet は、Container Runtime Interface (CRI) API を介して高レベルなコンテナランタイム (e.g. containerd, cri-o) に任意のコマンドの実行をお願いします。containerd の場合は、kubectl exec と同様に ExecSync を呼び出して、任意のコマンドを実行しています。

TCP / HTTP / gRPC Probe

kubelet は、ホスト上の network namespace (ノード IP) から Pod IP に対して Probe を実行します。Pod の network namespace に setns(2) して、localhost に対して Probe を実行していない点に注意が必要です。

この実装の影響でいくつかの問題が指摘されています。

  • 悪意のあるユーザーが内部 / 外部サービスや他の namespace の Pod に対して HTTP GET リクエストを投げることができる (k/k#99425)
  • IPv4 / IPv6 デュアルスタック環境 (IPv4 優先) で IPv6 な PodIP に対して Probe が成功しない (k/k#101324)
  • Network Policy でノード IP からの通信は暗黙で許可している (comment - k/k#102613)

k/k#102613 で CRI API を介して Probe を実行する方法が議論されていますが、今のところ大きな動きはありません。Probe の host に localhost を指定して、ノード上のプロセスを監視しているユーザーにとって破壊的変更になってしまうことを懸念しているようです。

ヘルスチェックのコスト

ヘルスチェックの実行にはノードのリソースを消費します。大量の Pod が短い間隔で Probe を実行すると、ノードに負荷が掛かって Probe がタイムアウトします。Liveness Probe のタイムアウトが頻発すると、Pod は意図せず再起動されます。ヘルスチェックが失敗するのはワークロードに問題がある時だけでなく、ヘルスチェックを実行するノードの状態によっても起こりうることに注意しましょう。

KEP-3066: Sub-second / More granular probes の議論でも、ヘルスチェックのコストが懸念として挙げられています。Probe の間隔に 1 秒未満の値を設定できるようになると、大量の Pod が載っているノードの kubelet のパフォーマンスに影響するからです。

ヘルスチェックのコストを下げるための改善も進められています。

TCP / HTTP のヘルスチェックのパフォーマンスは、k/k#115143 で改善されました。kubelet が Probe を実行するたびにコネクションを作成していたことが原因でした。作成されたコネクションは使用後にすぐには解放されず、TIME-WAIT 状態のままデフォルトで 60 秒待ちます。これにより、Pod に短い間隔の Probe を設定している場合に、ネットワークリソース (ソケットやエフェメラルなポート、conntrack のエントリ...) を大量に消費していました。コネクションを使い回す方法で修正が進んでいましたが、思ったよりも複雑でより簡単な修正に倒れています。Probe のコネクション作成時に SO_LINGER を 1 に設定して TIME-WAIT 状態の時間を強制的に 1 秒に変更しています。Kubernetes SIG Network meeting for 20230119 で詳細な話が聞けます。この修正は既に 1.26 / 1.25 / 1.24 / 1.23 にバックポートされています。コネクションを再利用する修正に関しても k/k#116058 で実装が進められています。

gRPC のヘルスチェックのパフォーマンスも k/k#115321 で改善されました。こちらも同様にコネクションに対して SO_LINGER を 1 に設定しています。GRPCContainerProbe の機能は Kubernetes 1.26 時点でベータのため、この修正はバックポートされていません。

サービスの異常を早く検知したいという思いから periodSeconds を短く設定しがちです。ヘルスチェックを実行する側も受ける側もコストが掛かることを忘れないで下さい。ノードのスペックや Pod 数を考慮しながら余裕のある値を設定しましょう。

Liveness Probe の必要性

Liveness Probe と Readiness Probe を同じ設定にしている場合があります。それぞれの Probe の用途は異なるので、同じ設定になることはないはずです。Liveness Probe は、処理がデッドロックなどを起こして応答しなくなった場合に復旧させるための救済措置です。何となく Liveness Probe を設定している場合は、見直しましょう。

  • アプリケーションでデッドロックが発生するか分からない場合は設定せずに様子を見る
  • ステートフルなデータベース等で問題が起きた場合は、手動オペレーション or Operator を通した修復作業が必要なはずなので、Liveness Probe による無闇な再起動は避けるべき
  • どうしても設定する場合は、何の処理にも依存しないシンプルな返答を返すだけのハンドラとして実装する
  • timeoutSecondsfailureThreashold は気持ち余裕を持って設定しておく

クラウドサービスなど仮想マシン上でワークロードを動かしている場合、Noisy neighbor やホストエラーの前兆により、ノードのパフォーマンスが大幅に劣化することがあります。その場合、Pod は再起動を繰り返すことになります。劣化したパフォーマンスでも一時的にサービスを提供し続け、Readiness Probe によってサービスアウトする方が理想的なはずです。

Liveness Probe の必要性や Liveness Probe が原因で起こった障害についての以下のブログ記事が参考になります。

Startup Probe と LB

サービスの事前準備 (e.g. 初期化処理や warm up) ができるのを待つために、Startup Probe を使うことがあります。Container-native load balancing through Ingress を利用している場合、Startup Probe が考慮される保証はありません。正確に言うと、LB のヘルスチェック (= Readiness Probe) に成功すると、Startup Probe の処理中であってもリクエストが流れてきます。Startup Probe と Readiness Probe は別々に実装し、Startup Probe が完了してから Readiness Probe で Healthy を返すようにしましょう。

カスケード障害

ヘルスチェックに外部サービスやデータベースの正常性を含めるか長年議論されてきました。個人的には含めない方が良いと思っています。

ヘルスチェックに外部サービスやデータベースの正常性を含めているケースを考えます。外部サービスやデータベースに障害が起こると、依存するサービスのヘルスチェックが失敗してサービスアウトします。これにより、障害が発生したサービス以外にも影響が出てしまいます。バックエンドのサービスがいなくなると、クライアントからのリクエストはタイムアウトするまで待ちます。それよりも、バックエンドのサーバーがすぐに 503 などのレスポンスを返して、クライアント側で Exponential Backoff と Jitter などのリトライ戦略を取った方が有意義なはずです。

ヘルスチェックによるカスケード障害に関する議論やブログ記事は以下が参考になります。

Discussion