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

CRDとオペレータ

調整 — 観測・比較・行動・報告の四拍子

TT Labで続きを見る

一言でいうと

reconcileは「何が起きたのか」を問いません。ただ「今ほしい状態は何で、実際は何か」だけを問い、その差を冪等に縮めます。

なぜ必要なのか

命令型の自動化スクリプトは、「これを作って、あれを設定して、そのあとこれを実行する」の羅列です。途中で失敗するとシステムが中途半端な状態で残り、再実行すると「すでに存在します」というエラーが出たり、重複が生じたりします。そのため、人々は再実行を怖がるようになり、怖い自動化は結局使われなくなります。

reconcileはこの問題を逆転させます。どの時点で失敗しても、次の呼び出しが最初からやり直して、終わっていない部分だけを仕上げます。コントローラーが途中で落ちて復活してもかまいません。「どこまでやったか」をメモリに持っておく必要がないからです。本当の状態は、常にクラスターの中にあります。

どう動くのか

実際のコントローラーでリクエストが届く経路は、3つの段階です。

API Server --watch--> Informer --> WorkQueue --> Reconciler
                       (로컬 캐시)  (중복 제거,   (사용자 코드)
                                    속도 제어)

このコードブロックの韓国語の3つの注記は、順に、ローカルキャッシュ、重複の除去と速度制御、ユーザーコード、という意味です。

ここで最も重要な洞察は、reconcileに渡されるのはオブジェクトではなく、オブジェクトのキー(ネームスペース/名前)だけだという点です。何が変わったのか、作成だったのか変更だったのかは渡されません。reconcileは、そのキーで最新の状態を読み直します。この設計が冪等性を強制します。

reconcile 1回は、6つの質問に要約されます。

  1. 今このオブジェクトの望ましい状態は何か(desired)
  2. 実際のクラスターの状態は何か(observed)
  3. 両者の差は何か(diff)
  4. その差を冪等に埋めるには(action)
  5. まだ埋められていない部分はあるか(requeueの判断)
  6. 観察した結果をstatusにどう反映するか(status)

4つ目は冪等性です。「作成する」ではなく、「望ましい姿で存在するか確認し、そうでなければ合わせる」と書く必要があります。すでに合っているなら何もしないのが正解で、そのとき下位オブジェクトのresourceVersionはそのままでなければなりません。内容が同じなのにもう一度書くと、新しいwatchイベントが生まれ、そのイベントがまたreconcileを呼びます。

5つ目は再キューです。「まだ準備ができていない」をすべてエラーとして処理してはいけません。エラーとして返すと、ワークキューが指数バックオフでリトライし、エラーログが溜まり、そのオブジェクトのバックオフが長くなって応答性が落ちます。依存先がまだないのは失敗ではなく、「少しあとでもう一度見よう」ということです。

区分 トリガー 待ち時間 用途
エラーバックオフ エラーを返す 指数的に増加(自動) 本当の失敗のリトライ
遅延再キュー 結果に明示 自分で指定した値 意図的な定期チェック

バックオフが指数的なのは、1つのオブジェクトの繰り返しの失敗が、キュー全体を飢えさせないようにするためです。そして、成功すると、そのキーのバックオフカウンターはリセットされます。

6つ目はstatusと無限ループです。statusを書くと、それ自体が新しいwatchイベントとなって、reconcileをまた呼ぶことがあります。実際のコントローラーは、GenerationChangedPredicateでこれを防ぎます。generationはspecが変更されたときにしか上がらないため、このフィルターをかけると、自分のstatus書き込みによる再呼び出しが取り除かれます。ただし、定期チェックが必要なコントローラーなら、このフィルターがその定期チェックまで止めてしまわないよう注意が必要です。

削除の経路とファイナライザーについてです。所有者参照でつながったクラスター内の子は、ガベージコレクターが片付けますが、クラスターの外のリソース(クラウドのロードバランサー、外部DBのアカウント)は、Kubernetesが知りません。ファイナライザーは、その後片付けを保証する仕組みです。

삭제 요청 -> deletionTimestamp 설정 (오브젝트는 아직 존재)
          -> 리컨사일 재호출: 정리 수행
          -> 파이널라이저 제거
          -> 그제서야 실제 삭제

このコードブロックの韓国語の4行は、順に、削除リクエストでdeletionTimestampが設定される(オブジェクトはまだ存在する)、reconcileが再び呼ばれてクリーンアップを行う、ファイナライザーを削除する、そこではじめて実際に削除される、という意味です。

核心は、「削除=すぐに消える」ではなく、「削除=削除時刻が刻まれて、reconcileがもう一度呼ばれる」ということです。その1回が、後片付けをする最後のチャンスです。そして、クリーンアップの関数も冪等でなければなりません。すでにないものを消そうとする試みがエラーを投げると、ファイナライザーが永遠に外れず、削除のデッドロックが生じます。

このラボ環境についての正直な説明

このラボには、コンパイル済みのGoコントローラーを実行する手段がありません。そのため、reconcileのループをシェルスクリプトのReconcilerとして作ります。informerキャッシュとワークキューはシミュレーションせず、その代わりに、reconcile関数の本質である、観測 → 比較 → 行動 → statusの報告と再キューの判断、ファイナライザーの流れを、手で実装します。採点されるのはコントローラーのバイナリではなく、判断の正確さと冪等性です。実際のコントローラーをGoに移しても、この4拍子はそのままです。

現場での姿

1つ目は、無限reconcileです。ほとんどすべてのOperator開発者が一度は経験する通過儀礼です。reconcileがstatusを書くと、その書き込みが新しいwatchイベントを生み、そのイベントがまたreconcileを呼びます。CPUが1コアを占有し、ログが毎秒数百行ずつ溜まります。解決策は2つです。specが変更されたときにだけ増加するmetadata.generationを見るpredicateをかけて、status変更による自己トリガーを断ち切るか、そもそも値が変わったときにだけstatusを書くことです。

2つ目は、statusの衝突です。キャッシュから読んだ古いオブジェクトでstatusを更新しようとして、conflictに当たります。コントローラーがリソースを同時に複数処理すると、確率が上がります。最新のオブジェクトを読み直して更新するか、サーバーサイドapplyでフィールド単位のマージに任せます。

3つ目は、作ったばかりのオブジェクトが見えないことです。読み取りはinformerキャッシュから、書き込みはAPIサーバーに対して行われます。そのため、Createの直後にGetすると、まだ「なし」が返ることがあります。ここでsleepを入れるのが、最もよくある誤答です。正しい答えは待たないことです。冪等に組んでおいて、次のreconcileで自然に見えるのに任せれば済みます。reconcileのループの設計思想が、そのまま表れる箇所です。

4つ目は、ファイナライザーに引っかかったオブジェクトです。コントローラーが落ちたままCRを削除すると、deletionTimestampだけが刻まれた状態で、永遠にTerminatingに残ります。ファイナライザーを外してくれる主体がいないからです。そのため、Operatorを削除するときは、コントローラーよりCRを先に片付ける順序を守る必要があり、急ぐ場合に備えて、ファイナライザーを手で外すエスケープハッチをランブックに書いておきます。

次のラボですること

/root/op/reconcile/reconcile.shを作成して、CRのspecをdesiredとして、下位のConfigMapをactualとして読んで比較し、なければ作り、すでに合っていれば何もしないことを、resourceVersionが変わらないことを根拠に証明します。statusをサブリソースのパスで書き戻し、依存先がないときの再キューの判断をJSONとして残し、ファイナライザーで後片付けをしてから削除される流れを作ったうえで、最後に3つのCRを1周させた調整レポートを作ります。