インフラエンジニアの羅針盤

インフラエンジニア1〜3年目のための技術ガイド

カオステスト設計|5分動かない理由

公開

第11回で、Podスペックの値を決めました。次に確かめるのは、その値が本当にそのとおり動くかです。

この工程には、実務でよく見る失敗が2つあります。1つは「Podを1つ消してみて、再作成されたので大丈夫」で終わること。もう1つは、ノードを停止させたのにPodが移らないのを見て「Kubernetesは自動復旧しない」と結論づけることです。

後者は誤りです。ノード障害の実験は、既定では5分以上のあいだ変化が観測されません。これは不具合ではなく仕様であり、その内訳を知らずに実験を設計すると、何度やっても誤った結論に到達します。本記事は、この種の「既定値が実験結果を先に決めてしまう」構造を潰していきます。

本記事はGoogle Cloud(GKE)を前提としますが、時間に関する既定値の大半はKubernetes本体の仕様です。GKE固有の制約は、実験を観測できる時間の上限として効いてきます。

目次
  1. 第11回で決めた値は、まだ検証していない
    1. この記事で作る成果物
  2. 実験は、仮説と受け入れ基準から書く
    1. 先に書くのは「注入の方法」ではなく「何が起きなければ合格か」
    2. 定常状態を数値で持っていないなら、先に監視を作る
    3. どのシナリオから始めるか
    4. 実験を中止する条件を、先に決める
    5. 実験で出てくるのは、不具合よりも手順の不足
    6. 実施する環境を先に決める
    7. ツールの選定には踏み込まない
  3. ノード障害の実験は、5分間は変化が観測されない
    1. 停止させてからPodが動き出すまでの内訳
    2. 注入の方法によって、通る経路が違う
    3. Podの強制削除は、最も安く、最も見落とされる
    4. DaemonSetは、この経路では退避されない
    5. 到達不能なノードの上では、Podが動き続けることがある
  4. 規模とワークロード種別で、結果が変わる
    1. 50ノード以下では、ゾーン障害の実験で退避が停止する
    2. StatefulSetは、ノードの電源断から自動復旧しない
    3. Graceful node shutdown は、既定では有効になっていない
  5. GKEの自動修復が、観測窓の上限を決める
    1. NotReady が約10分続くと、GKEがノードを作り直す
    2. Autopilotでは、この上限を外せない
    3. 自動修復は、本番でも同じ条件で発動する
  6. 負荷テストは、所要時間を計算で出す
    1. 観測できる最小の変化が、メトリクスの窓で決まる
    2. 1シナリオの最小所要時間
    3. 負荷の生成側をどこに置くか
    4. 上限に当たった状態を、必ず1度は作る
    5. 負荷テストの出力は、第11回への入力になる
    6. ストレステストは、壊れ方を見るために行う
  7. E2Eは、更新後ではなく更新中に回す
    1. 更新の順序が、テストすべき状態を決める
    2. kubeletの更新は、必ずドレインを伴う
    3. E2Eテストとして最低限そろえる項目
    4. 自動化するかどうかは、実行回数から決める
  8. テスト環境は、規模ではなく構成を揃える
    1. 揃えるものと、変えてよいもの
    2. データと外部連携先をどうするか
    3. 揃えた構成は、放っておくとずれる
    4. 費用から逆算する
  9. 【実務テンプレート】カオス・E2Eテスト計画書
  10. 次回予告

第11回で決めた値は、まだ検証していない

本記事と第11回の関係を先に整理します。片方向ではなく、往復です。

  • 第11回 → 本記事。設計した terminationGracePeriodSeconds、PodDisruptionBudget、分散制約は、Podを実際に停止させるまで検証していない値です
  • 本記事 → 第11回。Requests / Limits の算出に使う実測値を出すのは、本記事の負荷テストです

したがって工程は「第11回で設計 → 第12回で検証 → 第11回に戻って値を修正」という往復になります。片方向の工程として計画すると、測定値が出た時点で設計をやり直す時間が確保されていないことになります。スケジュールを引く段階で、この戻りを見込んでください。

この記事で作る成果物

2つです。テストシナリオマトリクス(実験ごとに仮説・注入方法・観測対象・観測窓・所要時間・受け入れ基準を並べた表)と、受け入れ基準(何が起きたら合格とするか)です。

後者を先に書けるかどうかが、この工程の成否を分けます。理由は次章で述べます。

実験は、仮説と受け入れ基準から書く

カオスエンジニアリングの枠組み(定常状態を定義し、仮説を立て、実験する)は既に広く知られています。本記事で繰り返す価値はありません。実務で抜けるのは枠組みではなく、その最後の1項目です。

先に書くのは「注入の方法」ではなく「何が起きなければ合格か」

1つのシナリオに、次の5項目を必ず書きます。

  1. 定常状態の定義。どの値がどの範囲にあれば正常か。監視で見ている値をそのまま使います(メトリクスの設計は第15回)
  2. 仮説。この障害を注入しても、定常状態は維持される
  3. 注入方法。何を、どの操作で停止させるか
  4. 観測対象と観測窓。何を、何分間見るか
  5. 受け入れ基準。合格と不合格の分岐

5が書けない実験は実施しません。結果を見ても何も判断できないためです。「やってみて様子を見る」は検証ではなく観察であり、時間だけを消費します。

第7回で「バックアップの完了条件は、取得できたことではなく復元テストに成功したこと」と決めました。実験にも同じ形で完了条件が要ります。そして4の観測窓は、次章で述べる理由により、勘で決めると必ず短すぎます。

定常状態を数値で持っていないなら、先に監視を作る

