調整が二度走った - リースの期限切れと引き継ぎ、そして障害の診断
目標
Leaseの5つのフィールドを自分で扱いながら、更新・期限切れ・引き継ぎを作ってみて、リーダー選出に必要な最小権限を合わせたうえで、リーダーが2つのときに実際に何が起きるかをフィールドマネージャーの衝突で証明し、運用チェックリストにまとめます。
なぜ重要なのか
Operatorを2つ以上起動する理由は、可用性です。ところが、調整ループはクラスターの状態を変更するコードなので、同時に2つが動いてはいけません。そのため、Kubernetesはごく小さなオブジェクト1つに「今誰がリーダーか」を書いておき、リーダーが定期的に更新して、生きていることを知らせます。ここで重要なのは、このオブジェクトに「期限切れ」のようなフィールドがないという事実です。生きているかどうかは、renewTime + leaseDurationSecondsを今と比べる計算で決まり、その計算は、各インスタンスが自分の時計で行います。時計がずれたり、APIサーバーの応答が遅れたりすると、2つのインスタンスが同時に、自分がリーダーだと信じる区間が生まれます。その区間に、2つの調整ループが同じリソースを作ろうとすると何が起きるのか、そしてその痕跡をどこで探すのかが、このラボの主題です。
ステップ
- ネームスペース
op-leaderを作成し、/root/op-leader/lease-widget.yamlに、coordination.k8s.io/v1のLeasewidget-operatorを書いてください。holderIdentityはwidget-operator-0、leaseDurationSecondsは15、acquireTimeとrenewTimeは現在の時刻、leaseTransitionsは0です。適用した後、kubectl -n op-leader get lease widget-operator -o yamlの出力を/root/op-leader/lease-initial.yamlに保存してください。 - リーダーがする仕事を、手で3回やってみてください。
renewTimeだけを現在の時刻に更新するpatchを、1秒間隔で3回実行し、そのたびに更新されたrenewTimeを/root/op-leader/renew-log.txtに1行ずつ残します(3行)。holderIdentityとleaseTransitionsは触りません。 - Leaseをさらに2つ作成してください。
/root/op-leader/lease-stale.yamlはstale-operator(holderstale-operator-0、leaseDurationSeconds15、renewTimeを10分前にする)、/root/op-leader/lease-fresh.yamlはfresh-operator(holderfresh-operator-0、leaseDurationSeconds86400、renewTimeは現在)です。/root/op-leader/leases.tsvに、<임차이름><탭><기대: EXPIRED 또는 LIVE>(プレースホルダーはLease名、タブ、期待値(EXPIREDまたはLIVE)です)の2行を書き、/root/op-leader/expiry.shが、各LeaseのrenewTime + leaseDurationSecondsを今と比べて分類した後、合っていればOK …を、間違っていればMISMATCH …を標準出力にだけ出力し、1行でも間違っていれば0ではないコードで終了するようにしてください。出力を/root/op-leader/expiry.txtに保存します。 widget-operatorのLeaseを、ほかのインスタンスが引き継いだものとして、1回のpatchで4つの値を変更してください。holderIdentityをwidget-operator-1に、acquireTimeとrenewTimeを現在の時刻に、leaseTransitionsを1にします。/root/op-leader/takeover.txtに4行を残してください。before-holder=、after-holder=、before-transitions=、after-transitions=です。- 秒単位の時刻で更新を試みてください。
renewTimeを2026-01-01T00:00:00Zのように、小数点のない値でpatchします。結果を/root/op-leader/microtime-error.txtに集めてください。1行目はpatch-rc=<종료 코드>(プレースホルダーは終了コードです)で、その下にサーバーが出した文をそのまま貼り付けます。 /root/op-leader/rbac.yamlに3つのオブジェクトを入れて適用してください。ServiceAccountwidget-operator、Roleleader-election(coordination.k8s.ioグループのleasesに、get・create・updateだけ)、そして、その2つをつなぐRoleBindingleader-electionです。そのあと、kubectl auth can-i <동사> leases.coordination.k8s.io --as=system:serviceaccount:op-leader:widget-operator -n op-leader(プレースホルダーはverbです)を、get・create・update・deleteの4回実行して、結果を/root/op-leader/rbac-check.txtに、<동사>=<yes 또는 no>(プレースホルダーはverbと、yesまたはnoです)の4行で保存してください。- 2つのインスタンスが同時に、自分がリーダーだと信じている状況を再現してください。
/root/op-leader/child-a.yamlにConfigMaporder-1(ネームスペースop-leader、data.phaseはA)を書き、kubectl apply --server-side --field-manager=widget-operator-0で適用します。そのあと、/root/op-leader/child-b.yamlに同じ名前のConfigMapを、data.phaseはBとして書き、--field-manager=widget-operator-1で適用してみてください。結果を/root/op-leader/split-brain.txtに集めてください。apply-b-rc=<종료 코드>、サーバーが出した文、そして最後の行にfinal-phase=<지금 값>です(プレースホルダーは順に、終了コードと、現在の値です)。 /root/op-leader/lease-audit.shを作成してください。op-leaderのすべてのLeaseを、<이름> holder=<홀더> transitions=<전환 횟수> duration=<임차 길이>(プレースホルダーは順に、名前、ホルダー、遷移回数、リース期間です)の1行ずつで、ソートして標準出力にだけ出力します(年齢や時刻は入れません)。出力を/root/op-leader/lease-audit.txtに保存し、/root/op-leader/runbook.txtに運用ルールを書いてください。--leader-elect-lease-duration・--leader-elect-renew-deadline・--leader-elect-retry-periodの3つの値の名前がすべて出てくる必要があり、更新期限がリース期間より短くなければならない理由と、遷移回数が増え続けるときに何を疑うべきかが入っている必要があります。
参考
acquireTimeとrenewTimeはMicroTimeなので、小数点以下6桁が必要です。- 更新は
renewTimeだけを変更し、引き継ぎはホルダー・acquireTime・leaseTransitionsを一緒に変更します。 - リーダー選出に必要なverbは、
get・create・updateの3つだけです。 kubectl auth can-i --as=で、ほかのサブジェクトの権限を代わりに問い合わせられます。- よくあるミス: 更新するときに
leaseTransitionsも一緒に上げて、遷移回数を無意味にしてしまうことです。 - よくあるミス: リースが期限切れかどうかを、オブジェクトのどこかのフィールドから探そうとすることです。計算する必要があります。
- 参考: https://kubernetes.io/docs/concepts/architecture/leases/
- 参考: https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/lease-v1/
- 参考: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/
Lease1枚がリーダーを決める
ネームスペースop-leaderを作成し、/root/op-leader/lease-widget.yamlに、coordination.k8s.io/v1のLease widget-operatorを書いてください。holderIdentityはwidget-operator-0、leaseDurationSecondsは15、acquireTimeとrenewTimeは現在の時刻、leaseTransitionsは0です。適用した後、kubectl -n op-leader get lease widget-operator -o yamlの出力を/root/op-leader/lease-initial.yamlに保存してください。
2つの時刻のフィールドは、普通のTimeではなくMicroTimeなので、小数点以下6桁が必要です(例: 2026-09-17T13:37:24.807518Z)。GNU dateなら、date -u +%Y-%m-%dT%H:%M:%S.%6NZですぐに作れます。leaseTransitionsは「リーダーが変わった回数」なので、最初は0です。
リーダーは休まず更新する
リーダーがする仕事を、手で3回やってみてください。renewTimeだけを現在の時刻に更新するpatchを、1秒間隔で3回実行し、そのたびに更新されたrenewTimeを/root/op-leader/renew-log.txtに1行ずつ残します(3行)。holderIdentityとleaseTransitionsは触りません。
更新は「私はまだ生きている」というシグナルにすぎないため、ホルダーも遷移回数も変わりません。遷移回数が更新のたびに上がるなら、それはリーダーが毎回変わっているという意味で、運用ではアラートをかけたくなる状態です。3行が時間順に大きくなっているか、確認してください。
生きているかどうかは、計算で決める
Leaseをさらに2つ作成してください。/root/op-leader/lease-stale.yamlはstale-operator(holder stale-operator-0、leaseDurationSeconds 15、renewTimeを10分前にする)、/root/op-leader/lease-fresh.yamlはfresh-operator(holder fresh-operator-0、leaseDurationSeconds 86400、renewTimeは現在)です。/root/op-leader/leases.tsvに、<임차이름><탭><기대: EXPIRED 또는 LIVE>(プレースホルダーはLease名、タブ、期待値(EXPIREDまたはLIVE)です)の2行を書き、/root/op-leader/expiry.shが、各LeaseのrenewTime + leaseDurationSecondsを今と比べて分類した後、合っていればOK …を、間違っていればMISMATCH …を標準出力にだけ出力し、1行でも間違っていれば0ではないコードで終了するようにしてください。出力を/root/op-leader/expiry.txtに保存します。
Leaseオブジェクトには、「期限切れ」のようなフィールドがありません。各候補が、自分の時計で計算するだけです。そのため、時計がずれると、同じオブジェクトを見ても判定が分かれます。計算は、date -u -d "<시각>" +%s(プレースホルダーは時刻です)で秒に変換して足せばよいです。
引き継ぎは遷移回数に表れる
widget-operatorのLeaseを、ほかのインスタンスが引き継いだものとして、1回のpatchで4つの値を変更してください。holderIdentityをwidget-operator-1に、acquireTimeとrenewTimeを現在の時刻に、leaseTransitionsを1にします。/root/op-leader/takeover.txtに4行を残してください。before-holder=、after-holder=、before-transitions=、after-transitions=です。
引き継ぎと更新を分けるのが、acquireTimeとleaseTransitionsです。更新はrenewTimeだけが動きますが、引き継ぎはホルダーが変わるので、いつ取得したかを新しく書き、遷移回数を1つ上げます。この数値を見守れば、リーダーがどれだけ頻繁に変わっているかがわかります。
時刻の形式1つが、更新を阻む
秒単位の時刻で更新を試みてください。renewTimeを2026-01-01T00:00:00Zのように、小数点のない値でpatchします。結果を/root/op-leader/microtime-error.txtに集めてください。1行目はpatch-rc=<종료 코드>(プレースホルダーは終了コードです)で、その下にサーバーが出した文をそのまま貼り付けます。
このフィールドはTimeではなくMicroTimeです。形式が違うと、値ではなくパースで阻まれますが、エラーの文に、期待する形式がそのまま書かれています。コントローラーを自分で組んでいて、標準ライブラリのデフォルトの形式をそのまま使うと、この場所で阻まれて、更新がまるごと失敗します。
リーダー選出に必要な最小権限
/root/op-leader/rbac.yamlに3つのオブジェクトを入れて適用してください。ServiceAccount widget-operator、Role leader-election(coordination.k8s.ioグループのleasesに、get・create・updateだけ)、そして、その2つをつなぐRoleBinding leader-electionです。そのあと、kubectl auth can-i <동사> leases.coordination.k8s.io --as=system:serviceaccount:op-leader:widget-operator -n op-leader(プレースホルダーはverbです)を、get・create・update・deleteの4回実行して、結果を/root/op-leader/rbac-check.txtに、<동사>=<yes 또는 no>(プレースホルダーはverbと、yesまたはnoです)の4行で保存してください。
リーダー選出に必要なverbは、3つだけです。読み、なければ作り、定期的に更新します。削除する必要はありません。watchも必要ありません。候補は、監視ではなく定期的な取得で、期限切れを確認するからです。権限を広く与えると、誤ってほかのLeaseを削除する経路が開きます。
リーダーが2つなら、同じフィールドをめぐって争う
2つのインスタンスが同時に、自分がリーダーだと信じている状況を再現してください。/root/op-leader/child-a.yamlにConfigMap order-1(ネームスペースop-leader、data.phaseはA)を書き、kubectl apply --server-side --field-manager=widget-operator-0で適用します。そのあと、/root/op-leader/child-b.yamlに同じ名前のConfigMapを、data.phaseはBとして書き、--field-manager=widget-operator-1で適用してみてください。結果を/root/op-leader/split-brain.txtに集めてください。apply-b-rc=<종료 코드>、サーバーが出した文、そして最後の行にfinal-phase=<지금 값>です(プレースホルダーは順に、終了コードと、現在の値です)。
サーバーサイドapplyは、フィールドごとに「誰がこの値を管理しているか」を記録しています。ほかのマネージャーが同じフィールドを別の値で書こうとすると、APIサーバーは黙って上書きせず、衝突を知らせてくれます。リーダー選出が崩れたときに、2つの調整ループがお互いの結果を消してしまうことを、この仕組みが目に見えるようにしてくれます。
チェックリストと運用ルールにまとめる
/root/op-leader/lease-audit.shを作成してください。op-leaderのすべてのLeaseを、<이름> holder=<홀더> transitions=<전환 횟수> duration=<임차 길이>(プレースホルダーは順に、名前、ホルダー、遷移回数、リース期間です)の1行ずつで、ソートして標準出力にだけ出力します(年齢や時刻は入れません)。出力を/root/op-leader/lease-audit.txtに保存し、/root/op-leader/runbook.txtに運用ルールを書いてください。--leader-elect-lease-duration・--leader-elect-renew-deadline・--leader-elect-retry-periodの3つの値の名前がすべて出てくる必要があり、更新期限がリース期間より短くなければならない理由と、遷移回数が増え続けるときに何を疑うべきかが入っている必要があります。
チェックリストは、「何を見れば異常なのか」を縮めたものです。ホルダーがずっと同じなのに遷移回数だけが増えているなら、引き継ぎが繰り返されているという意味で、原因はたいてい、時計のずれやAPIサーバーのレイテンシです。3つの値の関係は、公式ドキュメントのデフォルト値(15秒・10秒・2秒)を根拠に説明してください。