APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

AKSのcgroupを覗いてみよう

はじめに

あけましておめでとうございます。
ACS事業部の谷合です。
年末年始休みに、ぼけーっとAKSのChangelogを眺めていたら、以下のアナウンスを見つけました。

Starting Kubernetes 1.29, the default cgroups implementation on Azure Linux AKS nodes will be cgroupsv2. Older versions of Java, .NET and NodeJS do not support memory querying v2 memory constraints and this will lead to out of memory (OOM) issues for workloads. Please test your applications for cgroupsv2 compliance, and read the FAQ for cgroupsv2.

どうやら、Kubernetes(以降K8s) 1.29からAKSのOS SKUがAzure Linuxのノードも、cgroup v2がデフォルトで使われるようになるようです。
Ubuntuノードは1.25からすでにcgroup v2が使われていました。
恥ずかしながら、Ubuntuノードもcgroup v1だと思っていたのでは内緒です。

1.29でAKSが完全にcgroup v2になる前に、調査せねばとの思いからこのブログを書いてみました。
cgroupを知っている方も、知らない方もAKSではcgroupがどう使われているかをこのブログで知ってもらえたら嬉しいです。

cgroupとは?

cgroup(Control Group)とは、Linuxのプロセスをグループ化して、共通のリソース制限をかけます。 cgroupにはCPUやメモリ、デバイスなど管理リソースごとにcontrollerがあり、controller経由でリソースに制限を設けています。

gihyo.jp

K8sでは1.25からcgroup v1からv2に変更されました。

kubernetes.io

現状K8s 1.28ではAlphaですがMemoryQoS機能で、従来のresourece.memory.limitsだけでなく、memory.high(requests.memory)やmemory.min(limits.memoryの90%)が設定でき、より柔軟なメモリ制御ができるようになっています。
K8sでのmemory.minとmemory.highについては以下の公式ブログを参照ください。

kubernetes.io

なお、cgroup v2を使用しているかは、以下のコマンドで確認できます。
cgroup v2でない場合は、tempfs と出力されます。

# stat -fc %T /sys/fs/cgroup/
cgroup2fs

cgroup-v2-doc/cgroup-v2.md at main · tenforward/cgroup-v2-doc · GitHub

cgroup v2のresource管理

軽くcgroupの階層構造を見てみましょう。
K8sでは、Guaranteed、Burstable、BestEffortがQuality of Service classes(QoSクラス)として定められています。
これは、ノードのメモリが足りなくなった場合に、どのようにEvictされるかの設定となります。
これは本ブログの主題とは若干離れるため、今回は説明は割愛します。
もしQoSクラスをもっと知りたい場合は以下の公式ドキュメントを参照ください。

kubernetes.io

cgroup v2ではQoSクラスごとに、階層を形成し、リソースの制限しています。
各QoSクラスごとの階層構造は以下の通りとなっています。

Guaranteed:
/sys/fs/cgroup/kubepods.slice/kubepods-pod[POD UID].slice/cri-containerd-[some ID].scope
Burstable:
/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod[POD UID]/cri-containerd-[some ID].scope
BestEffort:
/sys/fs/cgroup/kubepods.slice/kubepods-besteffort.slice/kubepods-besteffort-pod[POD UID]/cri-containerd-[some ID].scope

まずは以下のドキュメントのコマンドを使用し、AKSノードに接続しましょう。

learn.microsoft.com

$ kubectl debug node/ノード名 -it --image=mcr.microsoft.com/dotnet/runtime-deps:6.0
Creating debugging pod node-debugger-aks-userpool-37287841-vmss000001-v45js with container debugger on node aks-userpool-37287841-vmss000001.
If you don't see a command prompt, try pressing enter.
root@ノード名:/#

接続後は、以下のコマンドcgroup v2を使用しているかを確認できます。
cgroup v2出ない場合は、tempfs と出力されます。

$ stat -fc %T /host/sys/fs/cgroup/
cgroup2fs