1項目めでつまずく組織が少なくありません。「正常とはどういう状態か」を数値で言えないなら、実験をしても合否を判定できません。

必要なのは、少なくとも次の3つが時系列で見えていることです。リクエストのエラー率、レイテンシのパーセンタイル、そして処理量。これらの設計は第15回の管轄ですが、本記事の実験は、その3つが揃っていることを前提にしています。

揃っていない状態で実験を始めると、観測結果が「Podは再作成された」という事実だけになります。それは利用者から見た正常性とは別の話です。Podが再作成されていても、その間ずっとエラーを返していたのなら、受け入れ基準は不合格です。順序としては、監視の設計が先です。

どのシナリオから始めるか

優先順位を「発生確率×影響度」で決めようとすると、確率の見積もりで議論が止まります。本連載を通ってきた読者には、もっと単純な基準があります。

これまでの回で「これには耐える」と宣言した障害から始めてください。設計書に書いた耐性の主張が、そのままシナリオの初期セットになります。

宣言した回宣言の内容対応するシナリオ
第3回リージョンクラスタでゾーン障害に耐えるゾーン全体の停止
第4回Spot VMの中断を前提に運用するSpotノードの強制的な回収
第11回PDBと分散制約でノードの入れ替えに耐えるノードのドレイン
第11回preStop の待機でデプロイ時のエラーをなくすPodの強制削除とローリング更新
第7回StatefulSetのデータは保護されるノードの電源断

この5つが最初のシナリオセットです。設計書に書いた主張のうち、検証していないものが残っている状態で本番へ進むのは、根拠のない宣言を運用に持ち込むことになります。実験の網羅性は「あらゆる障害を試したか」ではなく、「自分が耐えると書いたものを全部試したか」で測ってください。

実験を中止する条件を、先に決める

受け入れ基準とは別に、中止条件を書きます。「ここまで悪化したら実験を止めて復旧に切り替える」という線です。

必要な理由は単純で、実験の最中は、想定した障害と本物の障害を区別できないからです。注入したノード停止のつもりが、その裏で別のノードにも問題が起きているという状況は起こり得ます。区別できない以上、「どの状態になったら手を引くか」を、判断力の落ちていない事前の段階で決めておきます。

中止条件には、復旧の手順もセットで書きます。注入したものを元に戻す操作(taintの除去、ノードの再作成、ネットワーク遮断の解除)を、実験の前に手元に用意しておいてください。復旧手順を用意していない実験は、実施する資格がありません。

実験で出てくるのは、不具合よりも手順の不足

実際に回すと分かりますが、カオステストで見つかるものの多くはアプリケーションの不具合ではありません。「復旧に人手が要ることの発見」です。

典型例は後半で扱うStatefulSetです。ノードの電源が落ちた場合、運用者が手動で操作しない限り復旧しません。これは設計の誤りではなく、Kubernetesの仕様です。したがって実験の結論は「直す」ではなく「手順書に書く」になります。

そのため、シナリオマトリクスには「発見された手順の不足をどこへ渡すか」という列を持たせてください。渡す先は第16回のインシデント対応ランブックです。この列がないと、実験の結果は記録も手当てもされないまま終わります。

実施する環境を先に決める

本番環境で障害を注入するかどうかは、度胸の問題ではありません。受け入れ基準に「利用者に影響が出ない」と書けるかどうかで決まります。書けないなら、本番では実施しません。書けるなら、実施してよい根拠が既に手元にあることになります。

環境そのものにも制約があります。第3回で Prod(リージョン)、Stg(リージョンのミラー)、Dev(ゾーン)という3クラスタ構成を選びました。ゾーン障害の実験は、ゾーンが1つしかないDevでは成立しません。実験の種類によって、実施できる環境が決まります。

シナリオマトリクスに実施環境の列を置き、その環境で本当にその障害を注入できるかを、計画の時点で確認してください。

あわせて、誰が計画し、誰が実施し、誰が承認するかを決めます。第2回のRACIで、平時の責任分担は既に定義済みです。実験はその延長線上にあります。本番に近い環境で障害を注入する行為は、運用チームの単独判断で行うものではありません。特に、実験の中止条件を判断する権限を誰が持つのかは、実施前に合意しておいてください。実験中に「止めてよいか」を確認する相手を探すことになると、判断が遅れます。

ツールの選定には踏み込まない

カオスツールの選定は第6回のADRの管轄です。ここでは事実を1つだけ述べます。Chaos Mesh と Litmus は、いずれもCNCFのIncubatingです。どちらもGraduatedではなく、成熟度では差がつきません。第6回で確立した枠組み(成熟度で差がつかないなら別の基準で決める、そして決め手は1つに絞る)をそのまま適用してください。

そのうえで、本記事の立場を述べます。最初の数シナリオは、ツールなしで実施できます。ノードの停止、Podの削除、ネットワークの遮断は、いずれも手作業とクラウドのコンソールで注入できます。ツールが必要になるのは、実験を定期実行に組み込む段階です。1度も実験していない状態でツールの比較から始めると、選定に時間を使ったまま1つも検証が終わらないという結果になります。

ノード障害の実験は、5分間は変化が観測されない

本記事で最も重要な章です。ここを知らずに実験すると、Kubernetesの挙動を誤って理解したまま設計を進めることになります。

停止させてからPodが動き出すまでの内訳

ノードを1台停止させてから、そのPodが別のノードで動き出すまでには、複数の既定値が直列に並んでいます。

段階既定値設定項目
ノードコントローラが各ノードの状態を確認する間隔5秒--node-monitor-period
到達不能を検知し、Ready条件を Unknown にする
Unknown にしてから最初の退避要求まで5分ノードコントローラの既定
Pod側に自動付与される猶予300秒tolerationSeconds(自動付与)
退避を進める速度0.1/秒(10秒に1ノード)--node-eviction-rate
新しいPodが起動してReadyになるまで第11回で実測した起動時間

