削除するとき実際に何が起きているのか
一言でいうと
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で止めておいたオブジェクトを対象に、止まった親と、それを止めている子を一度に取り出す診断スクリプトを作ります。