なお、上記コマンドは/host配下を見ています。
kubectl debugコマンドでは、Podの/hostに、ノードの/ディレクトリをmountしています。
そのため、AKSノードの情報を確認する場合は、/hostからパス指定をする必要があります。

次に、Podをデプロイして、cgroupの階層を辿るために必要な情報を取得します。
この時、検証用に.spec.resources.limitsやrequestsを設定してください。

$ cat << EOT | k apply -f -
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: nginx
  name: nginx
spec:
  containers:
  - image: nginx
    name: nginx
    resources: 
      limits:
        cpu: 500m
        memory: "250Mi"
      requests:
        cpu: 250m
        memory: "150Mi"
EOT

# QoSクラス
$ kubectl get pod nginx -ojsonpath='{.status.qosClass}{"\n"}'
Burstable

# PodのUID確認
$ kubectl get pod nginx -ojsonpath='{.metadata.uid}{"\n"}'
8b6c1eb3-12fe-4fcc-a206-af45b128523f

# ContainerのID確認
$ kubectl get pod nginx -ojsonpath='{.status.containerStatuses[0].containerID}{"\n"}'
containerd://2c94fd56ab8baa12f7ad42bd72d9b5b275fba43acc90142a9c7da09ab937e6af

これをパスに当てはめると、以下のようになります。

/host/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8b6c1eb3_12fe_4fcc_a206_af45b128523f.slice/cri-containerd-2c94fd56ab8baa12f7ad42bd72d9b5b275fba43acc90142a9c7da09ab937e6af.scope

このパスに移動して、CPUとMemoryのControllerのファイルを確認してみましょう。

root@aks-userpool-37287841-vmss000001:/host/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod8b6c1eb3_12fe_4fcc_a206_af45b128523f.slice/cri-containerd-2c94fd56ab8baa12f7ad42bd72d9b5b275fba43acc90142a9c7da09ab937e6af.scope# ls -l | grep -e cpu -e memory 
-rw-r--r-- 1 root root 0 Jan  4 10:26 cpu.idle
-rw-r--r-- 1 root root 0 Jan  4 10:26 cpu.max
-rw-r--r-- 1 root root 0 Jan  4 10:26 cpu.max.burst
-rw-r--r-- 1 root root 0 Jan  4 10:26 cpu.pressure
-r--r--r-- 1 root root 0 Jan  4 10:26 cpu.stat
-rw-r--r-- 1 root root 0 Jan  4 10:26 cpu.uclamp.max
-rw-r--r-- 1 root root 0 Jan  4 10:26 cpu.uclamp.min
-rw-r--r-- 1 root root 0 Jan  4 10:26 cpu.weight
-rw-r--r-- 1 root root 0 Jan  4 10:26 cpu.weight.nice
-rw-r--r-- 1 root root 0 Jan  4 10:26 cpuset.cpus
-r--r--r-- 1 root root 0 Jan  4 10:26 cpuset.cpus.effective
-rw-r--r-- 1 root root 0 Jan  4 10:26 cpuset.cpus.partition
-rw-r--r-- 1 root root 0 Jan  4 10:26 cpuset.mems
-r--r--r-- 1 root root 0 Jan  4 10:26 cpuset.mems.effective
-r--r--r-- 1 root root 0 Jan  4 10:26 memory.current
-r--r--r-- 1 root root 0 Jan  4 10:26 memory.events
-r--r--r-- 1 root root 0 Jan  4 10:26 memory.events.local
-rw-r--r-- 1 root root 0 Jan  4 10:26 memory.high
-rw-r--r-- 1 root root 0 Jan  4 10:26 memory.low
-rw-r--r-- 1 root root 0 Jan  4 10:26 memory.max
-rw-r--r-- 1 root root 0 Jan  4 10:26 memory.min
-r--r--r-- 1 root root 0 Jan  4 10:26 memory.numa_stat
-rw-r--r-- 1 root root 0 Jan  4 10:26 memory.oom.group
-rw-r--r-- 1 root root 0 Jan  4 10:26 memory.pressure
-r--r--r-- 1 root root 0 Jan  4 10:26 memory.stat
-r--r--r-- 1 root root 0 Jan  4 10:26 memory.swap.current
-r--r--r-- 1 root root 0 Jan  4 10:26 memory.swap.events
-rw-r--r-- 1 root root 0 Jan  4 10:26 memory.swap.high
-rw-r--r-- 1 root root 0 Jan  4 10:26 memory.swap.max