4行目を補足します。Kubernetesは、node.kubernetes.io/not-readynode.kubernetes.io/unreachable に対する tolerationSeconds を、明示的に設定していない限り自動的に300秒として付与します。公式の表現では「これらの自動付与された toleration により、Podは問題が検知されてから5分間ノードに留まる」となります。

したがって結論はこうです。「ノードを停止させたのにPodが移らない」は正常な挙動です。観測窓は5分+新しいPodの起動時間を下回らないように設計してください。5分未満で打ち切った実験は、例外なく「復旧しない」という誤った結論を出します。

この5分は短くできます。Pod側で tolerationSeconds を明示すればよいだけです。ただし、短くすると一時的なネットワークの揺らぎでもPodが移動します。公式は逆方向の例も挙げており、ローカル状態を多く持つアプリケーションでは6000秒といった長い値を設定して、分断が回復するのを待つ構成を示しています。

「速く復旧させる」と「揺らぎで動かさない」は両立しません。ワークロードごとにどちらを優先するかを決め、設計書に書いてください。既定の300秒のままにするのも判断ですが、それは「5分の停止を許容する」と決めたことと同じです。第3回で定めたSLA目標と突き合わせてください。

注入の方法によって、通る経路が違う

「ノード障害」と一言で言っても、注入の方法によってKubernetesが通る経路が変わります。同じつもりで実施して、違う結果を見ていることがあります。

注入の方法Kubernetesから見た状態観測される挙動
ノードのVMを停止するハートビートが途絶え、Ready条件が Unknown到達不能の経路。5分の待機を経て退避。kubeletは応答しない
kubeletを停止する同じく Unknown上と同様。ただしコンテナは動き続けるため、実質はネットワーク分断に近い
ノードのネットワークを遮断するUnknown分断されたノード上でPodが動き続ける(前述)
ノードを正常にシャットダウンするkubeletが検知すればNotReadygraceful node shutdown が設定されていれば猶予に従って終了。設定していなければ突然の終了
kubectl drain を実行する意図的な退避PDBが効く唯一の経路。猶予も尊重される

この表から、実験の設計に2つの示唆が出ます。

  1. PDBの効果を検証したいなら、注入方法はドレインです。VMを停止させてもPDBは効きません(第11回で確認した非自発的な中断です)。「PDBを設定したのに守られなかった」という実験結果の多くは、注入方法の選択の誤りです
  2. 猶予(terminationGracePeriodSeconds)を検証したいなら、ドレインか正常なシャットダウンです。電源断では猶予そのものが与えられません

逆に言えば、電源断のシナリオで検証しているのは、猶予やPDBではなく「猶予が与えられなかったときに何が壊れるか」です。目的が違うので、受け入れ基準も別に書きます。

Podの強制削除は、最も安く、最も見落とされる

ノード障害の話が続きましたが、最初に実施すべきは、もっと単純なシナリオです。Podを1つ削除する実験は、準備も費用もほぼゼロで、しかも第11回で設計した内容を直接試験します。

ただし、観測すべき対象を間違えないでください。「Podが再作成されたか」ではありません。それはKubernetesの基本動作であり、確かめる価値がほとんどありません。観測するのは、削除された瞬間にリクエストが落ちたかどうかです。

第11回で、Podの削除と同時に2つのことが並行して起きると述べました。EndpointSliceからの除去と、kubeletによる終了処理です。そして経路からの除去が反映されるまでの時間差を、preStop の待機で覆う設計にしました。この実験は、その待機時間が足りているかを直接測ります。

  • 受け入れ基準: 削除の前後でエラーが1件も発生しないこと
  • 不合格だった場合: preStop の待機時間が、経路からの除去が反映されるまでの実測時間を下回っている。直すのは設計(第11回)であり、アプリケーションではありません

なお、kubectl delete pod はEviction APIを経由しないためPodDisruptionBudgetをバイパスします(第11回)。したがってこのシナリオでPDBは試験されません。同じ「Podが消える」でも、削除とドレインは別の実験です。

DaemonSetは、この経路では退避されない

DaemonSetのPodには、node.kubernetes.io/unreachablenode.kubernetes.io/not-ready に対して tolerationSeconds を持たない NoExecute の toleration が付与されます。公式は、これによってDaemonSetのPodがこれらの理由で退避されることはない、と明記しています。

実験で「DaemonSetだけ残っている」という観測結果になるのは、そのためです。不具合ではありません。ログ収集エージェント、CSIのノードプラグイン、セキュリティエージェントをDaemonSetで動かしている環境(第3回でEDRの配置を検討した読者は該当します)では、この違いを受け入れ基準に書き分けてください。

到達不能なノードの上では、Podが動き続けることがある

ネットワーク分断の実験では、もう1つ知っておくべき挙動があります。ノードが到達不能な場合、APIサーバはそのノードのkubeletと通信できません。したがって「このPodを削除する」という決定を、kubeletに伝える手段がありません。

公式の記述はこうです。削除の対象になったPodは、通信が回復するまで、分断されたノード上で動き続ける可能性があります。

実験の設計に直接効きます。ネットワーク分断のシナリオでは、「新しいPodが起動したこと」と「古いPodが停止したこと」を別々に観測してください。片方だけを見ると、同じワークロードが2箇所で動いている状態を見逃します。

書き込みを伴うワークロードでは、これが最も危険な状態です。第1回の条件3で「水平にスケールできないシングルトン処理」を不採用条件に挙げた理由が、ここで実害として現れます。排他制御を外部化していないワークロードは、分断中に二重に動きます。

