設定と動作のあいだ
一言でいうと
プローブ・Job・QoS・設定の注入は、すべてkubeletが行う仕事です。kubeletがなければ、YAMLを書く練習はできても、それが何をするのかを最後まで見ることはできません。
なぜ重要なのか
CKADが問うのは「アプリケーションがクラスター上でどのように生きていくか」です。プローブ、Job、QoS、設定の注入は、すべてその話です。
ところが、前のモジュールが動いている疑似クラスターにはkubeletがないため、そのどれも実際には起こりません。プローブを設定しても動かず、Jobを作っても完了せず、ConfigMapはマウントされません。
3つのプローブの関係
| プローブ | 何を問うか | 失敗すると |
|---|---|---|
startupProbe |
起動が終わったか | 残りの2つを後回しにし続けます |
livenessProbe |
生きているか | コンテナを作り直します |
readinessProbe |
リクエストを受けられるか | エンドポイントから外します |
startupProbeが成功するまで、残りの2つはそもそも開始されません。これがないと選択肢は2つしかありません。
- livenessを緩くする → 本当に死んだときに長く放置されます
- livenessを細かくする → 起動中に死んで、永遠に再起動を繰り返します
Jobの2つの値
completionsは何回成功する必要があるか、parallelismはいくつを同時に動かすかです。混同すると、Jobがいつまでも終わらなかったり、必要以上にリソースを使ったりします。
backoffLimitは再試行の間隔が指数的に増えていくため、既定値の6では、あきらめるまでに数分かかります。その間、失敗したPodがたまり続けます。
QoSは指定するものではない
qosClassフィールドを直接書くことはできません。requestsとlimitsの組み合わせからkubeletが決めます。そして、ノードのメモリが足りなくなると、BestEffortから追い出されます。
設定の注入はいつ反映されるのか
ConfigMapやSecretを変更したとき、Podがすぐにそれを見るかどうかは、どう注入したかで決まります。環境変数として入れた値は、コンテナの起動時に一度だけコピーされるため、その後の変更は決して反映されません。Podを作り直す必要があります。一方、ボリュームとしてマウントしたファイルは、kubeletが定期的に更新するので、しばらくすると新しい内容に変わりますが、アプリケーションがそのファイルを読み直さなければ何の意味もありません。
そのため、実務でよく使われる方法は2つあります。1つは、設定内容のハッシュをPodテンプレートのアノテーションに入れておき、設定が変わるとテンプレートも変わって、ローリングアップデートが自然に起きるようにする方法です。もう1つは、アプリケーションがファイルの変更を検知して読み直すようにする方法です。前者のほうがはるかに単純で、元に戻すのも簡単なので、ほとんどの場合はこちらが優れています。
もう1つ知っておきたいのが、存在しないものを参照したときの違いです。環境変数として存在しないConfigMapを参照すると、コンテナを作れずCreateContainerConfigErrorが表示されますが、ボリュームとして参照するとマウントが終わらず、PodはContainerCreatingで止まります。同じタイプミスなのに症状が違うので、この対応を知っておくと調査時間が大幅に減ります。
プローブの値は何を見て決めるのか
プローブを設定すること以上に、どんな値を入れるかが実際には難しい部分です。既定値のまま使ってサービスを自分で殺してしまうことは珍しくないので、各値が何を変えるのかを知っておく必要があります。
| 値 | 意味 | 誤って設定すると |
|---|---|---|
periodSeconds |
どのくらいの頻度で問い合わせるか | 短いとアプリに負担がかかり、長いと障害の検知が遅れます |
timeoutSeconds |
応答をどのくらい待つか | 既定は1秒なので、負荷がかかると正常なアプリが失敗と判定されます |
failureThreshold |
何回連続で失敗したら対処するか | 1だと一時的な遅延でも再起動します |
最も事故を起こしやすい組み合わせは、livenessのタイムアウトが短いことです。負荷が集中して応答が遅くなるとプローブが失敗し、コンテナが再起動し、残ったPodに負荷が集中してそちらも遅くなり、結局すべてが再起動を繰り返します。負荷が原因の遅延を再起動で解決しようとするため、状況が自ら悪化します。そのため、livenessは寛容に、readinessは敏感に設定するのが原則です。トラフィックから一時的に外れるのは元に戻せますが、再起動は元に戻せません。
何を確認するのかも重要です。livenessはプロセスが自力で回復できない状態かどうかだけを見るべきなので、依存するデータベースに届くかどうかまで確認してはいけません。データベースが一時的に不安定になったときにアプリケーションのPodがすべて再起動し、そうなると接続が一斉に再び押し寄せて状況がさらに悪化します。依存先の確認はreadinessの役割です。そちらで失敗してもトラフィックが外れるだけで、データベースが戻れば静かに復帰します。
起動が遅いアプリでstartupProbeを使うときは、failureThreshold × periodSecondsがそのまま許容される起動時間になる、という点だけ覚えておけば十分です。30秒ごとに20回なら10分まで待ってくれるという意味で、その間に起動できなければ、そこから再起動が始まります。
実務で本当に大切なこと
起動が遅いアプリには、startupProbeを答えとして使います。これがないと選択肢は2つしかありません。livenessを緩くして、本当に死んだときに長く放置するか、細かくして起動中に殺し、永遠に再起動するかです。3つ目のプローブは、このジレンマをなくすために生まれました。
backoffLimitの既定値6は、思ったより長いです。再試行の間隔が指数的に増えるため、あきらめるまでに数分かかり、その間に失敗したPodがたまり続けます。すばやく失敗すべきJobは、値を下げる必要があります。
QoSは書くものではなく、付けられるものです。requestsとlimitsの組み合わせでkubeletが決め、ノードのメモリが足りなくなるとBestEffortから追い出されます。重要なワークロードにrequestsを書かないのは、「先に殺してもよい」と書くのと同じです。
次のラボで、これらをすべて動作で確認します。