Upgrade to Pro — share decks privately, control downloads, hide ads and more …

story about confronting node failure due to excessive memory consumption in kubelet

story about confronting node failure due to excessive memory consumption in kubelet

Kota Kimura

April 01, 2024
Tweet

More Decks by Kota Kimura

Other Decks in Technology

Transcript

  1. 自己紹介
 木村 洸太 - Kota Kimura • 株式会社サイバーエージェント グループIT推進本部 CIU所属

    2023年5月中途入社 • KaaS プロダクト AKE の開発・運用を担当 • 趣味はサウナ巡り @ KKimura
  2. AKE - Kubernetes as a Service
 • ユーザーが指定の k8s バージョンでクラスタを利用できる

    → ClusterAPI で実現 管理クラスタ ユーザークラスタ v1.26.5 v1.27.1 v1.25.10 プライベートクラウド版 マネージド Kubernetes kind: Cluster metadata: name: test-cluster-01 spec: topology: version: v1.27.1 controleplane: replicas: 3 workers: machinedeployments: - class: type1-workers name: cpu-nodepool-1 replicas: 3 kind: Cluster metadata: name: test-cluster-01 spec: topology: version: v1.27.1 controleplane: replicas: 3 workers: machinedeployments: - class: type1-workers name: cpu-nodepool-1 replicas: 3
  3. AKE - Kubernetes as a Service
 • 各クラスタで利用するアドオンの Enable /

    Disable が切り替え可能 → ArgoCD で実現 管理クラスタ ユーザークラスタ External DNS Cluster Autoscaler, nginx-ingress Prometheus, External Secret プライベートクラウド版 マネージド Kubernetes kind: ApplicationSet metadata: name: app spec: generators: - clusters: {} template: metadata: name: 'app-{{ name }}' spec: project: default source: repoURL: [email protected]:xxxxx/addons.git targetRevision: main path: manifests/example-app/overlays/release destination: server: '{{ server }}'
  4. AKE - Kubernetes as a Service
 AKE についてより詳しく知りたい方は、以下の資料などを参照ください AKE全般 ArgoCD

    https://speakerdeck.com/kkk777/k8s-ecosystem-management-with-using-of-argo-cd https://speakerdeck.com/masayaaoyama/cainfra01-amsy810-kubernetes
  5. 初動対応
 Kernel の問題などノード自体に問題がある場合や、 ノード上で起動しているプロセスに関連するアラートの時 • ps コマンドでプロセス状況を確認 • top コマンドでリソース使用量を確認

    • systemctl status xxxx, journalctl xxxx でコンポーネントのログ等を確認 • /var/log/message を確認 sshd が落ちているのか?、 該当ノード に ssh できない!!! OpenStack コンソールから VM にログインできるか確認したが、 右のログが大量に出力されており、ログインもできなかった → 5分から10分ほどで自動復旧
  6. kubelet が Not Ready になっている原因の仮説
 1. CNI Pod が落ちたり?など、ネットワーク的に Not

    Ready 状態になっているのでは? 2. CPU / Memory などノードが高負荷状態に陥ってるのでは? 使用割合 󰣻 → kubectl get pod、kubectl describe pod したが、特に問題なさそう → Grafana ダッシュボードから対象ノードを調査 定期的にノードの上限までメモリを使用していることを発見
  7. kubelet が Not Ready になっている原因の仮説 
 ユーザー Pod の resource.limits

    の設定漏れがあり、 Memory 過剰利用している Pod がいるのではないか? → 該当 Node に載っている Pod 全てに適切に設定がされていて、問題なさそう ノード上に起動しているプロセスがメモリを過剰利用しているのではないか? → kubelet が CPU / Memory ともに過剰消費している
  8. 調査
 kubelet の journal ログを確認すると... I1108 00:20:27.240296 2907284 pod_container_manager_linux.go:210] "Failed

    to delete cgroup paths" cgroupName=[kubepods burstable pod1495abbc-> I1108 00:20:27.240943 2907284 pod_container_manager_linux.go:210] "Failed to delete cgroup paths" cgroupName=[kubepods podbff40d3b-2852-41fa-> I1108 00:20:27.241207 2907284 pod_container_manager_linux.go:210] "Failed to delete cgroup paths" cgroupName=[kubepods podeb98897f-ce7f-4fba-> I1108 00:20:27.242681 2907284 pod_container_manager_linux.go:210] "Failed to delete cgroup paths" cgroupName=[kubepods podda588b94-30dc-4678-> Pod の cgroup を削除しようとして、失敗しているエラーが大量に発生していた
  9. この時点で分かったこと
 kubelet が CPU / Memory を過剰消費していて、 Pod の cgroup

    の削除に失敗している ノードのメモリを食い潰してしまう何らかのプロセスにより、 メモリ高負荷状態に陥り、kubelet down になっている
  10. cgroup
 プロセスをグループ化して、グループごとにホストOSが持つ CPU / Memory などの リソースの使用量を制限する Linux の機能 •

    cgroupfs というファイルシステムを介して操作を行う ( /proc のようなイメージ) ※1 • 扱うリソースごとに サブシステム と呼ぶ独立した機能でリソースを扱う サブシステム cpu memory blkio net_cls 機能 CPUのスケジューリングを制御 グループ内のタスクが消費するメモリを制限 グループ内のプロセスが発信するパケットを制御 ブロックデバイスに対する制限 /sys/fs/cgroup /myapp /myapp2 /aaa /aaa memory を 30% 割り当て memory を 20% 割り当て memory を 10% 割り当て memory を 70% 割り当て ※1 現在では systemd などの cgroup を管理するエージェントに操作を依頼して扱うことがほとんど
  11. cgroup v1
 cgroup は v1, v2 と異なるバージョンがあり、階層構造の違いなどがある • cgroup v1

    は複数階層を持てる • 階層ごとにサブシステムを紐づける ※ サブシステムは 1階層でしか存在できない memory プロセス /sys/fs/cgroup cgroup cpu blkio cgroup cgroup cgroup プロセス プロセス cgroup プロセス プロセス プロセス 階層1 階層2 mount mount $ mount -t cgroup -o cpu,memory cgroup /sys/fs/cgroup/test
  12. cgroup v1
 cgroup v1 の課題 • 複数階層は柔軟に見えるが、サブシステムは同時に1階層にしか所属できないため、柔軟性が低い • サブシステム間の連携の難しさやサブシステム間の一貫性の欠如など、内部実装の複雑化 多くのケースで、サブシステムごとにディレクトリを切り

    cgroup をマウントして 利用することがほとんどな印象 プロセス /sys/fs/cgroup cpu cpu devices devices cgroup cgroup プロセス 階層1 階層N mount mount プロセス memory memory cgroup 階層2 mount blkio blkio cgroup プロセス 階層N-1 mount …
  13. cgroup v2
 cgroup v2 からは、単一階層構造を採用 利用できるサブシステムは、 `/sys/fs/cgroup/cgroup.controllers` にリスト化される $ cat

    /sys/fs/cgroup/cgroup.controllers cpuset cpu io memory hugetlb pids rdma misc 子階層で利用させたいサブシステムは、 `/sys/fs/cgroup/cgroup.subtree_control` に指定する cgroup root cgroup test1 cgroup test2 cgroup.controllers cpuset cpu io memory cgroup.subtree_control cpuset cpu memory cgroup.controllers cpuset cpu memory cgroup.subtree_control memory cgroup.controllers memory
  14. k8s における cgroup (v1)
 apiVersion: v1 kind: Pod metadata: name:

    myapp spec: containers: - name: myapp image: nginx resources: limits: memory: 100Mi requests: memory: 100Mi 各サブシステムの階層配下で、以下のディレクトリ構造で cgroup を管理 ex) /sys/fs/cgroup/memory/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod7e165ea0_bd0c_4697_9404_31028a6a325f.slice apply Memory サブシステムの 該当の container ID の memory.limit_in_bytes に 100 Mi が書き込まれる = 利用可能なメモリ量 100 Mi に制限 /sys/fs/cgroup memory devices … memory mount Granteed Burstable BestEffort Pod UID Pod UID Container ID Container ID Container ID e.g. QoSクラス
  15. 調査 (再掲)
 kubelet の journal ログを確認すると... I1108 00:20:27.240296 2907284 pod_container_manager_linux.go:210]

    "Failed to delete cgroup paths" cgroupName=[kubepods burstable pod1495abbc-> I1108 00:20:27.240943 2907284 pod_container_manager_linux.go:210] "Failed to delete cgroup paths" cgroupName=[kubepods podbff40d3b-2852-41fa-> I1108 00:20:27.241207 2907284 pod_container_manager_linux.go:210] "Failed to delete cgroup paths" cgroupName=[kubepods podeb98897f-ce7f-4fba-> I1108 00:20:27.242681 2907284 pod_container_manager_linux.go:210] "Failed to delete cgroup paths" cgroupName=[kubepods podda588b94-30dc-4678-> Pod の cgroup を削除しようとして、失敗しているエラーが大量に発生していた 捜索隊結成の瞬間
  16. 調査
 systemd-cgls memory Controller memory; Control group /: └─kubepods.slice ├─kubepods-pod71d1febb_b6ca_491d_97cb_4340a4e7ca51.slice

    │ ├─cri-containerd-156fa789e2773919c353c9d92199e3d4649f8a173f1041e8d534211fc7725d6c.scope │ ├─cri-containerd-e2c1ff44e50abe1b03f90186fec5fbd63904dd2056d2413db72916c2cbb2f851.scope │ ├─cri-containerd-fd1798f09401c00a044353f04b67c8b86349b4a64df3209d288a745762134213.scope │ └─cri-containerd-aded3f852faa31cf3dc19d61806bd22a595f4526b85926dfd255bd0f4e7a561a.scope └─kubepods-burstable.slice ├─kubepods-burstable-podba758fe9_e5df_4f21_822d_82dd8e69d490.slice │ ├─cri-containerd-759b532fbbbc505f0a4b8087189a417bfba132ba0fd980376513b95362279962.scope │ ├─cri-containerd-b631e07d61e1bcc4a0c48fccf24ed119364dd3ca65d8d0d8c3fa3bfc1e3c8bb0.scope │ └─cri-containerd-396f42d93f69e67cabb85082eb7cb6a168819005c49f293e8cd209bd7604b738.scope ├─kubepods-burstable-pode95be299b415ebcd7a94d4301040f429.slice │ ├─cri-containerd-db8fe54f168ea 02764ae48c6083a713ebfd643f8a53136834996c6540df9bb38.scope │ └─cri-containerd-5cb34f00d28e465c98de6332de4db71e6ceff3319355571b3180373b290258bc.scope ├─kubepods-burstable-podf0a82a340a7886f682b8a95f0135e11c.slice │ ├─cri-containerd-905f9ea46ea994ffc94c3e306e3112984e221a60fae892aa2e0da8413ee569c6.scope │ └─cri-containerd-c1c29715932d8e4e51501dcca91c9a45d9d4f1a6f2c0256b793a4ae34489380c.scope ├─kubepods-burstable-podf26d1464_bc0d_43c8_aad9_664504951095.slice │ ├─cri-containerd-d9bd512ff1851174e93a0cd1ef3d2deef7f52fa4f1f8f7ac89e4cc89afc884c5.scope │ └─cri-containerd-4bbb4928e47b1bc074566e26efcc9940da825f6491461028d40e986ab933cdae.scope ├─kubepods-burstable-pod39eb450f575ebf1ca6dff5b53bacca00.slice │ ├─cri-containerd-4a790c7f3594e78df87e1be55b5dcc2b30b57fecffc0ac333a62c9da16e5d764.scope │ └─cri-containerd-185a10c5b3cb25cbdf9265e9f80b06f959ec1c2717257c29ba7f6c004a8ac94a.scope └─kubepods-burstable-pod5bb51662384d78ca722fef44e0cf31df.slice Memory サブシステム配下で、 kubelet が消そうとしている cgroup が存在するか確認 → 該当の cgroup 発見できず... 利用環境 • OS : Ubuntu 20.04.6 LTS
 • Linux Kernel : 5.15.0-71-generic
 • Kubernetes : 1.27.1
 • CRI : containerd 1.7.1
 • OCI Runtime : runc 1.1.7 ※ 後述しますが、該当 cgroup はしっかり存在していました。。 ここでは、Memory サブシステム配下しか見ていなかったことが原因
  17. コードリーディング
 ログから、コードベース (kubelet) で原因分析することに func (m *podContainerManagerImpl) Destroy(podCgroup CgroupName) error

    { … // Now its safe to remove the pod's cgroup containerConfig := &CgroupConfig{ Name: podCgroup, ResourceParameters: &ResourceConfig{}, } if err := m.cgroupManager.Destroy(containerConfig); err != nil { klog.InfoS("Failed to delete cgroup paths", "cgroupName", podCgroup, "err", err) return fmt.Errorf("failed to delete cgroup paths for %v : %v", podCgroup, err) } return nil } (m *podContainerManagerImpl) Destroy() [What] Pod の cgroup path の削除を行う関数 cgroupManager の Destroy()を呼び出す [次に確認したいこと] 削除対象となる CgroupName はどこで指定されるか? kubelet のログレベルを上げてみることに → 以下のログが追加された Orphaned pod found, removing pod cgroups func (kl *Kubelet) cleanupOrphanedPodCgroups(pcm cm.PodContainerManager, cgroupPods map[types.UID]cm.CgroupName, possiblyRunningPods map[types.UID]sets.Empty) { // Iterate over all the found pods to verify if they should be running for uid, val := range cgroupPods { // if the pod is in the running set, its not a candidate for cleanup if _, ok := possiblyRunningPods[uid]; ok { continue } … klog.V(3).InfoS("Orphaned pod found, removing pod cgroups", "podUID", uid) go pcm.Destroy(val) } } (kl *Kubelet) cleanupOrphanedPodCgroups() [What] 全 Pod から running 状態ではないと思われるものを削除する 全 Pod の UID に対して、ループ処理 [次に確認したいこと] 削除対象となる map[types.UID]cm.CgroupName はどこで指定されるか? ※ 表示のため、本来のコードから一部変更してます https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/cm/pod_container_manager_linux.go#L197-L214 https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet_pods.go#L2301-L2330
  18. func (kl *Kubelet) HandlePodCleanups(ctx context.Context) error { var ( cgroupPods

    map[types.UID]cm.CgroupName err error ) if kl.cgroupsPerQOS { pcm := kl.containerManager.NewPodContainerManager() cgroupPods, err = pcm.GetAllPodsFromCgroups() … // Remove any cgroups in the hierarchy for pods that are definitely no longer // running (not in the container runtime). if kl.cgroupsPerQOS { pcm := kl.containerManager.NewPodContainerManager() klog.V(3).InfoS("Clean up orphaned pod cgroups") kl.cleanupOrphanedPodCgroups(pcm, cgroupPods, possiblyRunningPods) } } (kl *Kubelet) HandlePodCleanups() [What] 不要な Pod などを削除することで、ユーザーが指定する望ましい状態になるように 調整する kubelet の main sync loop 内から呼び出される [次に確認したいこと] 全 Pod を取得する GetAllPodsFromCgroups() のロジックは? コードリーディング
 ※ 表示のため、本来のコードから一部変更してます https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/kubelet_pods.go#L1006-L1280
  19. func (m *podContaine…) GetAllPodsFromCgroups() (map[types.UID]CgroupName, error) { foundPods := make(map[types.UID]CgroupName)

    qosContainersList := [3]CgroupName{m.BestEffort, m.Burstable, m.Guaranteed} for _, val := range m.subsystems.MountPoints { for _, qosContainerName := range qosContainersList { qcConversion := m.cgroupManager.Name(qosContainerName) qc := path.Join(val, qcConversion) dirInfo, err := os.ReadDir(qc) … podUID := parts[1] foundPods[types.UID(podUID)] = internalPath } } } return foundPods, nil } GetAllPodsFromCgroups() [What] Cgroups から 全ての Pod を取得する QoS クラスごとに、サブシステムのマウントポイントから Pod cgroup が 存在するか 走査 1つのサブシステムでも存在すると該当 Pod を削除 [次に確認したいこと] サブシステムはどこから算出される? /kubepods.slice /<Guaranteed Pod UID> /<container ID> /kubepods-burstable.slice /<Burstable Pod UID> /<container ID> /kubepods-besteffort.slice /<BestEffort Pod UID> /<container ID> k8s cgroup ディレクトリ構成 コードリーディング
 ※ 表示のため、本来のコードから一部変更してます https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/cm/pod_container_manager_linux.go#L246-L298
  20. func GetCgroupSubsystems() (*CgroupSubsystems, error) { if libcontainercgroups.IsCgroup2UnifiedMode() { return getCgroupSubsystemsV2()

    } return getCgroupSubsystemsV1() } allSubsystems, err := ParseCgroupFile("/proc/self/cgroup") - - - func parseCgroupFromReader(r io.Reader) (map[string]string, error) { s := bufio.NewScanner(r) cgroups := make(map[string]string) for s.Scan() { text := s.Text() parts := strings.SplitN(text, ":", 3) … for _, subs := range strings.Split(parts[1], ",") { cgroups[subs] = parts[2] } } func GetAllSubsystems() ([]string, error) { if IsCgroup2UnifiedMode() { data, err := ReadFile("/sys/fs/cgroup", "cgroup.controllers") if err != nil { return nil, err } subsystems := append(pseudo, strings.Fields(data)...) return subsystems, nil } GetCgroupSubsystems() [What] cgroup バージョン ごとの取得方式でサブシステムを取得 取得のコアロジックの部分は runc/libcontainer のpkg を呼び出す ※ runc/libcontainer : コンテナ管理で必要な namespace/cgroup などを制御する go の pkg cgroup v1 cgroup v2 コードリーディング
 ※ 表示のため、本来のコードから一部変更してます https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/cm/helpers_linux.go#L263-L269 runc/libcontainer https://github.com/opencontainers/runc/blob/main/libcontainer/cgroups/utils.go#L98-L137 https://github.com/opencontainers/runc/blob/main/libcontainer/cgroups/utils.go#L184-L211 /proc/self/cgroup hierarchy-ID:subsystem-list:cgroup-path
  21. 調査
 /proc/self/cgroup を参照し、利用しているサブシステムを全て整理し 再度 削除しようとしている cgroup を確認すると misc サブシステム配下に存在した! $

    cat /proc/self/cgroup 13:misc:/ 12:blkio:/ 11:freezer:/ 10:perf_event:/ 9:memory:/ 8:cpu,cpuacct:/ 7:pids:/ 6:hugetlb:/ 5:net_cls,net_prio:/ 4:cpuset:/ 3:rdma:/ 2:devices:/ 1:name=systemd:/ 0::/user.slice/ misc サブシステム カーネル 5.13 で追加された新規サブシステム ホスト上にある有限リソースの使用を cgroup で制限する場合に利用する e.g. ホスト上に 50 あるうちの特定の cgroup に 30 までは使用させるなど kubelet の削除ロジック 1つのサブシステムでも存在すると該当 Pod を削除 削除処理ではmisc サブシステム の cgroup のパスが認知できていない AKE で利用していた Linux Kernel : 5.15.0-71-generic
  22. この時点で分かったこと
 misc サブシステム の cgroup が存在することで削除対象に入るが、 削除処理で該当の cgroup パスが認知できていない kubelet

    が CPU / Memory を過剰消費していて、 Pod の cgroup の削除に失敗している ノードのメモリを食い潰してしまう何らかのプロセスにより、 メモリ高負荷状態に陥り、kubelet down になっている
  23. 調査 
 今までの情報をもとに、 kubelet や runc の Issue を漁りまくっていると... opencontainers/runc

    repo でそれらしき Issue を発見 https://github.com/opencontainers/runc/issues/3849 runc v1.1.6 を利用すると、「Failed to delete cgroup paths」というログが大量に出ていて kubelet や systemd などの CPU、メモリリソースが大量に消費されているとのこと ※ AKE で利用していた OCI Runtime : runc 1.1.7
  24. 原因 
 [ Issue で議論されていた原因 ] runc v1.1.6バイナリが misc cgroupを作成し、kubelet

    が古いバージョンの runc/libcontainer を 使用して削除するため、不整合が起きているのではないか? (runc が misc cgroup を処理するようになったのが、v1.1.6 から) var legacySubsystems = []subsystem{ &fs.CpusetGroup{}, &fs.DevicesGroup{}, … &fs.RdmaGroup{}, + &fs.NameGroup{GroupName: "misc"}, } func (m *LegacyManager) Destroy() error { … if err := cgroups.RemovePaths(m.paths); err != nil { return err } } runc cgroupv1 の Destory() https://github.com/opencontainers/runc/blob/main/libcontainer/cgroups/systemd/v1.go Destroy() 内で利用される m.paths は、 初期化処理のNewLegacyManager() 内で 呼び出される initPaths() で設定される initPaths() ではハードコードされた legacySubsystems []subsystem の変数によって 処理する cgroup サブシステムが指定される ここに misc cgroup が 追加されたのが、 v1.1.6 から func (m *podContainerManagerImpl) Destroy(podCgroup CgroupName) error { … if err := m.cgroupManager.Destroy(containerConfig); err != nil { return fmt.Errorf("failed to delete cgroup paths for %v : %v", podCgroup, err) } … kubelet の呼び出し
  25. 1. runc のリリース (v1.1.x → v1.1.y) 2. containerd で利用する runc

    バージョンの変更 (v1.1.y) 3. (containerd が安定したら) k8s で利用する runc/libcontainer バージョンの変更 (v1.1.y) 原因 
 k8s における runc のリリースプロセス なぜ バージョンのズレが生じたのか.... おっ!runc / containerd の パッチバージョン リリースされてるじゃん!バージョン上げたろ! (v1.1.y) 今の k8s 1.27 の最新の パッチバージョンが 1.27.1 だから、これで k8s イメージ作ろっと! (1.27.1 は まだ v1.1.y 対応していない) runc の バージョン : v1.1.7 (misc サブシステム処理有) kubelet で利用する runc/libcontainer のバージョン : v1.1.4 (misc サブシステム処理無) module k8s.io/kubernetes go 1.20 require( … github.com/opencontainers/runc v1.1.4 https://github.com/kubernetes/kubernetes/blob/v1.27.1/go.mod#L55 kubernetes v1.27.1の go.mod k8s のリリースプロセスよりも先取りして、 依存関係のバージョンを上げることは非推奨 その結果.... k8s イメージ作成中 ↑ runc/libcontainer
  26. 修正対応 
 [方針] 1. runc のダウングレードを行う 2. runc/libcontainer v1.1.6 以上を利用している

    k8s バージョンを利用 (1.28.0>=, 1.27.2>=, 1.26.4>=) 3. cgroup v2 移行 [暫定対応] runc/libcontainer v1.1.6 以上を利用している k8s バージョンを利用 cgroup v2 移行 元々 ubuntu 20.04 → 22.04 の移行対応も必要だったため、このタイミングで cgroupv2 移行も実施 現在の AKE では v1.27 以上のクラスタで cgroupv2 を利用している ※ cgroupv1 ではカーネルに新規サブシステムが追加されたら、同様の問題が再現するため、cgroupv2 の移行が良いと考えている [恒久対応]
  27. まとめ 
 cgroup v1 利用時に k8s (kubelet) の runc/libcontainer と

    runc バイナリのバージョン差分による cgroup 処理のエラーによって、ノード障害 が発生した 自前 で k8s イメージを作成する場合は、k8s パッチバージョンで利用している runc/libcontainer の バージョンを確認して、先取りして新しい runc バージョンを利用しない方が吉 ※ Releases Note の Known issues も確認しておいたほうが良い! (可能であれば) cgroupv2 移行を進めた方が吉 https://github.com/opencontainers/runc/releases/tag/v1.1.6