規模とワークロード種別で、結果が変わる

同じ実験でも、クラスタの規模とワークロードの種類によって結果が変わります。他社の事例をそのまま受け入れ基準に写せない理由が、この章です。

50ノード以下では、ゾーン障害の実験で退避が停止する

ノードコントローラの退避には、誤検知による大量退避を防ぐための保護機構があります。

  • 通常の退避レートは --node-eviction-rate の既定 0.1/秒、つまり10秒に1ノードです
  • ゾーン内の不健全なノードの割合が --unhealthy-zone-threshold の既定 0.55 以上になると、挙動が変わります
  • そのとき、クラスタが --large-cluster-size-threshold の既定 50ノード以下であれば、退避そのものが停止します
  • 50ノードを超える場合は --secondary-node-eviction-rate の既定 0.01/秒に落とされます
  • すべてのゾーンが不健全な場合、退避は行われません。コントロールプレーンとノードの間の問題と見なされるためです

ここから重要な帰結が出ます。50ノード以下のクラスタでゾーン全体を落とすと、Podは退避されません。本連載が想定する規模(第2回で置いた開発チーム4〜15)のクラスタは、多くがこの範囲に入ります。

この事実を知らずに実験すると、「マルチゾーンにした意味がない」という誤った結論に至ります。退避が止まるのは不具合ではなく、大規模な誤検知でクラスタ全体が空になるのを防ぐための設計です。

したがって、小規模クラスタにおけるゾーン障害の受け入れ基準は「Podが退避されること」ではありません。次のように置き換えます。

  • 残ったゾーンのPodだけで、定常状態が維持されること
  • そのために必要なレプリカ数が、残ったゾーンに実際に存在していること

第11回で分散制約を書いた目的が、この一点です。退避を待って移動させるのではなく、最初から分散していることで耐えます。そしてこの受け入れ基準は、第11回で topologySpreadConstraintsDoNotSchedule にしたかどうかを、そのまま試験します。ScheduleAnyway のままなら、偏った配置が許されているため、この基準を満たさない日があり得ます。

StatefulSetは、ノードの電源断から自動復旧しない

kubeletに検知されない形でノードが停止した場合(電源断、ハイパーバイザの障害など)、StatefulSetのPodは自動では復旧しません。公式が明記している挙動は次のとおりです。

  • StatefulSetのPodは、停止したノード上で Terminating のまま留まります。kubeletが応答しないため削除が完了せず、同じ名前のPodを新しいノードに作れません
  • ボリュームを使っている場合、VolumeAttachment が元のノードから削除されないため、そのボリュームを他のノードへ接続できません
  • 元のノードが復帰すればkubeletがPodを削除し、別のノードで新しいPodが作られます。復帰しなければ、この状態が続きます

復旧の手順も公式に定義されています。運用者が手動で node.kubernetes.io/out-of-service の taint をノードに付けます(Kubernetes 1.28で安定版)。これにより、対象のPodが強制的に削除され、ボリュームのデタッチが即座に実行されます。

注意点が2つあります。

  1. taintを付ける前に、ノードが本当に停止していることを確認する必要があります。再起動の途中に付けてはいけません
  2. Podが移った後、この taint は手動で外す必要があります。付けたのが人間である以上、外すのも人間です

もう1つ、知らないと危険な仕様があります。Podの削除が6分間成功せず、ノードが不健全な状態にある場合、Kubernetesはボリュームを強制的にデタッチします。公式は、この動作がCSI仕様に反する状態を作り、データ破損に至りうると明記しています。この強制デタッチは設定で無効化できます。

この実験の成果物を明確にしておきます。発見されるのは不具合ではなく、ランブックの不足です。「ノードが落ちたらどうするか」の答えが「運用者が手でtaintを付け、Podの移動を確認し、手で外す」である以上、その手順が第16回のランブックに存在しなければ、この実験は不合格です。

第7回でStatefulSetとマネージドDBを比較したとき、StatefulSetを選ぶ側の代償として運用工数を挙げました。ここがその工数の具体です。実験してはじめて、必要な手順書の分量が見えます。

Graceful node shutdown は、既定では有効になっていない

kubeletには、ノードのシャットダウンを検知してPodを正常終了させる機能があります。ただし既定では動作していません。

設定項目は shutdownGracePeriodshutdownGracePeriodCriticalPods の2つで、どちらも既定値は0であり、この状態では機能が有効になりません。両方に0でない値を設定してはじめて有効になります。有効な場合、kubeletは通常のPodを先に、クリティカルなPodを後に、2段階で終了させます。

第11回で terminationGracePeriodSeconds を算出しました。ノードのシャットダウン時にその猶予が使われるかどうかは、この設定に依存します。設定されていなければ、ノードの停止はPodにとって突然の終了になります。

実験の前に、使っているノードでこの設定がどうなっているかを確認し、シナリオマトリクスの前提欄に書いてください。「正常にシャットダウンすれば猶予は守られるはず」という前提で受け入れ基準を書くと、設定されていない環境では毎回不合格になります。

GKEの自動修復が、観測窓の上限を決める

実験は、クラウド側の自動化と競合します。障害を注入したまま観測できる時間には上限があり、それを決めているのはGKEです。

NotReady が約10分続くと、GKEがノードを作り直す

GKEのノード自動修復が発動する条件は次のとおりです。

条件閾値
ノードが NotReady を連続して報告する約10分
ノードが状態をまったく報告しない約10分
ノードの起動ディスクの空き容量が不足している約30分
Autopilotのノードが cordon されたままである約10分

