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

CRDとオペレータ

削除するとき実際に何が起きているのか

TT Labで続きを見る

一言でいうと

Kubernetesには、所有関係を数える帳簿がありません。子が親を指すownerReferencesの1行がすべてで、ガベージコレクターがその行を信じて動きます。そのため、その行を間違って書くと、故障が黙って発生します。

なぜこの設計が必要なのか

Operatorは、カスタムリソースを1つ受け取って、複数の実際のリソースを作ります。Site1つにConfigMapが2つ、Serviceが1つ、Secretが1つ、といった具合です。では、Siteを削除するとき、その4つは誰が削除するのでしょうか。

コントローラーに直接削除させるのは簡単そうに見えますが、問題があります。コントローラーが落ちている間に親が削除されると、子は永遠に残ります。コントローラーをまるごと撤去すると、それらのリソースは誰も知らないゴミになります。そこでKubernetesは、後片付けをコントローラーではなくAPIサーバー側のガベージコレクターに任せ、そのコレクターが読む情報を、子のメタデータに書かせました。

metadata:
  ownerReferences:
    - apiVersion: own.labhub.io/v1
      kind: Site
      name: alpha
      uid: 8f1c...            # 진짜 열쇠는 이것
      controller: true        # 이 참조가 '관리자' 인가
      blockOwnerDeletion: true

このコードブロックの2つの韓国語コメントは、順に、本当の鍵はこれ(uid)、この参照が「管理者」(controller)かどうか、という意味です。

4つのフィールドが必須です。apiVersion・kind・name・uidです。そして、このうち実際に親を探すのに使われるのはuidです。名前は人が読む値であり、ガベージコレクターはuidで親の存在を確認します。

どう動くのか

uidが間違っていると、子が削除されます。コレクターから見ると、「そのuidを持つオブジェクトがない」は、「親がすでに削除された」と同じ意味です。そのため、数秒以内に子を片付けます。この性質が現場で事故につながる典型的な経路は、こうです。コントローラーが親のuidをキャッシュしておいたのに、その間に親が削除され、同じ名前で作り直されます。新しい親のuidは違います。古いuidで作った子は、作った途端に消えます。

ネームスペースをまたぐ所有はありません。ネームスペーススコープの子は、同じネームスペースの親しか持てず、クラスタースコープのオブジェクトだけが例外として、どのネームスペースの子でも持てます。ルールに違反した参照は、applyでは止められません。その代わり、コレクターがあとでその子を片付けるときに、OwnerRefInvalidNamespaceという警告イベントを残します。イベントはデフォルトでは1時間で消えるので、その時間を過ぎてから調査に入ると、子がなぜなくなったのかを知る手段がほとんどありません。

削除の伝播は3種類です。

--cascade 親 子 使いどころ
background(デフォルト) すぐに消える コレクターが後から削除する 通常の削除
foreground 子がすべて消えた後に消える 先に削除される 後片付けの順序が重要なとき
orphan すぐに消える 残り、参照だけが外れる Operatorだけを撤去するとき

foregroundは、待機を表現するために、親にforegroundDeletionというファイナライザーを付けます。親はdeletionTimestampが刻まれたまま残り、blockOwnerDeletion: trueの子がすべて消えてはじめて、なくなります。orphanは、子を削除しない代わりに、子からその所有者参照を取り外します。外さないと、親のいない参照が残って、次の後片付けのときに子が消えてしまうからです。

ファイナライザーは削除を防ぐ仕組みではなく、遅らせる仕組みです。ファイナライザーが1つでもあると、削除リクエストはdeletionTimestampを刻むだけで終わります。オブジェクトは引き続き取得でき、修正もできますが、同じ名前で新しく作ることはできません。コントローラーは、この区間で外の世界を後片付けし(クラウドリソースの解放、バックアップの保管、接続の終了)、終わったら自分のファイナライザーをリストから外します。リストが空になった瞬間に、オブジェクトが本当に消えます。

現場での姿

1つ目は、Terminatingから抜けられないオブジェクトです。最もよくある原因は、ファイナライザーを削除するコントローラーがいないことです。Operatorを先に撤去して、そのあとでCRを削除すると、まさにこうなります。このとき人々が最もよくやるのが、ファイナライザーを手で外すことですが、それは後片付けを飛ばすことなので、クラウドリソースがそのまま残り、料金がかかり続けます。順序は逆でなければなりません。CRを先に片付けて、Operatorを撤去します。

2つ目は、ネームスペースがTerminatingで止まることです。原因は、たいていその中のあるオブジェクトがファイナライザーを付けていることです。ネームスペースを無理やり削除すると、その中のオブジェクトが後片付けされないまま、記録だけが消えます。

3つ目は、診断の手順を先に作っておくことです。止まったオブジェクトに出会ったときに問うべきことは、3つです。deletionTimestampがいつ刻まれたか、どのファイナライザーが残っているか、そのファイナライザーを外す主体が今生きているかです。foregroundDeletionが残っているなら、質問がもう1つ加わります。blockOwnerDeletionがtrueの子のうち、まだ消えていないものは何か、です。この4つを一度に取り出すスクリプトを先に作っておけば、事故のときに、最初から考えなくて済みます。

このラボ環境の限界

kwokクラスターのPodは偽物なので、コンテナが実際には実行されません。そのため、ファイナライザーの中で外部のシステムを後片付けする本当の作業(クラウドAPIの呼び出し、ボリュームの解放)は、真似しかできません。逆に、ガベージコレクターと削除の伝播そのものは、本物のkube-controller-managerが処理するので、uidが間違った子が消えるのも、foreground削除がファイナライザーを残して止まるのも、実際の動作そのままです。

次のラボですること

Siteという親の型を作り、uidが合っている子と間違っている子を並べて作って、ガベージコレクターが何をするかを見ます。ネームスペースをまたぐ参照が残す警告イベントを確認し、--cascadeの3種類をそれぞれ実行して、結果をオブジェクトで証明します。最後に、foregroundで止めておいたオブジェクトを対象に、止まった親と、それを止めている子を一度に取り出す診断スクリプトを作ります。