早速CPUとメモリの制限を確認してみましょう。

Podでデプロイ時に指定した.spec.containers[].resourcesは以下の通りになっていました。

resources: 
  limits:
    cpu: 500m
    memory: "250Mi"
  requests:
    cpu: 250m
    memory: "150Mi"

cgroupでは、以下のようにPodのresources.limitsの値が見えると思います。
以下のことが分かると思います。

  • CPUは、limitsの値から、1000mまで読み書きできる
  • メモリは250Mi(262144000/1024/1024)
# cat cpu.max       
50000 100000

# cat memory.max 
262144000

上記のようにcgroupでは、Podの.spec.containers[].resource.limitsの値で制限をかけていることが分かります。
K8sでは、Podに割り当てられたcgroupによってCPUやメモリなどのリソースを厳しく制御しているのです。

Out-Of-Memory時の挙動

ここまでで、cgroup v2でのリソース制御を見てきました。
ここからは、メモリに焦点を当てて、Out-Of-Memory(以降OOM)の挙動を見ていきましょう。

まずは、Podをデプロイします。
この時、limits.memoryを2Miなどの小さい値にして、OOMを起こしやすくします。

$ cat << EOT | k apply -f -
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: nginx
  name: cgroup-test
spec:
  containers:
  - image: nginx
    name: nginx
    resources: 
      limits:
        cpu: 500m
        memory: "2Mi"
      requests:
        cpu: 250m
        memory: "1Mi"
EOT
pod/cgroup-test created

すると、Eventリソースからcgroupによって、OOMが起きているのが確認できるかと思います。

$ kubectl get events --sort-by=.metadata.creationTimestamp
LAST SEEN   TYPE      REASON                   OBJECT                                  MESSAGE
3m56s       Normal    Scheduled                pod/cgroup-test                         Successfully assigned default/cgroup-test to aks-userpool-37287841-vmss000001
3m56s       Warning   OOMKilling               node/aks-userpool-37287841-vmss000001   Memory cgroup out of memory: Killed process 3107606 (runc:[2:INIT]) total-vm:1091532kB, anon-rss:2312kB, file-rss:6636kB, shmem-rss:0kB, UID:0 pgtables:148kB oom_score_adj:-998
9s          Warning   FailedCreatePodSandBox   pod/cgroup-test                         Failed to create pod sandbox: rpc error: code = Unknown desc = failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: container init was OOM-killed (memory limit too low?): unknown
3m43s       Warning   OOMKilling               node/aks-userpool-37287841-vmss000001   Memory cgroup out of memory: Killed process 3108094 (runc:[2:INIT]) total-vm:1091020kB, anon-rss:2340kB, file-rss:6568kB, shmem-rss:0kB, UID:0 pgtables:148kB oom_score_adj:-998
3m30s       Warning   OOMKilling               node/aks-userpool-37287841-vmss000001   Memory cgroup out of memory: Killed process 3108632 (runc:[2:INIT]) total-vm:1091020kB, anon-rss:2316kB, file-rss:6864kB, shmem-rss:0kB, UID:0 pgtables:148kB oom_score_adj:-998
3m15s       Warning   OOMKilling               node/aks-userpool-37287841-vmss000001   Memory cgroup out of memory: Killed process 3109153 (runc:[2:INIT]) total-vm:1091276kB, anon-rss:2324kB, file-rss:6480kB, shmem-rss:0kB, UID:0 pgtables:152kB oom_score_adj:-998
3m4s        Warning   OOMKilling               node/aks-userpool-37287841-vmss000001   Memory cgroup out of memory: Killed process 3109661 (runc:[2:INIT]) total-vm:1164752kB, anon-rss:2312kB, file-rss:6664kB, shmem-rss:0kB, UID:0 pgtables:164kB oom_score_adj:-998
2m52s       Warning   OOMKilling               node/aks-userpool-37287841-vmss000001   Memory cgroup out of memory: Killed process 3110070 (runc:[2:INIT]) total-vm:1091020kB, anon-rss:2332kB, file-rss:6592kB, shmem-rss:0kB, UID:0 pgtables:152kB oom_score_adj:-998
2m41s       Warning   OOMKilling               node/aks-userpool-37287841-vmss000001   Memory cgroup out of memory: Killed process 3110463 (runc:[2:INIT]) total-vm:1091020kB, anon-rss:2320kB, file-rss:6788kB, shmem-rss:0kB, UID:0 pgtables:152kB oom_score_adj:-998
2m29s       Warning   OOMKilling               node/aks-userpool-37287841-vmss000001   Memory cgroup out of memory: Killed process 3111474 (runc:[2:INIT]) total-vm:1091276kB, anon-rss:2324kB, file-rss:6436kB, shmem-rss:0kB, UID:0 pgtables:148kB oom_score_adj:-998
2m13s       Warning   OOMKilling               node/aks-userpool-37287841-vmss000001   Memory cgroup out of memory: Killed process 3112023 (runc:[2:INIT]) total-vm:1091020kB, anon-rss:2324kB, file-rss:6808kB, shmem-rss:0kB, UID:0 pgtables:148kB oom_score_adj:-998
9s          Warning   OOMKilling               node/aks-userpool-37287841-vmss000001   (combined from similar events): Memory cgroup out of memory: Killed process 3117354 (runc:[2:INIT]) total-vm:1091020kB, anon-rss:2336kB, file-rss:6732kB, shmem-rss:0kB, UID:0 pgtables:152kB oom_score_adj:-998