修復の内容はドレインして再作成です(ノード名は維持されます)。そして公式は「GKEはドレインの完了を1時間待つ」と明記しています。完了しない場合はノードをシャットダウンして新しいノードを作成します。

実験への帰結は明確です。ノードを停止させたまま観測できるのは、約10分が上限です。それを超える観測を計画しているなら、観測しているのは自分が注入した障害ではなく、GKEの自動修復です。

前章の内訳と並べると、時間の余裕がほとんどないことが分かります。5分の待機を経てPodの退避が始まり、その数分後には自動修復が動き出します。シナリオマトリクスのタイムラインには、自動修復が発動する時刻を必ず書き込んでください。

ノードを停止させてからPodが別のノードで動き出すまでに、5秒周期の検知、5分の待機、10秒に1ノードの退避、新しいPodの起動という既定値が直列に並んでおり、観測窓はその合計より長く、約10分で発動するGKEの自動修復より短く取る必要があることを示した図
観測窓には下限と上限があります。下限は5分と起動時間の合計、上限はGKEの自動修復が発動する約10分です。

Autopilotでは、この上限を外せない

公式の記述は明確です。Autopilotのクラスタは常にノードを自動修復し、この設定を無効にすることはできません。Standardのクラスタでは新しいノードプールで既定有効ですが、無効化できます。

したがって、テスト環境をAutopilotにすると、ノード障害系の実験設計が制限されます。第3回で運用モードを選ぶとき、判断材料は費用と運用負荷が中心でした。本記事は、そこに「その環境で何を検証できるか」という材料を1つ足します。

実務上の落とし所を示します。Standardのクラスタであれば、実験用のノードプールを常設し、そこだけ自動修復を無効にします。実験のたびに設定を切り替えるのではなく、実験対象のワークロードをそのノードプールへ寄せる設計です。第4回でノードプールの構成を決めたので、そこに用途を1つ追加する形になります。

なお、自動修復とノードのアップグレードは別の機能です。第11回で「GKEのノード更新はPodDisruptionBudgetと猶予を最大1時間まで尊重する」と述べましたが、これはアップグレードの話であり、自動修復のドキュメントにPodDisruptionBudgetの記述はありません。2つを混同しないでください。

自動修復は、本番でも同じ条件で発動する

ここまで実験の制約として述べてきましたが、同じ機能は本番でも動いています。実験を設計する過程で条件を把握すること自体に、運用上の価値があります。

押さえるべき帰結は2つです。

  1. ノードは再作成されます。名前は維持されますが、中身は新しいノードです。ノード上のローカルな状態は残りません。第1回でLocal SSDについて「ノードに固定され、別ノードへ移動できず、エフェメラルである」と述べました。自動修復は、その性質が本番で表面化する経路の1つです
  2. メンテナンスのためにノードを cordon したまま放置しないでください。Autopilotでは、cordonが約10分続くと自動修復の対象になります。「あとで調べるために止めておく」という運用は成立しません

2つ目は、実験の途中にも起こります。ノードを隔離して観察するつもりで cordon したまま席を外すと、戻ってきたときにノードが作り直されています。Autopilotのクラスタで実験を計画するなら、この10分は常に意識してください。

負荷テストは、所要時間を計算で出す

「10分の負荷テストを回しました」という報告をよく見ます。その10分では、スケールインを一度も観測できていません。理由は計算で示せます。

観測できる最小の変化が、メトリクスの窓で決まる

HPAが参照するMetrics APIのCPU値は、瞬間値ではありません。カーネルの累積カウンタから求めたレートであり、計算に使った時間窓が応答の window フィールドに示されます(公式のサンプルでは30秒)。メモリは収集時点の working set です。

つまり、窓より短いスパイクは平均に埋もれ、HPAからは見えません。「瞬間的に負荷をかけたがスケールしなかった」は不具合ではありません。負荷を何秒維持するかは、この窓を基準に決めます。自分の環境の窓は、Metrics APIの応答で確認できます。

1シナリオの最小所要時間

第11回で確定したHPAの既定値と組み合わせると、必要な時間が積み上がります。

1シナリオの最小所要時間
  = 定常状態の確認
  + 負荷の維持(メトリクスの窓 + HPAの制御周期15秒 を下回らない)
  + スケールアウトの観測(新しいPodがReadyになるまで=起動時間)
  + 負荷の低下
  + スケールインの観測(安定化ウィンドウ 300秒 を待つ)

最後の行が効きます。スケールインの安定化ウィンドウは既定300秒であり、負荷を下げてから5分以上待たなければ、レプリカが減る様子を一度も観測できません。10分で打ち切ったテストは、実質的にスケールアウトだけを検証しています。

第11回で「HPAの設計対象はスケールインである」と述べました。検証の側でも、時間を食うのはスケールインです。シナリオマトリクスに所要時間の列を持たせ、上の合計を書いてください。テスト全体の計画は、その合計にシナリオ本数を掛けたものになります。ここを見積もらないまま「来週テストします」と宣言すると、必ず溢れます。

負荷の生成側をどこに置くか

見落とされやすい設計項目です。負荷をかける側をクラスタの内側に置くか、外側に置くかで、測定しているものが変わります。

置き場所利点測定への影響
クラスタの内側(Podとして動かす)準備が簡単。ネットワークの経路が短い負荷生成側がノードのCPUとメモリを消費し、測定対象と資源を奪い合います。ResourceQuota(第4回)も消費します
クラスタの外側資源の競合がない。利用者に近い経路で測れるロードバランサとネットワークが経路に入ります。下り方向のデータ処理には費用が発生します(第1回のCloud NATの単価)

