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

CRDとオペレータ

リーダーが二人になる瞬間

TT Labで続きを見る

一言でいうと

リーダー選出は、小さなオブジェクト1つ(Lease)に「今誰がリーダーか」を書いておき、定期的に更新する約束です。期限切れかどうかはオブジェクトに書かれておらず、各インスタンスが自分の時計で計算するため、リーダーが2つになる区間が、原理的に存在します。

なぜこの仕組みが必要なのか

Operatorを1つだけ起動すると、そのPodが落ちている間、誰も調整しません。そのため、2つ以上を起動します。ところが、調整ループはクラスターの状態を変更するコードです。2つが同時に動くと、同じリソースを2回作ろうとしたり、お互いの結果を消したり、スケールの値を交互に元に戻したりします。

最も単純な解決策は、「1つだけを働かせる」ことです。複数のインスタンスが起動していても、そのうち1つだけが調整し、残りは待機します。問題は、その1つをどうやって決めるかですが、別途合意システムを置くと、それ自体がもう1つの障害点になります。Kubernetesは、すでにあるものを使いました。APIサーバーの楽観的同時実行制御です。1つのオブジェクトを複数が同時に更新しようとすると、resourceVersionが合っているほうだけが成功します。その性質の上に、ごく薄い約束を載せたものが、リーダー選出です。

どう動くのか

約束の入れ物が、coordination.k8s.io/v1のLeaseです。5つのフィールドがすべてです。

フィールド 意味
holderIdentity 今リーダーだと主張しているインスタンスの名前
leaseDurationSeconds 更新が途絶えた後、これだけ経過すると期限切れとみなす
acquireTime 現在のホルダーがリースを取得した時刻
renewTime ホルダーが最後に更新した時刻
leaseTransitions ホルダーが変わった回数

acquireTimeとrenewTimeは、普通のTimeではなくMicroTimeなので、小数点以下6桁が必要です。形式が違うと、値ではなくパースの段階で拒否されます。

リーダーがする仕事は、renewTimeだけを定期的に書き換えることです。ホルダーも遷移回数も触りません。候補がする仕事は、リースを定期的に読んで、renewTime + leaseDurationSecondsが今より過去かどうかを見ることです。過去なら期限切れとみなして、自分の名前で引き継ぎ、acquireTimeを新しく書き、leaseTransitionsを1つ上げます。だから、遷移回数は運用メトリクスです。ホルダーが変わり続けているなら、その数値が増え続けます。

kube-controller-managerの3つのフラグが、このリズムを決めます。--leader-elect-lease-duration(デフォルトは15秒)、--leader-elect-renew-deadline(デフォルトは10秒)、--leader-elect-retry-period(デフォルトは2秒)です。関係が重要です。更新期限は、必ずリース期間より短くなければなりません。リーダーは、更新期限内に更新できなければ、自分からリーダーの座を降りますが、この期限がリース期間より長いと、「リーダーはまだ自分がリーダーだと信じているのに、ほかの候補はすでに期限切れとみなして引き継いだ」区間が生まれます。リトライ間隔は、その期限内に何度も試せるように、ずっと短くしておきます。

権限は、驚くほど小さいです。coordination.k8s.ioグループのleasesに対する、get・create・updateの3つがあれば十分です。削除する必要もなく、監視(watch)も必要ありません。候補は、監視ではなく定期的な取得で、期限切れを確認します。

現場での姿

1つ目は、リーダーが2つになる区間をなくせないことです。ネットワークが一瞬途切れたリーダーは、自分がまだリーダーだと信じて調整を続けますが、その間に、ほかのインスタンスがリースを引き継いで調整を始めます。この区間をなくすには、分散システムのより強い保証が必要で、Kubernetesのリーダー選出は、それを約束していません。そのため、調整ループそのものが冪等でなければなりません。リーダー選出は、確率を下げる仕組みであり、排他を保証する仕組みではありません。

2つ目は、時計のずれが原因の事故です。ノードの時計が数秒ずれていると、期限切れの判定がノードごとに違う結果になります。症状は「リーダーが変わり続ける」で、画面で見えるのは、leaseTransitionsが分単位で上がることだけです。調査するときに、Leaseオブジェクトだけを見ても、原因にたどり着けません。ノードの時計とAPIサーバーのレイテンシを、一緒に見る必要があります。

3つ目は、2つのリーダーが残す痕跡です。2つの調整ループが同じリソースを作ろうとすると、1つは名前の衝突で失敗し、同じフィールドをサーバーサイドapplyで書こうとすると、フィールドマネージャーの衝突が起きます。後者は、特に役に立ちます。APIサーバーが「このフィールドはほかのマネージャーのもの」と名前まで教えてくれるので、衝突のメッセージ1つで、誰が一緒に書いていたのかが明らかになります。Operatorをサーバーサイドapplyで組んでおけば、この事故が黙って通り過ぎることはありません。

このラボ環境の限界

ラボのPodでは、本物のコントローラーを2つ起動して競わせることはできません。kwokのPodは偽物で、コンテナが動かないからです。そのため、更新・期限切れ・引き継ぎは人がpatchで真似して、時計のずれも、renewTimeを過去に書いて再現します。逆に、APIサーバーは本物なので、MicroTime形式の拒否、RBACの判定、サーバーサイドapplyのフィールドマネージャーの衝突は、いずれも実際の動作そのままで確認できます。

次のラボですること

Leaseを作成して更新してみて、期限切れのLeaseと生きているLeaseを判定するスクリプトを作ります。引き継ぎをpatchで真似して遷移回数が上がることを確認し、MicroTime形式に違反したときに更新がどう阻まれるかを見ます。リーダー選出に必要な最小権限を自分で合わせたうえで、2つのインスタンスが同じリソースを書こうとするときにAPIサーバーが残す衝突を受け取ってみて、最後に運用チェックリストにまとめます。