次にノードの接続して、cgroupを見てみます。

まずcgroupの階層を辿るための情報を集めます。

# QoSクラス
$ kubectl get pod cgroup-test -ojsonpath='{.status.qosClass}{"\n"}'
Burstable

# PodのUID確認
$ kubectl get pod cgroup-test -ojsonpath='{.metadata.uid}{"\n"}'
0595fa18-031c-406f-b1af-87411176003d

# ContainerのID確認(起動していないためなし)
$ kubectl get pod cgroup-test -ojsonpath='{.status.containerStatuses[0].containerID}{"\n"}'

これをパスに当てはめると、以下のようになります。

/host/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod0595fa18_031c_406f_b1af_87411176003d.slice/

次に先ほどcgroup v2のresource管理で、作成したdebug用Podでノードに接続し、syslogを確認します。

$ kubectl exec node-debugger-aks-userpool-37287841-vmss000001-v45js -it -- bash

# cat /host/var/log/syslog | grep oom
Jan  5 12:34:23 aks-userpool-37287841-vmss000001 kernel: [110381.489836] oom-kill:constraint=CONSTRAINT_MEMCG,nodemask=(null),cpuset=cri-containerd-af79d19de33da0791a84166851eb29a1f4cc683b479dd5fff4dad0290dfb98e8.scope,mems_allowed=0,oom_memcg=/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod0595fa18_031c_406f_b1af_87411176003d.slice,task_memcg=/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod0595fa18_031c_406f_b1af_87411176003d.slice/cri-containerd-af79d19de33da0791a84166851eb29a1f4cc683b479dd5fff4dad0290dfb98e8.scope,task=runc:[2:INIT],pid=3147714,uid=0
Jan  5 12:34:23 aks-userpool-37287841-vmss000001 kernel: [110381.489856] Memory cgroup out of memory: Killed process 3147714 (runc:[2:INIT]) total-vm:1091020kB, anon-rss:2316kB, file-rss:6860kB, shmem-rss:0kB, UID:0 pgtables:152kB oom_score_adj:-998