判断基準はこうです。Requests / Limits を決めるための測定なら、資源の競合を避けたいので外側に置きます。一方、公開経路まで含めた耐性を見るなら、外側でなければ意味がありません(第3回で決めたGateway経由の経路を通らないためです)。内側に置いてよいのは、経路を除いたアプリケーション単体の挙動を見る場合に限られます。

内側に置く場合は、負荷生成Podを測定対象と別のノードに寄せてください。第11回で扱ったノードアフィニティやtaintで分離できます。同じノードに同居させると、測定対象のCPU使用率が負荷生成側の影響を含んだ値になり、そのままRequestsに反映すると過大な値になります。

上限に当たった状態を、必ず1度は作る

負荷テストで最も検証されないまま本番へ行くのが、HPAが maxReplicas に達した状態です。

第11回で maxReplicas を、3つの天井(第3回のIPアドレス、第4回のResourceQuota、ノードの空き容量)のうち最も低いものから決めました。その上限に達したとき、システムがどう振る舞うかは設計書に書かれていません。レイテンシが伸びるのか、エラーを返すのか、キューが伸びるのか。

この状態を1度作り、そのときの挙動を受け入れ基準として文書化してください。本番でこの状態になるのは、多くの場合、最も注目が集まっている時間帯です。その日に初めて挙動を知るのは避けたいところです。

あわせて、上限に達したことが検知できるかも確認します。レプリカ数が maxReplicas に張り付いている状態は、アラートの対象として設計されているべきです(設計は第15回)。検知できないなら、上限に当たったまま数日気づかないという事態が起こります。

負荷テストの出力は、第11回への入力になる

この負荷テストで得られるのは、コンテナごとのCPUとメモリの時系列です。その数値をどうRequests / Limits に変換するかは第11回の管轄です。

したがって、負荷テストの計画には「測定して終わり」ではなく、どの値を誰が受け取り、どこに書くかまでを含めてください。第11回のPodスペックテンプレートには「根拠」の欄があり、測定日と測定条件、使用したパーセンタイルを書くことになっています。その欄を埋めるのが、本記事の負荷テストの目的の1つです。

ストレステストは、壊れ方を見るために行う

負荷テストとストレステストは目的が違います。

種類問い受け入れ基準の形
負荷テスト想定内の負荷で、設計どおりに動くか定常状態が維持されること
ストレステスト想定を超えたとき、どう限界に達するか限界の現れ方が予測どおりであること

ストレステストの受け入れ基準は「限界に達しないこと」ではありません。たとえば「上限に達したPodは Pending に留まり、既に動いているPodは応答を続ける」といった形で、限界の現れ方を先に宣言し、そのとおりになるかを見ます。

ここで確かめられるのは、どの上限に最初に当たるかです。候補は第3回のIPアドレス、第4回のResourceQuota、そしてノードの空き容量です。第11回で maxReplicas を決めるとき、3つの天井のうち最も低いものが実際の限界になると述べました。ストレステストは、その想定が正しいかを実測で確かめる場です。想定と食い違ったら、直すのは設計側です。

E2Eは、更新後ではなく更新中に回す

バージョンアップの動作確認というと、更新が終わってから一通り触る作業を思い浮かべます。それでは遅すぎます。問題が出やすいのは更新中にしか存在しない状態であり、そこで見つけたのでは戻る判断が難しくなります。

更新の順序が、テストすべき状態を決める

Kubernetesのコンポーネントには更新順序が定められています。

kube-apiserver
  → kube-controller-manager / kube-scheduler / cloud-controller-manager
    → kubelet
      → kube-proxy

そして許容されるバージョンの差(skew)も定義されています。kubelet は kube-apiserver より最大3マイナー古くてよく、kube-proxy も同様です。kubectl は前後1マイナーです。GKEも同じ順序で、コントロールプレーンを先に更新します。

ここから出る結論が本章の主張です。「コントロールプレーンだけが新しい」という状態は、更新のたびに必ず発生します。一時的な例外ではなく、正規の手順の一部です。その状態でアプリケーションが動くことを確認しないまま更新を始めると、問題が出たときに前へも後ろへも進めなくなります。

したがってE2Eを回す位置は3つあります。更新前、コントロールプレーンだけを更新した時点、ノードの更新完了後です。2番目を飛ばしている計画が大半です。

kubeletの更新は、必ずドレインを伴う

公式は「kubeletのマイナーバージョンをその場で更新することはサポートされない」と明記しています。更新の前にPodをドレインする必要があります。

つまり、バージョンアップは計画された大規模なノード障害です。そして第11回で設計した3つが、全ノードで順番に、同時に効きます。

  • PodDisruptionBudgetがドレインの速度を制限します
  • terminationGracePeriodSecondspreStopが、Podごとの終了の作法を決めます
  • 分散制約が、ドレインされたPodの新しい配置先を決めます

そのため、更新時のE2Eは新しく設計する必要がありません。カオステストで検証済みの項目を、全ノードに対して連続で回すものとして設計できます。受け入れ基準は次の3つです。

  1. ドレイン中にエラー率が定常状態の閾値を超えないこと
  2. PodDisruptionBudgetによってドレインが停止しないこと。第11回で述べたとおり、GKEはPodDisruptionBudgetを最大1時間まで尊重し、その後は強制的に排除します。1時間待たされた末に強制排除されるのは合格ではありません
  3. 全ノードの入れ替えが、想定した時間内に完了すること

3つ目の見積もりは、ノード1台あたりのドレイン時間 × 台数で概算できます。ドレイン時間には第11回の terminationGracePeriodSeconds が含まれます。猶予を長く取った設計は、更新に必要な時間をそのまま長くします。これは第14回で更新計画を立てるときの入力になります。

