YasuBlog

中年インフラエンジニアの備忘録です。

Kubernetes のガベージコレクション

KubernetesガベージコレクションGC) について整理し、挙動を確認しました。

1. Kubernetesガベージコレクション

Kubernetesガベージコレクションクラスタリソースを掃除する以下仕組みの総称です。

  • Failed 状態の Pod
  • 終了した Job
  • owner reference の無いオブジェクト
  • 未使用のイメージ
  • 未使用のコンテナ
  • reclaimPolicy が Delete の StorageClass から動的にプロビジョニングされた PersistentVolume
  • 失効または期限切れの CertificatesSigningRequest(CSR
  • 次のシナリオで削除された Node
    • クラスタが cloud controller manager を使う場合のクラウド環境
    • クラスタが cloud controller manager と同様のアドオンを使用する場合のオンプレ環境
  • 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-managerterminated-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;
2022223日 水曜日 002803秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         4s
2022223日 水曜日 002805秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         5s
2022223日 水曜日 002806秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         7s
2022223日 水曜日 002808秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         9s
2022223日 水曜日 002809秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         10s
2022223日 水曜日 002811秒 JST
NAME       COMPLETIONS   DURATION   AGE
test-job   1/1           3s         12s
2022223日 水曜日 002813秒 JST
No resources found in default namespace.
2022223日 水曜日 002814秒 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 フィールドも持ちます。これは truefalse を値に持つことができ、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 で blockOwnerDeletiontrue から 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 の情報なので KubernetesAPI サーバから取得できます。

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 がデフォルトで用意されており、reclaimPolicyDelete になっているのでこちらを使用します。

以下 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

KubernetesAPI サーバは X.509 クライアント証明書による認証をサポートしており、証明書を作成するためには KubernetesCSR オブジェクトが必要となります。 失効または有効期限切れの 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 を使う場合のクラウド環境
  • クラスタが cloud controller manager と同様のアドオンを使用する場合のオンプレ環境

cloud controller manager とは、KubernetesAWS/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

Pod Lifecycle | Kubernetes

kube-controller-manager | Kubernetes

Automatic Clean-up for Finished Jobs | Kubernetes

Owners and Dependents | Kubernetes

ObjectMeta | Kubernetes

Use Cascading Deletion in a Cluster | Kubernetes

Kubelet Configuration (v1beta1) | Kubernetes

Persistent Volumes | Kubernetes

Cloud Controller Manager | Kubernetes

Nodes | Kubernetes