上記より、以下のcgroupで、OOMが発生していたことが確認できます。
これは、cgroup-test Podデプロイ時のQoSクラス、Pod UIDを当てはめたパスと同一になるはずです。

/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod0595fa18_031c_406f_b1af_87411176003d.slice/

cgroupに移動し、メモリの状況を確認してみましょう。
メモリのmaxが2Mi(2097152/1024/1024)になっていることが確認できます。

# ls -l | grep memory
-r--r--r-- 1 root root 0 Jan  5 12:18 memory.current
-r--r--r-- 1 root root 0 Jan  5 12:18 memory.events
-r--r--r-- 1 root root 0 Jan  5 12:18 memory.events.local
-rw-r--r-- 1 root root 0 Jan  5 12:18 memory.high
-rw-r--r-- 1 root root 0 Jan  5 12:18 memory.low
-rw-r--r-- 1 root root 0 Jan  5 12:18 memory.max
-rw-r--r-- 1 root root 0 Jan  5 12:18 memory.min
-r--r--r-- 1 root root 0 Jan  5 12:18 memory.numa_stat
-rw-r--r-- 1 root root 0 Jan  5 12:18 memory.oom.group
-rw-r--r-- 1 root root 0 Jan  5 12:18 memory.pressure
-r--r--r-- 1 root root 0 Jan  5 12:18 memory.stat
-r--r--r-- 1 root root 0 Jan  5 12:18 memory.swap.current
-r--r--r-- 1 root root 0 Jan  5 12:18 memory.swap.events
-rw-r--r-- 1 root root 0 Jan  5 12:18 memory.swap.high
-rw-r--r-- 1 root root 0 Jan  5 12:18 memory.swap.max

# cat memory.max 
2097152

メモリに起きたイベントはmemory.eventsファイルに記録されます。

# cat memory.events
low 0
high 0
max 2635
oom 126
oom_kill 126

このファイルから以下のことが分かります。

  • メモリ使用量が制限を超えようとした回数(max)が2635回
  • 制限に達し、割り当てに失敗した回数(oom)が126回
  • cgroupに属するプロセスがOOM killer によって kill された回数(oom_kill)が126回

ちなみに、memory.oom.groupという機能もあります。
これは、K8s 1.28から導入された機能で、OOMが発生した際に、コンテナ全体を停止させる機能となります。
こちらについては、以下の記事でうまくまとめてくれていますので、興味ある方は是非ご参照ください。

qiita.com

AKSとcgroup

ここまで、controlplane以外のAKS内部のcgroupを見てきましたが、正直バニラなK8sと使われ方は変わりはないなと思いました。(齟齬があればご指摘ください!)
AKSでもOOMなどのリソース制限時には、K8sのドキュメントなどから、cgroupの深堀りや、トラシューができるかと思います。
ただし、cgroupとは? にも記載の通り、MemoryQoS機能はバニラなK8sでも1.28時点でAlphaなため、AKSではまだサポートされておらず、FeatureGateでの有効化もできません。
そのため、cgroup v2の新機能である、memory.minとmemory.highは設定されず、cgroup v1相当の状態となっています。
MemoryQoS機能のGAが待たれますね。

さいごに

いかがでしたでしょうか?
AKSのcgroupについてDeepDiveできたかと思います。
cgroupでは、メモリ以外のCPUなどのリソース制限も行っています。
Azure Linuxがcgroup v2になった際の導入検証を行う際は、この記事を参照いただけたら嬉しいです。
是非是非皆さんもAKSをDeepDiveしてみましょう!

ACS事業部のご紹介

私達ACS事業部はAzure・AKSなどのクラウドネイティブ技術を活用した内製化やGitHub Enterpriseの導入のご支援をしております。
www.ap-com.co.jp www.ap-com.co.jp また、一緒に働いていただける仲間も募集中です!
今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。 www.ap-com.co.jp

本記事の投稿者: 谷合純也
AKS/ACA、GitHubをメインにインフラ系のご支援を担当しています。
junya0530さんの記事一覧 | Zenn