Kubernetesの更新ではコントロールプレーンが先に上がるため、コントロールプレーンだけが新しくノードが旧いという状態が毎回必ず発生し、そこでE2Eを回す必要があること、およびノード更新の区間は全ノードのドレインを伴う計画された大規模なノード障害であることを示した図
E2Eを回す位置は3つあります。中央の状態は許容された正規の構成であり、ここを飛ばした計画が大半です。

E2Eテストとして最低限そろえる項目

アプリケーションの利用者操作は開発チームが定義します。基盤側が定義すべきなのは、クラスタの機能が生きているかを確かめる項目です。

  • Serviceによる名前解決と疎通
  • Ingress / Gateway 経由の外部からの疎通(第3回で決めた公開方式)
  • PersistentVolumeのマウント(第7回で決めたStorageClass)
  • Secretの取得(第5回で決めたシークレット管理の経路)
  • NetworkPolicyによる拒否が効いていること(第5回。通ることの確認だけでなく、通らないことの確認
  • Podの再作成を伴う操作(デプロイとスケール)

5番目を落とす計画が多くあります。更新でポリシーエンジンやCNIが動かなくなった場合、通信は「通る」方向に壊れます。疎通確認だけでは検出できません。

なお、どのAPIが非推奨になったかの調査は第14回の管轄です。本記事で扱うのは、確認する対象の一覧までです。

自動化するかどうかは、実行回数から決める

E2Eテストは自動化すべきだ、という主張は正しいのですが、最初から自動化しようとすると、1度も実行しないまま計画だけが残ります。

判断は実行回数で決めてください。Kubernetesのマイナーバージョンは年に数回上がり、そのたびに同じ項目を確認します。加えて、更新は3つの時点(更新前、コントロールプレーンのみ、ノード完了後)で回します。1回の更新あたり3回、年に数回の更新となれば、手作業のチェックリストは早い段階で割に合わなくなります。

現実的な進め方は、最初の1回は手動のチェックリストで実施し、そこで安定した項目から順に自動化することです。手動で1度も通していない項目を自動化すると、テストコードの不具合と本番の不具合を切り分けられません。

自動化した場合の置き場所は、第9回で設計したパイプラインです。更新のたびに人が思い出して実行するのではなく、更新の手順(第14回)から呼ばれる形にしてください。思い出して実行する運用は、忙しい時期に飛ばされます。

テスト環境は、規模ではなく構成を揃える

「本番同等の環境が必要です」と言うと、多くの場合は台数の話だと受け取られ、費用の議論で止まります。揃えるべきは台数ではありません。

揃えるものと、変えてよいもの

第3回で「ミラーの定義は構成の同一性であって、規模の同一性ではない」と確定しました。テスト環境にもそのまま適用します。

揃える(後から変えられない・環境を分けないと分離できない)変えてよい
Kubernetesのバージョンノードの台数
max Pods per node(作成後変更不可・第3回)レプリカ数
ゾーン数とトポロジー(リージョンかゾーンか・第3回)データ量
ノードのマシンタイプの種類外部連携先(模擬に置き換える)
CNI・アドオン・ポリシーエンジン(第5回・第6回)
StorageClassとCSIドライバ(第7回)

左の列を見てください。第3回で「Namespaceでは分離できない」と整理した項目と、ほぼ一致します。分離できないものは、環境を分けたうえで揃えるしかありません。逆に言えば、右の列を本番に合わせても、検証の精度はほとんど上がりません。

この表は、費用の議論を技術の議論に戻すための道具でもあります。「本番同等にできないからテストできない」と言われたときに、揃えるべき項目を1つずつ確認すれば、多くは台数以外の話であり、費用はさほど増えません。

データと外部連携先をどうするか

構成を揃えても、この2つを決めていないとテストが成立しません。

データ。本番データをそのままコピーする案は、第5回で定めたガバナンスと衝突します。個人情報を含むデータをテスト環境へ複製すると、本番と同じ保護を全環境に適用しなければならなくなります。選択肢はマスキングか合成データですが、どちらを採るにしてもデータ量は本番に近づける必要があります。件数が2桁少ないデータでは、インデックスの効き方もメモリの使用量も別物になり、第11回へ渡す測定値としては使えません。

外部連携先。模擬に置き換えるのが一般的ですが、置き換えた瞬間に検証できなくなるものがあります。タイムアウトとリトライの挙動です。常に即座に正常応答を返す模擬を相手にすると、遅延や失敗に対する備えが動く場面が1度も現れません。

したがって基準はこうなります。模擬に置き換えるのは、応答の内容が問題になるものだけ。応答の遅さや失敗が問題になるものは、遅延と失敗を注入できる模擬にする。後者は、それ自体がカオステストのシナリオになります(依存先の遅延を注入し、自分たちのタイムアウト設定が想定どおり働くかを見る)。

揃えた構成は、放っておくとずれる

テスト環境を本番に合わせて作っても、そこから先は本番だけが変わっていきます。アドオンが追加され、ノードプールが増え、バージョンが上がる。半年後には、揃えたはずの左の列が揃っていません。

対策は第8回で既に決めています。同じIaCコードから、パラメータの差だけで両方の環境を作ることです。環境ごとに別のコードを持つと、片方だけに入った変更が検出されません。そして第8回で扱ったドリフト検出(変更がないときの定期的な plan)を、テスト環境に対しても回します。

クラスタの上に乗るワークロード側も同じです。第10回で決めた共通Chartを使い、環境ごとの差はvaluesだけにしてください。テスト環境だけ別のマニフェストを手で書いている状態では、揃っているのは見た目だけになります。基盤は第8回のIaC、ワークロードは第10回のChart。どちらも「同じ定義から、差分だけで作る」という同じ原則です。

もう1つ、運用ルールとして書いておくべき項目があります。本番に加える変更は、テスト環境に先に入れる。順序を逆にすると、テスト環境は永久に本番を追いかける側になり、追いつかないまま実験の前提が崩れます。

費用から逆算する

第1回で確定したK8s税を再掲します。東京リージョンのリージョンクラスタで1環境あたり月約$160、Prod / Stg / Dev の3環境で月約$407(Devはゾーンクラスタで無料枠が使えるため)。これはノード台数を除いた固定費です。

したがって、台数まで本番に揃えると増えるのは変動費であり、そこが費用の大半を占めます。判断はこうなります。

  • 常設するのは、構成が同一で規模の小さい環境(第3回のStg)
  • 本番同等の規模が必要な実験だけ、一時的に環境を作る

後者を成立させる前提が、第8回のIaCです。第8回で「IaCの完了条件はapplyが通ることではなく、空のプロジェクトから再現できること」と決めました。その完了条件は、この一時環境を作るためのものでもあります。再現できないIaCしかないなら、本番規模の実験は最初から選択肢に入りません。

そして第8回で決めたとおり、再現テストの手順には削除まで含めます。一時環境は作った時点ではなく、消した時点で完了です。放置された検証環境は、使われないままK8s税を払い続けます。

【実務テンプレート】カオス・E2Eテスト計画書

セクション1: 前提

  • 対象システム/第3回の環境構成(クラスタとゾーン)/実験を実施する環境
  • 第11回で設計した耐障害性の値(猶予・PDB・分散制約・レプリカ数)

セクション2: 既定値の確認欄(実験の前に埋める。ここが埋まっていないと受け入れ基準を書けません)

  • Pod側の tolerationSeconds(明示しているか。していなければ300秒
  • ノードコントローラの待機(既定5分
  • クラスタのノード数が50以下かどうか(ゾーン障害時に退避が停止する条件)
  • GKEのノード自動修復(有効か。有効なら観測窓の上限は約10分
  • shutdownGracePeriod の設定有無

セクション3: テストシナリオマトリクス(1行1シナリオ)

  • ID/分類(ノード障害・ネットワーク分断・Pod強制Kill・負荷・ストレス・更新)
  • 仮説/注入方法/観測対象観測窓(分)所要時間
  • 受け入れ基準/実施環境/発見された手順の不足の受け渡し先(第16回)

記入例を1行示します。この粒度で埋まっていれば、実施者が誰でも同じ実験になります。

項目記入例
ID / 分類CH-03 / ノード障害
仮説ノードを1台停止させても、残りのゾーンのPodでエラー率が0.1%を超えない
注入方法対象ノードのVMを停止する(ドレインではない。PDBは効かない
観測対象エラー率、レイテンシのp95、対象Podの状態遷移、新しいPodのReady時刻
観測窓12分(5分の待機+退避+起動時間。GKEの自動修復が約10分で発動するため、その時刻も記録する。下限の合計が10分を超えるこの例では、自動修復を無効化した実験用ノードプールで実施する)
所要時間25分(前後の定常状態の確認を含む)
受け入れ基準エラー率が0.1%以下。Podの移動は5分後に始まってよい(それより早いことは求めない)
中止条件エラー率が1%を超えた時点で停止し、ノードを再作成する
実施環境Stg(第3回のリージョンクラスタ・本番のミラー構成)
手順不足の受け渡し先第16回のランブック(該当があれば記入)

受け入れ基準の行に注目してください。「Podの移動は5分後に始まってよい」と明示しています。これを書いておかないと、実施者が「5分も止まっていた」と報告し、設計どおりの挙動が不具合として扱われます。

セクション4: 負荷テスト計画

  • 負荷の形(定常・スパイク・段階)/維持時間(メトリクスの窓を下回らない
  • スケールインの観測に確保した5分以上の待機
  • 測定する値/その値を渡す先(第11回のテンプレートのどの欄か)

セクション5: 更新時E2E

  • コントロールプレーンだけを更新した状態で確認する項目
  • ドレイン中に確認する項目
  • 受け入れ基準(エラー率/PDBでドレインが停止しないこと/全ノード入れ替えの所要時間)

セクション6: テスト環境

  • 揃える項目の一覧/変えてよい項目/常設か一時作成か
  • 一時環境の削除手順/月額の見積もり

セクション7: 実施記録

  • 実施日/結果(合格・不合格)/発見された手順の不足
  • 反映先(第11回の値/第16回のランブック/第14回の更新計画)

セクション8: 見直し

  • シナリオを再実行するトリガー。日付ではなく条件で書く(Kubernetesのマイナーバージョン更新、ノードプール構成の変更、レプリカ数やクラスタ規模の大幅な変更、第11回の値の変更)

次回予告

設計を検証しました。次に決めるのは、それを本番のトラフィックにどう乗せ換えるかです。

次回の第13回「既存システムからの本番移行・トラフィック切り替え計画書」では、移行戦略の選択(Big Bang / Strangler Fig / Blue-Green)、DNSによる段階的なトラフィック移行、Go-Live判定基準、そして致命的な障害が起きたときの旧環境への即時ロールバック手順を扱います。

接続を明示します。本記事のテストは繰り返す検証、第13回の判定は一度きりの意思決定です。目的が違うため、同じ試験を流用しても合否の意味が変わります。そして本記事で作った受け入れ基準は、そのまま第13回のGo-Live判定の材料になります。基準を持たないまま移行日を迎えると、判断は勘になります。

前の記事
次の記事