Kubernetes のガベージコレクション
Kubernetes のガベージコレクション(GC) について整理し、挙動を確認しました。
- 1. Kubernetes のガベージコレクション
- 2. まとめ
- 3. 参考
1. Kubernetes のガベージコレクション
Kubernetes のガベージコレクションはクラスタリソースを掃除する以下仕組みの総称です。
- Failed 状態の Pod
- 終了した Job
- owner reference の無いオブジェクト
- 未使用のイメージ
- 未使用のコンテナ
- reclaimPolicy が Delete の StorageClass から動的にプロビジョニングされた PersistentVolume
- 失効または期限切れの CertificatesSigningRequest(CSR)
- 次のシナリオで削除された Node
- Node Lease オブジェクト
eksctl コマンドで EKS Cluster を作成する - YasuBlog の記事で作成した EKS Cluster を使用して検証してみました。
1.1. Failed 状態の Pod
まずは Pod のライフサイクルについて整理します。Pod のとりうるフェーズは以下の 5 種類です。
フェーズ | 説明 |
---|---|
Pending | Pod がクラスタによって承認されたが、1 つ以上のコンテナが稼働する準備ができていない状態。 これには、スケジュールされるまでの時間やネットワーク経由でイメージをダウンロードするための時間などが含まれる。 |
Running | Pod が Node にバインドされ、全てのコンテナが作成された状態。 少なくとも1つのコンテナが実行されているか、開始または再起動中。 |
Succeeded | Pod 内の全てのコンテナが正常に終了し、再起動しない状態。 |
Failed | Pod 内の全てのコンテナが終了し、少なくとも1つのコンテナが異常終了した状態。 つまり、コンテナが 0 以外のステータスで終了したか、システムによって終了された。 |
Unknown | 何らかの理由により Pod の状態を取得できない。 このフェーズは通常は Node との通信エラーにより発生する。 |
Failed 状態になった Pod は、人またはコントローラーが明示的に削除するまで存在します。
コントロールプレーンは、終了状態の Pod(Succeeded または Failed フェーズを持つ Pod)の数が設定された閾値(kube-controller-manager
の terminated-pod-gc-threshold
)を超えたとき、それらのPodを削除します。
terminated-pod-gc-threshold
のデフォルト値は 12,500 です。つまり Failed 状態の Pod が 12,500 個を超えたらコントロールプレーンによって自動で削除されます。(あまり無いユースケースかと思います)
12,500 個の Failed Pod を作成して検証するのは面倒なので、「Failed 状態になった Pod は、人またはコントローラーが明示的に削除するまで存在します。」の部分のみ確認します。
Failed は コンテナが 0 以外のステータスで終了したか、システムによって終了された
という状態なので、終了コードが 1 となり終了後に再起動しない設定の以下 Pod を作成してみます。
apiVersion: v1 kind: Pod metadata: name: test-pod spec: containers: - name: amazonlinux image: public.ecr.aws/amazonlinux/amazonlinux:latest command: - "bin/bash" - "-c" - "exit 1" restartPolicy: Never
apply します。
$ kubectl apply -f test-pod.yaml
pod/test-pod created
Pod が作成されました。
$ kubectl get pod NAME READY STATUS RESTARTS AGE test-pod 0/1 Error 0 18s
exit 1
を実行しているため STATUS が Error になっています。restartPolicy: Never
のため再起動もしません。この状態のまま変化はありませんでした。
describe で Pod の詳細を確認します。
$ kubectl describe pod test-pod Name: test-pod Namespace: default Priority: 0 Node: ip-10-0-102-214.ap-northeast-1.compute.internal/10.0.102.214 Start Time: Wed, 23 Feb 2022 00:02:53 +0900 Labels: <none> Annotations: kubernetes.io/psp: eks.privileged Status: Failed IP: 10.0.102.139 ~省略~
9 行目の通り、Status は Failed
になっています。Failed Pod は人またはコントローラーが明示的に削除するまで存在する事が確認できました。
1.2. 終了した Job
TTL-after-finished controller
が、終了した Job(Complete か Failed)を削除します。Job 終了から削除までの時間を .spec.ttlSecondsAfterFinished
フィールドで指定します。この機能は 1.23 で stable になりました。
Job の終了後 10 秒経ったら削除される設定の以下 Job を作成してみます。6 行目に ttlSecondsAfterFinished
を設定しています。
apiVersion: batch/v1 kind: Job metadata: name: test-job spec: ttlSecondsAfterFinished: 10 template: spec: containers: - name: amazonlinux image: public.ecr.aws/amazonlinux/amazonlinux:latest command: - "bin/bash" - "-c" - "exit 0" restartPolicy: Never
apply します。
$ kubectl apply -f test-job.yaml
job.batch/test-job created
1 秒おきに get job を実行して 10 秒で削除されることを確認します。
$ while true; do date; kubectl get job; sleep 1; done; 2022年 2月23日 水曜日 00時28分03秒 JST NAME COMPLETIONS DURATION AGE test-job 1/1 3s 4s 2022年 2月23日 水曜日 00時28分05秒 JST NAME COMPLETIONS DURATION AGE test-job 1/1 3s 5s 2022年 2月23日 水曜日 00時28分06秒 JST NAME COMPLETIONS DURATION AGE test-job 1/1 3s 7s 2022年 2月23日 水曜日 00時28分08秒 JST NAME COMPLETIONS DURATION AGE test-job 1/1 3s 9s 2022年 2月23日 水曜日 00時28分09秒 JST NAME COMPLETIONS DURATION AGE test-job 1/1 3s 10s 2022年 2月23日 水曜日 00時28分11秒 JST NAME COMPLETIONS DURATION AGE test-job 1/1 3s 12s 2022年 2月23日 水曜日 00時28分13秒 JST No resources found in default namespace. 2022年 2月23日 水曜日 00時28分14秒 JST No resources found in default namespace.
コンテナ起動後、10 秒後に削除されて表示されなくなりました。
終了した Job が .spec.ttlSecondsAfterFinished
フィールドで指定した時間後に削除されることを確認できました。
1.3. owner reference の無いオブジェクト
1.3.1. owner と従属オブジェクト
まずはオブジェクトの親子関係について整理します。
Kubernetes では、いくつかのオブジェクトは他のオブジェクトの owner です。例えば ReplicaSet により起動した Pod の owner はその ReplicaSet です。 owner に所有されているオブジェクトは従属オブジェクトと呼びます。
従属オブジェクトは metadata.ownerReferences
フィールドで owner を示します。ownerReferences
はオブジェクト名と UID で構成されます。ReplicaSet, DaemonSet, Deployment, Job, CronJob, ReplicationController の従属オブジェクトは Kubernetes が自動で ownerReferences
を設定します。自動ではなくユーザがマニュアルで設定する事も可能ですが、通常はマニュアルで設定する必要はありません。
従属オブジェクトは metadata.ownerReferences.blockOwnerDeletion
フィールドも持ちます。これは true
か false
を値に持つことができ、owner オブジェクトの削除をブロックするかどうかを制御できます。これは後述する foreground カスケード削除を明示的に指定した時のみ作用します。background カスケード削除(デフォルトの削除方法)の場合は意味のないフィールドとなります。なお、Kubernetes は自動で blockOwnerDeletion
を true に設定します。こちらも自動ではなくユーザがマニュアルで設定する事が可能です。
例えば Deployment を foreground カスケード削除する場合、Pod, ReplicaSet は blockOwnerDeletion
フィールドが true
のため Pod は ReplicaSet, ReplicaSet は Deployment の削除をブロックします。つまり、Pod, ReplicaSet が削除された後に Deployment が削除されます。
では、Deployment を起動して Deployment,ReplicaSet,Pod の owner 関連のフィールドを確認します。
apiVersion: apps/v1 kind: Deployment metadata: name: deployment spec: replicas: 1 selector: matchLabels: app: app template: metadata: labels: app: app spec: containers: - name: amazonlinux image: public.ecr.aws/amazonlinux/amazonlinux:latest command: - "bin/bash" - "-c" - "sleep 3600"
Deployment の uid を確認します。なお、Deployment は owner なので ownerReferences
フィールドはありません。
$ kubectl get deploy deployment -o jsonpath='{.metadata.uid}' 1dee8bae-4a55-41c0-8a43-4b35dd4dbb24%
ReplicaSet の ownerReferences
フィールドです。owner が Deployment になっていることがわかります。
$ kubectl get rs -o yaml | grep -i owner -A4 ownerReferences: - apiVersion: apps/v1 blockOwnerDeletion: true controller: true kind: Deployment name: deployment uid: 1dee8bae-4a55-41c0-8a43-4b35dd4dbb24 $ kubectl get rs deployment-6bb985c8c9 -o jsonpath='{.metadata.uid}' 450c9fea-f13e-462d-9320-509dc1a90cf9%
Pod の ownerReferences
フィールドです。owner が ReplicaSet になっていることがわかります。
$ kubectl get pod -o yaml | grep -i owner -A4 ownerReferences: - apiVersion: apps/v1 blockOwnerDeletion: true controller: true kind: ReplicaSet name: deployment-6bb985c8c9 uid: 450c9fea-f13e-462d-9320-509dc1a90cf9
続いて blockOwnerDeletion
の挙動を確認します。Deployment を(foreground カスケード削除で)削除すると Pod -> ReplicaSet -> Deployment の順に削除されることを確認します。
時間がわかりやすいようにタイムスタンプを出力します。
$ gdate +"%Y-%m-%d %H:%M:%S.%3N"; kubectl delete deploy deployment --cascade=foreground; gdate +"%Y-%m-%d %H:%M:%S.%3N" 2022-02-26 21:25:14.747 deployment.apps "deployment" deleted 2022-02-26 21:25:46.899
get pod/rs/deploy のログは以下のようになりました。※ mac で実行しているため gdate コマンドを使用しています(mac のデフォルトの date コマンドだとミリ秒が出力できないため)
# Pod $ kubectl get pod -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done 2022-02-26 21:24:42.681 NAME READY STATUS RESTARTS AGE 2022-02-26 21:24:42.684 deployment-6bb985c8c9-n8rj8 1/1 Running 0 16s 2022-02-26 21:25:14.994 deployment-6bb985c8c9-n8rj8 1/1 Terminating 0 48s 2022-02-26 21:25:45.815 deployment-6bb985c8c9-n8rj8 0/1 Terminating 0 79s 2022-02-26 21:25:46.800 deployment-6bb985c8c9-n8rj8 0/1 Terminating 0 80s 2022-02-26 21:25:46.808 deployment-6bb985c8c9-n8rj8 0/1 Terminating 0 80s # ReplicaSet $ kubectl get rs -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done 2022-02-26 21:24:44.108 NAME DESIRED CURRENT READY AGE 2022-02-26 21:24:44.110 deployment-6bb985c8c9 1 1 1 18s 2022-02-26 21:25:14.969 deployment-6bb985c8c9 1 1 1 48s 2022-02-26 21:25:14.979 deployment-6bb985c8c9 1 1 1 48s 2022-02-26 21:25:15.012 deployment-6bb985c8c9 1 0 0 48s 2022-02-26 21:25:46.863 deployment-6bb985c8c9 1 0 0 80s # Deployment $ kubectl get deploy -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done 2022-02-26 21:24:45.646 NAME READY UP-TO-DATE AVAILABLE AGE 2022-02-26 21:24:45.649 deployment 1/1 1 1 19s 2022-02-26 21:25:14.908 deployment 1/1 1 1 48s 2022-02-26 21:25:14.933 deployment 1/1 1 1 48s 2022-02-26 21:25:15.020 deployment 0/1 0 0 49s 2022-02-26 21:25:46.874 deployment 0/1 0 0 80s 2022-02-26 21:25:46.895 deployment 0/1 0 0 80s
Pod -> ReplicaSet -> Deployment の順に削除されていることを確認できました。(厳密には削除された時間は不明ですが、最後に出力された時間の順が Pod -> ReplicaSet -> Deployment になっています)
では、ここで Pod, ReplicaSet の blockOwnerDeletion
フィールドを false
にして同様に検証してみます。
まずは kubectl edit で blockOwnerDeletion
を true
から false
に変更します。
# Pod $ kubectl get pod NAME READY STATUS RESTARTS AGE deployment-6bb985c8c9-8h9hh 1/1 Running 0 9m20s $ kubectl edit pod deployment-6bb985c8c9-8h9hh pod/deployment-6bb985c8c9-8h9hh edited $ kubectl get pod -o yaml | grep -i owner -A4 ownerReferences: - apiVersion: apps/v1 blockOwnerDeletion: false controller: true kind: ReplicaSet name: deployment-6bb985c8c9 uid: 14861de6-9073-42ee-ac13-1e87b1d263be # ReplicaSet $ kubectl get rs NAME DESIRED CURRENT READY AGE deployment-6bb985c8c9 1 1 1 8m8s $ kubectl edit rs deployment-6bb985c8c9 replicaset.apps/deployment-6bb985c8c9 edited $ kubectl get rs -o yaml | grep -i owner -A4 ownerReferences: - apiVersion: apps/v1 blockOwnerDeletion: false controller: true kind: Deployment name: deployment uid: 9a411b52-54e7-4a8a-bb4e-09be999cdbec
それでは Deployment を削除します。
$ gdate +"%Y-%m-%d %H:%M:%S.%3N"; kubectl delete deploy deployment --cascade=foreground; gdate +"%Y-%m-%d %H:%M:%S.%3N" 2022-02-26 21:37:51.581 deployment.apps "deployment" deleted 2022-02-26 21:37:51.950
get pod/rs/deploy のログは以下のようになりました。
# Pod $ kubectl get pod -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done 2022-02-26 21:37:10.680 NAME READY STATUS RESTARTS AGE 2022-02-26 21:37:10.683 deployment-6bb985c8c9-8h9hh 1/1 Running 0 9m57s 2022-02-26 21:37:51.955 deployment-6bb985c8c9-8h9hh 1/1 Terminating 0 10m 2022-02-26 21:38:22.868 deployment-6bb985c8c9-8h9hh 0/1 Terminating 0 11m 2022-02-26 21:38:28.068 deployment-6bb985c8c9-8h9hh 0/1 Terminating 0 11m 2022-02-26 21:38:28.075 deployment-6bb985c8c9-8h9hh 0/1 Terminating 0 11m # ReplicaSet $ kubectl get rs -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done 2022-02-26 21:37:11.632 NAME DESIRED CURRENT READY AGE 2022-02-26 21:37:11.635 deployment-6bb985c8c9 1 1 1 9m58s 2022-02-26 21:37:51.927 deployment-6bb985c8c9 1 1 1 10m 2022-02-26 21:37:51.934 deployment-6bb985c8c9 1 1 1 10m 2022-02-26 21:37:51.953 deployment-6bb985c8c9 1 1 1 10m # Deployment $ kubectl get deploy -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done 2022-02-26 21:37:12.989 NAME READY UP-TO-DATE AVAILABLE AGE 2022-02-26 21:37:12.992 deployment 1/1 1 1 9m59s 2022-02-26 21:37:51.865 deployment 1/1 1 1 10m 2022-02-26 21:37:51.889 deployment 1/1 1 1 10m 2022-02-26 21:37:51.933 deployment 1/1 1 1 10m
blockOwnerDeletion
フィールドが false
のため owner の削除はブロックされませんでした。そのため Deployment -> ReplicaSet -> Pod の順に削除されました。
true
の時は delete deploy コマンドが返ってくるまでに 32 秒かかりましたが、false
の時は Pod, ReplicaSet の削除を待たないので 1 秒未満で返ってきました。
1.3.2. finalizer
続いて finalizer について整理します。
finalizer とは、必要なリソースを誤って削除する事を防止するための機能です。Kubernetes にリソースの削除を命令すると、コントローラはリソースの finalizer ルールを処理します。
例えば、Pod が使用中の PersistentVolume(PV)
を削除しようとすると、PV は Terminating ステータスとなりますが削除はすぐには行われません。なぜなら PV は kubernetes.io/pv-protection
の finalizer を持っているからです。PV がどの Pod にもバウンドされなくなると finalizer がクリアされて PV が削除されます。
では、以下 manifest の PVC と Pod を作成して、PV の finalizer を確認してみます。
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc spec: storageClassName: gp2 resources: requests: storage: 10G accessModes: - ReadWriteOnce
apiVersion: v1 kind: Pod metadata: name: test-pvc-pod spec: containers: - name: amazonlinux image: public.ecr.aws/amazonlinux/amazonlinux:latest command: - "bin/bash" - "-c" - "sleep 3600" volumeMounts: - name: hoge mountPath: /hoge volumes: - name: hoge persistentVolumeClaim: claimName: test-pvc
それぞれ apply して PV の finalizer を確認します。
$ kubectl apply -f test-pvc.yaml persistentvolumeclaim/test-pvc created $ kubectl apply -f test-pvc-pod.yaml pod/test-pvc-pod created $ kubectl get pv -o jsonpath='{.items[*].metadata.finalizers}' ["kubernetes.io/pv-protection"]%
kubernetes.io/pv-protection
の finalizer があることを確認できました。この状態で PV を削除してみます。
$ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-66312388-5700-472a-a75c-9a94c2a472d4 10Gi RWO Delete Bound default/test-pvc gp2 16s $ kubectl delete pv pvc-66312388-5700-472a-a75c-9a94c2a472d4 persistentvolume "pvc-66312388-5700-472a-a75c-9a94c2a472d4" deleted
deleted と表示されたまま応答は返ってきませんでした。別ターミナルで get pv したら Terminating
状態になっていました。PV は削除されてないので、Pod にログインして PV がマウントされているディレクトリにファイルを作成する事もできました。
$ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-66312388-5700-472a-a75c-9a94c2a472d4 10Gi RWO Delete Terminating default/test-pvc gp2 44s $ kubectl get pod NAME READY STATUS RESTARTS AGE test-pvc-pod 1/1 Running 0 69s $ kubectl exec -it test-pvc-pod -- /bin/bash bash-4.2# touch /hoge/fuga bash-4.2# ls -lh /hoge/fuga -rw-r--r-- 1 root root 0 Feb 28 15:06 /hoge/fuga
kubernetes.io/pv-protection
finalizer により PV が削除できないことが確認できました。
1.3.3. カスケード削除
続いてカスケード削除について整理します。
owner オブジェクトが削除されると、ガベージコレクションにより従属オブジェクトも自動で削除されます。従属オブジェクトの自動削除をカスケード削除と言います。 finalizer を使用して、ガベージコレクションがいつどのように従属オブジェクトを削除するかコントロールできます。
カスケード削除には foreground と background の二種類があります。削除のオプションとして orphan もあるので並べて記載します。
タイプ | owner 削除時の挙動 |
---|---|
foreground | ・owner オブジェクトは deletion in progress ステータスになり、以下の状態となる。・オブジェクトに削除マークが付いた時間が metadata.deletionTimestamp フィールドにセットされる。・ metadata.finalizers フィールドに foregroundDeletion がセットされる。・削除処理が完了するまでオブジェクトは API から見える状態となる。 ・ deletion in progress ステータスに変わったら従属オブジェクトを削除する。全ての従属オブジェクトを削除したら owner オブジェクトを削除する。・カスケード削除の間、 ownerReference.blockOwnerDeletion=true フィールドを持つオブジェクトは owner の削除をブロックする。 |
background | ・すぐに owner オブジェクトを削除し、従属オブジェクトをバックグラウンドで削除する。 ・Kubernetes はデフォルトで background を使う。 |
orphan | ・owner オブジェクトだけ削除する ・ metadata.finalizers フィールドに orphan がセットされる・ownerReferences の無いオブジェクトは orphan(みなしご)オブジェクトと呼ばれる |
では、foreground カスケード削除の挙動を確認します。
上の blockOwnerDeletion
の検証で未確認の部分のみ確認します。Deployment を --cascade=foreground
で削除します。
$ kubectl gdate +"%Y-%m-%d %H:%M:%S.%3N"; kubectl delete deploy deployment --cascade=foreground; gdate +"%Y-%m-%d %H:%M:%S.%3N" 2022-02-26 22:39:00.299 deployment.apps "deployment" deleted 2022-02-26 22:39:32.592
削除中に別ターミナルで Deployment の metadata を確認します。
$ kubectl get deploy -o jsonpath='{.items[*].metadata.deletionTimestamp}' 2022-02-26T13:39:00Z% $ kubectl get deploy -o jsonpath='{.items[*].metadata.finalizers}' ["foregroundDeletion"]%
削除命令を実行した時間が metadata.deletionTimestamp
フィールドにセットされ、foregroundDeletion
finalizer がセットされることを確認できました。
次は background カスケード削除を確認します。デフォルトで background になるため delete コマンドに特にオプションを追加する必要はありません。先ほどと同様にタイムスタンプを表示します。
$ gdate +"%Y-%m-%d %H:%M:%S.%3N"; kubectl delete deploy deployment; gdate +"%Y-%m-%d %H:%M:%S.%3N" 2022-02-26 22:47:23.343 deployment.apps "deployment" deleted 2022-02-26 22:47:23.604
get pod/rs/deploy の結果です。
# Pod $ kubectl get pod -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done 2022-02-26 22:47:08.734 NAME READY STATUS RESTARTS AGE 2022-02-26 22:47:08.739 deployment-6bb985c8c9-lj5rs 1/1 Running 0 8s 2022-02-26 22:47:23.594 deployment-6bb985c8c9-lj5rs 1/1 Terminating 0 23s 2022-02-26 22:47:54.449 deployment-6bb985c8c9-lj5rs 0/1 Terminating 0 54s 2022-02-26 22:47:58.114 deployment-6bb985c8c9-lj5rs 0/1 Terminating 0 58s 2022-02-26 22:47:58.120 deployment-6bb985c8c9-lj5rs 0/1 Terminating 0 58s # ReplicaSet $ kubectl get rs -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done 2022-02-26 22:47:10.540 NAME DESIRED CURRENT READY AGE 2022-02-26 22:47:10.543 deployment-6bb985c8c9 1 1 1 11s 2022-02-26 22:47:23.578 deployment-6bb985c8c9 1 1 1 24s # Deployment $ kubectl get deploy -w | while read line; do echo -e "$(gdate +"%Y-%m-%d %H:%M:%S.%3N")\t $line"; done 2022-02-26 22:47:11.555 NAME READY UP-TO-DATE AVAILABLE AGE 2022-02-26 22:47:11.558 deployment 1/1 1 1 12s 2022-02-26 22:47:23.519 deployment 1/1 1 1 24s
background の場合はすぐに Deployment が削除されて、その後に ReplicaSet, Pod が削除された事が確認できました。なお、以下のように削除時に明示的に background を指定しても挙動は同じです。
kubectl delete deploy deployment --cascade=background
最後に orphan です。削除時に --cascade=orphan
を与える事で owner だけ削除し、従属オブジェクトを削除しないようにできます。
$ kubectl delete deploy deployment --cascade=orphan deployment.apps "deployment" deleted
# Pod $ kubectl get pod NAME READY STATUS RESTARTS AGE deployment-6bb985c8c9-jtbzh 1/1 Running 0 2m7s# ReplicaSet # ReplicaSet $ kubectl get rs NAME DESIRED CURRENT READY AGE deployment-6bb985c8c9 1 1 1 2m8s # Deployment $ kubectl get deployment No resources found in default namespace.
Deployment だけ削除されて、ReplicaSet, Pod は削除されない事が確認できました。
少し経つと ReplicaSet の ownerReferences
フィールドが消えました。ReplicaSet はまだ生きているので Pod の ownerReferences
はまだありました。
$ kubectl get rs -o jsonpath='{.items[*].metadata.ownerReferences}' $ kubectl get pod -o jsonpath='{.items[*].metadata.ownerReferences}' [{"apiVersion":"apps/v1","blockOwnerDeletion":true,"controller":true,"kind":"ReplicaSet","name":"deployment-6bb985c8c9","uid":"2c1258b9-6d78-40f7-9ca6-e19a0a12298c"}]%
1.4. 未使用のイメージ
1.4.1. 説明
kubelet は未使用のイメージを 5 分おきにガベージコレクションで削除しています。
※ https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet.go#L172 に 5 分と定義されている
ガベージコレクションを実施するかどうかは、以下の変数によって決まります。
変数 | kubelet フラグ | 説明 | デフォルト値 |
---|---|---|---|
imageMinimumGCAge | なし | 未使用イメージが削除される前に経過すべき最小時間 | 2m |
imageGCHighThresholdPercent | image-gc-high-threshold | イメージのガベージコレクションをトリガーするディスク使用量の割合(%) | 85 |
imageGCLowThresholdPercent | image-gc-low-threshold | イメージのガベージコレクションが解放を試みるディスク使用量の割合(%) | 80 |
つまり、デフォルトでは、イメージが未使用になってから 2 分経ったらガベージコレクションの削除対象となります。 そして、ディスク使用量が 85% を超えたらディスク使用量が 80% になるまでイメージを削除します。最後に使用された時間に基づいて最も古いイメージから削除していきます。
1.4.2. EKS の設定値
EKS の場合の上記変数の値を確認します。Node の情報なので Kubernetes の API サーバから取得できます。
kubectl proxy コマンドでプロキシをローカルに起動すると API サーバにアクセスできます。
$ kubectl proxy Starting to serve on 127.0.0.1:8001
デフォルトでローカルの 8001 ポートでプロキシが起動するので、別ターミナルで http://localhost:8001/api/v1/nodes/<Node 名>/proxy/configz
にアクセスします。json なので jq で整形すると見やすいです。
$ kubectl get node NAME STATUS ROLES AGE VERSION ip-10-0-102-42.ap-northeast-1.compute.internal Ready <none> 24h v1.21.5-eks-9017834 $ curl -sSL "http://localhost:8001/api/v1/nodes/ip-10-0-102-42.ap-northeast-1.compute.internal/proxy/configz" | jq . { "kubeletconfig": { "enableServer": true, "syncFrequency": "1m0s", "fileCheckFrequency": "20s", "httpCheckFrequency": "20s", "address": "0.0.0.0", "port": 10250, "tlsCipherSuites": [ "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_256_GCM_SHA384", "TLS_RSA_WITH_AES_128_GCM_SHA256" ], "serverTLSBootstrap": true, "authentication": { "x509": { "clientCAFile": "/etc/kubernetes/pki/ca.crt" }, "webhook": { "enabled": true, "cacheTTL": "2m0s" }, "anonymous": { "enabled": false } }, "authorization": { "mode": "Webhook", "webhook": { "cacheAuthorizedTTL": "5m0s", "cacheUnauthorizedTTL": "30s" } }, "registryPullQPS": 5, "registryBurst": 10, "eventRecordQPS": 5, "eventBurst": 10, "enableDebuggingHandlers": true, "healthzPort": 10248, "healthzBindAddress": "127.0.0.1", "oomScoreAdj": -999, "clusterDomain": "cluster.local", "clusterDNS": [ "172.20.0.10" ], "streamingConnectionIdleTimeout": "4h0m0s", "nodeStatusUpdateFrequency": "10s", "nodeStatusReportFrequency": "5m0s", "nodeLeaseDurationSeconds": 40, "imageMinimumGCAge": "2m0s", "imageGCHighThresholdPercent": 85, "imageGCLowThresholdPercent": 80, "volumeStatsAggPeriod": "1m0s", "cgroupRoot": "/", "cgroupsPerQOS": true, "cgroupDriver": "cgroupfs", "cpuManagerPolicy": "none", "cpuManagerReconcilePeriod": "10s", "memoryManagerPolicy": "None", "topologyManagerPolicy": "none", "topologyManagerScope": "container", "runtimeRequestTimeout": "2m0s", "hairpinMode": "hairpin-veth", "maxPods": 17, "podPidsLimit": -1, "resolvConf": "/etc/resolv.conf", "cpuCFSQuota": true, "cpuCFSQuotaPeriod": "100ms", "nodeStatusMaxImages": 50, "maxOpenFiles": 1000000, "contentType": "application/vnd.kubernetes.protobuf", "kubeAPIQPS": 5, "kubeAPIBurst": 10, "serializeImagePulls": false, "evictionHard": { "memory.available": "100Mi", "nodefs.available": "10%", "nodefs.inodesFree": "5%" }, "evictionPressureTransitionPeriod": "5m0s", "enableControllerAttachDetach": true, "protectKernelDefaults": true, "makeIPTablesUtilChains": true, "iptablesMasqueradeBit": 14, "iptablesDropBit": 15, "featureGates": { "RotateKubeletServerCertificate": true }, "failSwapOn": true, "containerLogMaxSize": "10Mi", "containerLogMaxFiles": 5, "configMapAndSecretChangeDetectionStrategy": "Watch", "kubeReserved": { "cpu": "70m", "ephemeral-storage": "1Gi", "memory": "442Mi" }, "enforceNodeAllocatable": [ "pods" ], "volumePluginDir": "/usr/libexec/kubernetes/kubelet-plugins/volume/exec/", "logging": { "format": "text" }, "enableSystemLogHandler": true, "shutdownGracePeriod": "0s", "shutdownGracePeriodCriticalPods": "0s", "enableProfilingHandler": true, "enableDebugFlagsHandler": true } }
59-61 行目にイメージのガベージコレクション関連のパラメータがあります。全てデフォルト値でした。Node のインスタンスタイプを変更しても値は同じでした。
1.4.3. 設定変更方法
デフォルト値だとディスク使用量が 85% に達するとガベージコレクションがトリガーされます。
サーバのディスク監視の閾値として warning は 80%, critical は 90% にしているケースがあったりすると思います。そのケースの場合は 80% を超えてアラートがなってもその時点では未使用イメージが自動で削除されないので手動で不要なデータを消すか、85% を超えてガベージコレクションがトリガーされるまで待つ必要があります。それは微妙なので imageGCHighThresholdPercent を 70%, imageGCLowThresholdPercent を 50% に変更してみます。
Node 上の設定ファイル(/etc/kubernetes/kubelet/kubelet-config.json や /etc/systemd/system/kubelet.service)を直接修正する事で設定変更が可能です。 Node 再作成時にも同様に設定されるようにするには、カスタム起動テンプレートかカスタム AMI を使用する必要があります。
Amazon EKS ワーカーノードを設定して特定のディスク使用率でイメージキャッシュをクリーンアップする
今回は、起動テンプレートのユーザデータに以下を記載することで設定を変更しました。
if ! grep -q imageGCHighThresholdPercent /etc/kubernetes/kubelet/kubelet-config.json; then sed -i '/"apiVersion*/a \ \ "imageGCHighThresholdPercent": 70,' /etc/kubernetes/kubelet/kubelet-config.json fi if ! grep -q imageGCLowThresholdPercent /etc/kubernetes/kubelet/kubelet-config.json; then sed -i '/"imageGCHigh*/a \ \ "imageGCLowThresholdPercent": 50,' /etc/kubernetes/kubelet/kubelet-config.json fi systemctl restart kubelet
設定が変更されていることを確認しました。
$ curl -sSL "http://localhost:8001/api/v1/nodes/ip-10-0-102-160.ap-northeast-1.compute.internal/proxy/configz" | jq . | grep imageGC "imageGCHighThresholdPercent": 70, "imageGCLowThresholdPercent": 50,
1.5. 未使用のコンテナ
kubelet は未使用のコンテナを 1 分おきにガベージコレクションで削除しています。
※ https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet.go#L170 に 1 分と定義されている
完了したコンテナは以下変数に基づいて最も古いものから順に削除されます。
変数 | 説明 | デフォルト値 |
---|---|---|
MinAge | ・完了したコンテナが削除される前に経過すべき最小時間 ・0 に設定すると無効 |
0 |
MaxPerPodContainer | ・Pod が持つことができる dead 状態のコンテナの最大値 ・0 未満に設定すると無効 |
1 |
MaxContainers | ・クラスタが持つことができる dead 状態のコンテナの最大値 ・0 未満に設定すると無効 |
-1 |
つまり、デフォルトでは、コンテナが完了するとすぐに削除対象となり、1 分以内にガベージコレクションにより削除が実行されます。
MaxPerPodContainer と MaxContainers はコンフリクトするケースがあります。Pod あたりの dead コンテナ数の合計がクラスタあたりの dead コンテナ数を超えるようなケースです。この場合、コンフリクトを修正するため kubelet は MaxPerPodContainer を調整します。(MaxContainers が優先される)
なお、設定確認方法はわかりませんでした。(API で取得した Node 情報や Node 上のファイルからは確認できなかった)
1.6. reclaimPolicy が Delete の StorageClass から動的にプロビジョニングされた PersistentVolume
まず、StorageClass の reclaimPolicy
とは、PersistentVolumeClaim(PVC) が削除された際に PersistentVolume(PV)を削除するか残すかを指定するパラメータです。削除の場合は Delete
、残す場合は Retain
を指定します。デフォルトは Delete
です。
PV が削除されると、AWS EBS/GCE PD/Azure Disk/Tencent CBS などの外部ボリュームも同様に削除されます。
では、PVC を削除して PV, EBS が自動で削除されることを確認します。
まずは EKS でデフォルトで使用できる StorageClass を確認します。
$ kubectl get storageclass -o yaml apiVersion: v1 items: - apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"storage.k8s.io/v1","kind":"StorageClass","metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"},"name":"gp2"},"parameters":{"fsType":"ext4","type":"gp2"},"provisioner":"kubernetes.io/aws-ebs","volumeBindingMode":"WaitForFirstConsumer"} storageclass.kubernetes.io/is-default-class: "true" creationTimestamp: "2022-02-28T08:29:50Z" name: gp2 resourceVersion: "254" uid: c97d773f-a438-4d3b-aee4-a9f9d660647f parameters: fsType: ext4 type: gp2 provisioner: kubernetes.io/aws-ebs reclaimPolicy: Delete volumeBindingMode: WaitForFirstConsumer kind: List metadata: resourceVersion: "" selfLink: ""
gp2 の StorageClass がデフォルトで用意されており、reclaimPolicy
は Delete
になっているのでこちらを使用します。
以下 manifest の PVC を作成します。
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc spec: storageClassName: gp2 resources: requests: storage: 10G accessModes: - ReadWriteOnce
apply して PVC が作成されることを確認します。
$ kubectl apply -f test-pvc.yaml
persistentvolumeclaim/test-pvc created
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
test-pvc Pending gp2 41s¨
PVC を関連付けた以下 manifest の Pod を作成します。
apiVersion: v1 kind: Pod metadata: name: test-pvc-pod spec: containers: - name: amazonlinux image: public.ecr.aws/amazonlinux/amazonlinux:latest command: - "bin/bash" - "-c" - "sleep 3600" volumeMounts: - name: hoge mountPath: /hoge volumes: - name: hoge persistentVolumeClaim: claimName: test-pvc
apply して Pod,PV が作成されることを確認します。
$ kubectl apply -f test-pvc-pod.yaml pod/test-pvc-pod created $ kubectl get pod NAME READY STATUS RESTARTS AGE test-pvc-pod 1/1 Running 0 24s $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-8369a57f-a42c-43b8-98a2-d39ed34af8fb 10Gi RWO Delete Bound default/test-pvc gp2 19s
指定したサイズ 10GB の EBS が作成されていることを確認します。
%aws ec2 describe-volumes --filters "Name=size,Values=10" { "Volumes": [ { "Attachments": [], "AvailabilityZone": "ap-northeast-1a", "CreateTime": "2022-02-28T14:45:40.745000+00:00", "Encrypted": false, "Size": 10, "SnapshotId": "", "State": "available", "VolumeId": "vol-0bd96ea93c9f4c2b7", "Iops": 100, "Tags": [ { "Key": "Name", "Value": "kubernetes-dynamic-pvc-f71826f1-0721-429e-a73d-df4afad32da0" }, { "Key": "kubernetes.io/cluster/ekstest", "Value": "owned" }, { "Key": "kubernetes.io/created-for/pvc/name", "Value": "test-pvc" }, { "Key": "kubernetes.io/created-for/pv/name", "Value": "pvc-f71826f1-0721-429e-a73d-df4afad32da0" }, { "Key": "kubernetes.io/created-for/pvc/namespace", "Value": "default" } ], "VolumeType": "gp2", "MultiAttachEnabled": false } ] }
リソースの準備ができたので PVC を削除したいのですが、上で説明した通り PV には kubernetes.io/pv-protection
finalizer があり Pod が Bound されていると PV を削除できないため、先に Pod を削除しておきます。
# Pod 削除 $ kubectl delete pod test-pvc-pod pod "test-pvc-pod" deleted # PVC 確認 $ kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE test-pvc Bound pvc-f71826f1-0721-429e-a73d-df4afad32da0 10Gi RWO gp2 2m15s # PV 確認 $ kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pvc-f71826f1-0721-429e-a73d-df4afad32da0 10Gi RWO Delete Bound default/test-pvc gp2 2m8s
PVC と PV だけになったので、PVC を削除してみます。
$ kubectl delete pvc test-pvc persistentvolumeclaim "test-pvc" deleted $ kubectl get pv No resources found $ aws ec2 describe-volumes --filters "Name=size,Values=10" { "Volumes": [] }
PV,EBS が共に削除されたことを確認できました。
1.7. 失効または期限切れの CertificatesSigningRequest(CSR)
Kubernetes の API サーバは X.509 クライアント証明書による認証をサポートしており、証明書を作成するためには Kubernetes の CSR オブジェクトが必要となります。 失効または有効期限切れの CSR オブジェクトはガベージコレクションにより削除されます。
CSR を作成するケースはあまり無い気がするので検証は割愛します。
なお、ガベージコレクションのドキュメントに記載されている「Stale or expired CertificateSigningRequests (CSRs)」のリンクは 404 エラーになります。
https://kubernetes.io/reference/access-authn-authz/certificate-signing-requests/#request-signing-process
1.8. 次のシナリオで削除された Node
cloud controller manager とは、Kubernetes が AWS/GCP 等のクラウドと連携するためのコンポーネントです。例えば ingress を作成すると AWS ALB を作成するといった処理を行っています。
EKS 等のマネージドサービスを使う場合は気にする必要はないので検証は割愛します。
1.9 Node Lease オブジェクト
Kubernetes ではクラスタが各 Node の可用性を判断し、障害時には何かアクションを起こせるように Node がクラスタにハートビートを送信しています。Lease はそのハートビート用のオブジェクトの一つです。
kube-node-lease
namespace に各 Node ごとの Lease オブジェクトが保存されています。
kubelet はデフォルトでは 10 秒間隔で Lease オブジェクトの作成と更新を行います。更新に失敗した場合、200 ミリ秒で開始し上限を 7 秒としたエクスポネンシャルバックオフを使用してリトライします。
実際の Lease オブジェクトを確認してみます。
$ kubectl get node NAME STATUS ROLES AGE VERSION ip-10-0-101-200.ap-northeast-1.compute.internal Ready <none> 5m38s v1.21.5-eks-9017834 ip-10-0-102-225.ap-northeast-1.compute.internal Ready <none> 5m51s v1.21.5-eks-9017834 ip-10-0-103-131.ap-northeast-1.compute.internal Ready <none> 5m48s v1.21.5-eks-9017834 $ kubectl get lease -n kube-node-lease NAME HOLDER AGE ip-10-0-101-200.ap-northeast-1.compute.internal ip-10-0-101-200.ap-northeast-1.compute.internal 5m46s ip-10-0-102-225.ap-northeast-1.compute.internal ip-10-0-102-225.ap-northeast-1.compute.internal 5m59s ip-10-0-103-131.ap-northeast-1.compute.internal ip-10-0-103-131.ap-northeast-1.compute.internal 5m56s $ kubectl describe lease ip-10-0-101-200.ap-northeast-1.compute.internal -n kube-node-lease Name: ip-10-0-101-200.ap-northeast-1.compute.internal Namespace: kube-node-lease Labels: <none> Annotations: <none> API Version: coordination.k8s.io/v1 Kind: Lease Metadata: Creation Timestamp: 2022-02-28T08:42:48Z Managed Fields: API Version: coordination.k8s.io/v1 Fields Type: FieldsV1 fieldsV1: f:metadata: f:ownerReferences: .: k:{"uid":"16da548f-1128-4c0a-9f00-499a815c7146"}: .: f:apiVersion: f:kind: f:name: f:uid: f:spec: f:holderIdentity: f:leaseDurationSeconds: f:renewTime: Manager: kubelet Operation: Update Time: 2022-02-28T08:42:48Z Owner References: API Version: v1 Kind: Node Name: ip-10-0-101-200.ap-northeast-1.compute.internal UID: 16da548f-1128-4c0a-9f00-499a815c7146 Resource Version: 3197 UID: 1a81eed3-8df4-43cf-ab32-e753b6e7b65f Spec: Holder Identity: ip-10-0-101-200.ap-northeast-1.compute.internal Lease Duration Seconds: 40 Renew Time: 2022-02-28T08:49:16.077127Z Events: <none>
Node ごとに Lease オブジェクトがあることが確認できました。
更新頻度を確認します。
$ kubectl get lease ip-10-0-101-200.ap-northeast-1.compute.internal -n kube-node-lease -o yaml -o jsonpath='{.spec.renewTime}' 2022-02-28T08:55:02.308638Z% $ kubectl get lease ip-10-0-101-200.ap-northeast-1.compute.internal -n kube-node-lease -o yaml -o jsonpath='{.spec.renewTime}' 2022-02-28T08:55:12.608059Z% $ kubectl get lease ip-10-0-101-200.ap-northeast-1.compute.internal -n kube-node-lease -o yaml -o jsonpath='{.spec.renewTime}' 2022-02-28T08:55:22.722818Z%
10 秒間隔で更新されていることが確認できました。
では、この Lease オブジェクトがどのようにガベージコレクションによって削除されるかというと、
ガベージコレクションのドキュメントには「Node lease オブジェクト」とあるだけで説明がないので何がどのように削除されるのか不明でした。
※以前削除した Node の Lease オブジェクトが 100 日以上残っていたので Node を削除しても Lease オブジェクトは削除されなさそうです
2. まとめ
Kubernetes のガベージコレクションについて整理しました。量が多いですね。
EKS 等のマネージドサービスの場合はあまり気にする点はなかったです。が、kubelet の変数(イメージ GC の閾値等)はもっと柔軟に変更できるようになると嬉しいですね。
あと、公式ドキュメントのリンクが切れてたり情報が少なかったり古い場合があったのが微妙でした。
3. 参考
Garbage Collection | Kubernetes
kube-controller-manager | Kubernetes
Automatic Clean-up for Finished Jobs | Kubernetes
Owners and Dependents | Kubernetes
Use Cascading Deletion in a Cluster | Kubernetes
Kubelet Configuration (v1beta1) | Kubernetes
Persistent Volumes | Kubernetes