親を消したのに子が残った - 所有参照と削除の伝播
目標
所有者参照の4つのフィールドがそれぞれ何を決めるかを、自分で作って確認し、--cascadeの3種類の違いをオブジェクトで証明したうえで、Terminatingで止まったオブジェクトを診断するレポートを作ります。
なぜ重要なのか
Operatorが作るリソースは、単独では生きていません。親が1つ消えたら、その下に付いているものも一緒に片付けられる必要がありますが、Kubernetesには、この作業を行う参照カウントがありません。その代わり、子が親を指すownerReferencesの1行があり、ガベージコレクターがその参照をたどって片付けます。そのため、参照を間違って書くと、故障が黙って発生します。uidが間違っていると、作ったばかりの子が数秒で消え、ネームスペースをまたぐと、警告イベントを1つだけ残して子が片付けられます。逆に、ファイナライザーは後片付けが終わるまで削除をつかまえておく仕組みなので、その後片付けをするコントローラーがないと、オブジェクトは永遠にTerminatingにとどまります。運用者が実際に出会う画面は、たいていこの2つのどちらかで、どちらも原因を突き止める手順が必要です。
ステップ
/root/op-ownership/site-crd.yamlにCRDsites.own.labhub.ioを書いてください。グループown.labhub.io、kindSite、複数形sites、バージョンはv1を1つで、スキーマにはspec.region(string)だけを置きます。ネームスペースop-ownを作成し、/root/op-ownership/site-alpha.yamlでSitealpha(regionkr-east)を適用した後、そのオブジェクトのmetadata.uidを/root/op-ownership/alpha-uid.txtに1行で保存してください。/root/op-ownership/alpha-good.yamlにConfigMapalpha-good(ネームスペースop-own)を書き、ownerReferencesにSitealphaを書いてください。apiVersionown.labhub.io/v1、kindSite、namealpha、uidはステップ1で得た実際の値です。/root/op-ownership/alpha-bad.yamlには、同じ内容のConfigMapalpha-badを書きますが、uidだけを00000000-0000-0000-0000-000000000000にします。両方を適用して、しばらく待った後、kubectl -n op-own get cmの出力を/root/op-ownership/gc-uid.txtに保存してください。- ネームスペース
op-own-remoteを作成し、/root/op-ownership/alpha-remote.yamlにConfigMapalpha-remoteを書いてください。ネームスペースはop-own-remoteなのに、ownerReferencesはop-ownにあるSitealphaを(実際のuidで)指します。適用した後、しばらく待ってから、kubectl -n op-own-remote get eventsの出力を/root/op-ownership/xns-event.txtに保存してください。 /root/op-ownership/site-beta.yamlでSitebeta(regionkr-west)を作成し、その子としてConfigMapbeta-1とbeta-2を/root/op-ownership/beta-children.yamlの1つのファイルに入れて(2つを---でつなぎます)、実際のuidで所有者参照をかけて適用してください。そのあと、kubectl -n op-own delete site beta --cascade=backgroundで削除し、子が消えるまで待った後、kubectl -n op-own get cmの出力を/root/op-ownership/cascade-background.txtに保存してください。/root/op-ownership/site-gamma.yamlでSitegamma(regionjp-east)を作成し、子のConfigMapgamma-1・gamma-2を/root/op-ownership/gamma-children.yamlに入れて、実際のuidでかけて適用してください。kubectl -n op-own delete site gamma --cascade=orphanで削除した後、kubectl -n op-own get cm gamma-1 -o jsonpath='{.metadata.ownerReferences}'の結果を、/root/op-ownership/cascade-orphan.txtにgamma-1-ownerrefs=<값, 비어 있으면 none>(プレースホルダーは値(空ならnone)です)の1行で保存してください。/root/op-ownership/site-delta.yamlでSitedelta(regionus-west)を作成し、/root/op-ownership/delta-child.yamlでConfigMapdelta-1を作成してください。所有者参照に、実際のuidとともにblockOwnerDeletion: trueを入れ、このConfigMap自身に、ファイナライザーown.labhub.io/holdを付けます。そのあと、kubectl -n op-own delete site delta --cascade=foreground --wait=falseを実行し、しばらく後に、kubectl -n op-own get site delta -o jsonを/root/op-ownership/foreground-stuck.jsonに保存してください。このSiteは、このラボが終わるまで、そのままにしておきます。/root/op-ownership/site-epsilon.yamlでSiteepsilon(regioneu-west)を、ファイナライザーown.labhub.io/drainとともに作成し、子のConfigMapepsilon-dataを実際のuidでかけて、/root/op-ownership/epsilon-child.yamlで適用してください。kubectl -n op-own delete site epsilon --wait=falseを実行した直後のオブジェクトを/root/op-ownership/finalizer-pending.jsonに保存し、後片付けの作業(子のConfigMapを直接削除する)を行った後、何を後片付けしたかを/root/op-ownership/epsilon-cleanup.txtに1行以上書いてください。最後に、ファイナライザーを外して、Siteが実際に消えるようにしてください。/root/op-ownership/stuck-report.shを作成してください。op-ownで、(1)deletionTimestampが刻まれたSiteをSTUCK Site/<이름> finalizers=<쉼표로 이은 목록>(プレースホルダーは名前と、カンマでつないだ一覧です)として、(2)blockOwnerDeletionがtrueの所有者参照を持つConfigMapをBLOCKER ConfigMap/<이름> owner=<종류>/<이름> finalizers=<목록>(プレースホルダーは順に、名前、種類と名前、一覧です)として出力し、2種類をまとめてソートして標準出力にだけ出力します。STUCKの行が1つでもあれば0ではないコードで、なければNONEを出力して0で終了する必要があります。出力を/root/op-ownership/stuck-report.txtに保存し、誰が何をなぜ止めているかを/root/op-ownership/diagnosis.txtに、人が読める文で書いてください(止めている子の名前と、そのファイナライザーの名前が必ず入っている必要があります)。
参考
- 所有者参照の必須の4つのフィールドは、
apiVersion・kind・name・uidです。 --cascadeは、background(デフォルト)・foreground・orphanの3種類です。kubectl delete … --wait=falseを使うと、止まった状態をそのまま観察できます。- ファイナライザーの一覧から1つを外すには、
kubectl patch --type=jsonのremove操作が便利です。 - よくあるミス: 親を作り直した後に、古いuidをそのまま使って、子がすぐに削除されてしまうことです。
- よくあるミス: Terminatingが解消しないからといって、ファイナライザーを先に外して、後片付けを飛ばしてしまうことです。
- 参考: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/
- 参考: https://kubernetes.io/docs/concepts/architecture/garbage-collection/
- 参考: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers/
親になる型を作る
/root/op-ownership/site-crd.yamlにCRD sites.own.labhub.ioを書いてください。グループown.labhub.io、kind Site、複数形sites、バージョンはv1を1つで、スキーマにはspec.region(string)だけを置きます。ネームスペースop-ownを作成し、/root/op-ownership/site-alpha.yamlでSite alpha(region kr-east)を適用した後、そのオブジェクトのmetadata.uidを/root/op-ownership/alpha-uid.txtに1行で保存してください。
所有者参照では、名前は人が読む値で、本当の鍵はuidです。同じ名前のオブジェクトが削除されて作り直されるとuidは変わりますが、この性質が、続くステップの核心になります。uidはkubectl get … -o jsonpathで取り出してください。
uidを間違えると、ガベージコレクターが子を削除する
/root/op-ownership/alpha-good.yamlにConfigMap alpha-good(ネームスペースop-own)を書き、ownerReferencesにSite alphaを書いてください。apiVersion own.labhub.io/v1、kind Site、name alpha、uidはステップ1で得た実際の値です。/root/op-ownership/alpha-bad.yamlには、同じ内容のConfigMap alpha-badを書きますが、uidだけを00000000-0000-0000-0000-000000000000にします。両方を適用して、しばらく待った後、kubectl -n op-own get cmの出力を/root/op-ownership/gc-uid.txtに保存してください。
ガベージコレクターは、参照の名前ではなくuidで親を探します。見つからなければ「親がすでに消えた子」とみなして片付けます。この動作のために、コントローラーがuidをキャッシュしておいて、親が作り直された後に使うと、作ったばかりの子がすぐに消えます。待つときは、固定のsleepよりも、消えるまで回る短いループのほうが安全です。
ネームスペースをまたぐ所有はない
ネームスペースop-own-remoteを作成し、/root/op-ownership/alpha-remote.yamlにConfigMap alpha-remoteを書いてください。ネームスペースはop-own-remoteなのに、ownerReferencesはop-ownにあるSite alphaを(実際のuidで)指します。適用した後、しばらく待ってから、kubectl -n op-own-remote get eventsの出力を/root/op-ownership/xns-event.txtに保存してください。
所有者参照は、同じネームスペースの中でしか結べず、クラスタースコープのオブジェクトだけが、ネームスペーススコープの子を持てます。ルールに違反しても、APIサーバーはapplyを止めません。あとでガベージコレクターが処理して、その痕跡をイベントとして残します。イベントの理由(REASON)の語を、そのまま読んでみてください。
backgroundは親から先に削除する
/root/op-ownership/site-beta.yamlでSite beta(region kr-west)を作成し、その子としてConfigMap beta-1とbeta-2を/root/op-ownership/beta-children.yamlの1つのファイルに入れて(2つを---でつなぎます)、実際のuidで所有者参照をかけて適用してください。そのあと、kubectl -n op-own delete site beta --cascade=backgroundで削除し、子が消えるまで待った後、kubectl -n op-own get cmの出力を/root/op-ownership/cascade-background.txtに保存してください。
backgroundはデフォルトです。APIサーバーが親をすぐに削除し、子の後片付けはガベージコレクターに任せます。そのため、コマンドはすぐに戻ってくるのに、子は少しの間残っています。この時間差が、「削除したのにまだ見える」という勘違いの出どころです。
orphanは子を残して、紐だけを切る
/root/op-ownership/site-gamma.yamlでSite gamma(region jp-east)を作成し、子のConfigMap gamma-1・gamma-2を/root/op-ownership/gamma-children.yamlに入れて、実際のuidでかけて適用してください。kubectl -n op-own delete site gamma --cascade=orphanで削除した後、kubectl -n op-own get cm gamma-1 -o jsonpath='{.metadata.ownerReferences}'の結果を、/root/op-ownership/cascade-orphan.txtにgamma-1-ownerrefs=<값, 비어 있으면 none>(プレースホルダーは値(空ならnone)です)の1行で保存してください。
orphanは、子を削除しません。その代わり、ガベージコレクターが子からその所有者参照を取り外します。外さないと、親のない参照が残って、次の後片付けのときに子が消えてしまうからです。Operatorを撤去しながら、そのリソースは残しておきたいときに使う方式です。
foregroundは子を待って止まる
/root/op-ownership/site-delta.yamlでSite delta(region us-west)を作成し、/root/op-ownership/delta-child.yamlでConfigMap delta-1を作成してください。所有者参照に、実際のuidとともにblockOwnerDeletion: trueを入れ、このConfigMap自身に、ファイナライザーown.labhub.io/holdを付けます。そのあと、kubectl -n op-own delete site delta --cascade=foreground --wait=falseを実行し、しばらく後に、kubectl -n op-own get site delta -o jsonを/root/op-ownership/foreground-stuck.jsonに保存してください。このSiteは、このラボが終わるまで、そのままにしておきます。
foregroundは、順序を逆にします。子がすべて消えてはじめて、親を削除します。その待機を表現するために、APIサーバーが親にファイナライザーを1つ付けますが、その名前が何なのかを、保存したJSONから探してみてください。子にファイナライザーがあると、その子が消えられないため、待機が終わりません。
ファイナライザーを外すまで、削除は終わらない
/root/op-ownership/site-epsilon.yamlでSite epsilon(region eu-west)を、ファイナライザーown.labhub.io/drainとともに作成し、子のConfigMap epsilon-dataを実際のuidでかけて、/root/op-ownership/epsilon-child.yamlで適用してください。kubectl -n op-own delete site epsilon --wait=falseを実行した直後のオブジェクトを/root/op-ownership/finalizer-pending.jsonに保存し、後片付けの作業(子のConfigMapを直接削除する)を行った後、何を後片付けしたかを/root/op-ownership/epsilon-cleanup.txtに1行以上書いてください。最後に、ファイナライザーを外して、Siteが実際に消えるようにしてください。
ファイナライザーが付いていると、削除リクエストはdeletionTimestampを刻むだけで止まります。オブジェクトは引き続き取得でき、修正もできますが、新しく作ることはできません。コントローラーがする仕事が、まさにこの区間です。外部のシステムを後片付けしてから、自分のファイナライザーを削除します。リストから要素を1つ外すには、kubectl patch --type=jsonのremove操作が便利です。
Terminatingで止まったものを診断する
/root/op-ownership/stuck-report.shを作成してください。op-ownで、(1)deletionTimestampが刻まれたSiteをSTUCK Site/<이름> finalizers=<쉼표로 이은 목록>(プレースホルダーは名前と、カンマでつないだ一覧です)として、(2)blockOwnerDeletionがtrueの所有者参照を持つConfigMapをBLOCKER ConfigMap/<이름> owner=<종류>/<이름> finalizers=<목록>(プレースホルダーは順に、名前、種類と名前、一覧です)として出力し、2種類をまとめてソートして標準出力にだけ出力します。STUCKの行が1つでもあれば0ではないコードで、なければNONEを出力して0で終了する必要があります。出力を/root/op-ownership/stuck-report.txtに保存し、誰が何をなぜ止めているかを/root/op-ownership/diagnosis.txtに、人が読める文で書いてください(止めている子の名前と、そのファイナライザーの名前が必ず入っている必要があります)。
診断の核心は、2つの一覧を並べて置くことです。止まった親と、その親をつかまえられる子です。年齢や時刻のように、見るたびに変わる値は出力に入れないでください。そうすれば、このレポートをファイルとして残しておいて、あとの結果と比べられます。