Amazon EKS의 Aamazon Linux 2에서 Aamazon Linux 2023 마이그레이션: cgroup v2 변화에 따른 기존 워크로드의 영향과 대응 방안

12분 분량
콘텐츠 수준: 중급
3

본 기사에서는 기존 Amazon EKS의 Data Plane을 Amazon Linux 2(AL2)에서 Amazon Linux 2023(AL2023)으로 전환할 때 생기는 cgroup 버전 변화로 인한 영향 및 대응 방안을 설명합니다.

EKS는 2025년 11월 26일 이후 더 이상 EKS 최적화 AL2 AMI를 게시하지 않습니다. 또한 Kubernetes 버전 1.32는 EKS가 AL2 AMI를 출시하는 마지막 버전입니다. [1]

이에 대응하려고 많은 고객들이 EKS 의 Data Plane을 AL2 노드에서 AL2023 노드로 전환하는데, 이 전환 과정에서 마주하는 큰 변화 중 하나는 컨테이너 관리에 사용되는 cgroup의 버전이 바뀌었다는 점입니다.

여기서 cgroup 이란 control groups의 약칭으로 프로세스 모음의 리소스 사용량(CPU, 메모리, 디스크 I/O 등)을 제한, 설명 및 격리하는 Linux 커널 기능입니다. 컨테이너는 이 cgroup을 이용해 프로세스를 격리해 관리합니다. [2]

여기서 cgroup의 버전이 바뀌는 이유는 Kubernetes는 1.25 버전부터 cgroup v2로 컨테이너 리소스를 관리하는 기능이 GA(General Availability) 되었으나 AWS에서는 AL2에서 cgroup v2로 바로 마이그레이션 하지 않고, AL2023에서 cgroup v2를 활성화하는 정책을 취했기 때문입니다.

따라서 AL2 노드를 AL2023 노드로 전환하게 되면 내부적으로 cgroup v2로 전환되며 이에 따라 기존 워크로드에 영향이 있을 수 있습니다.

대표적으로 Pod의 메모리 사용량이 증가해 메모리 압박 및 퇴출이 증가하거나, kubectl top 노드 명령을 실행하면 노드가 이전 버전의 쿠버네티스보다 더 많은 메모리 사용량을 보고하는 경우가 있을 수 있습니다.

본 기사에서는 이처럼 AL2023으로 전환함에 따른 cgroup v2로의 변화가 워크로드에 영향을 미치는 요소들을 정리합니다.


이슈 : Pod의 메모리/CPU 사용량 증가 및 노드에서의 Pod 퇴출과 메모리 압박 증가

cgroup v2로 전환됨에 따라 아래와 같은 증상들이 발생할 수 있습니다.

  • 기존 cgroup v1 에서 동작하던 것에 비해 노드에서 더 많은 메모리 사용량이 확인됨
  • 기존보다 Pod에서 더 많은 메모리 사용량을 보고
  • CPU 쓰로틀링 발생
  • OOM 에러와 함께 Pod가 실패

예를 들면 아래와 같이 동일한 .Net 3 Pod를 AL2, AL2023 노드에 동작시켰을 때 사용되는 메모리 양이 차이가 나는 부분에서 이런 현상들을 확인할 수 있습니다.

$ kubectl top pod
NAME                                    CPU(cores)   MEMORY(bytes)
dotnet3-hello-al2-ccb9c5f46-z97wt       1m           13Mi
dotnet3-hello-al2023-756f989455-dk7v7   1m           15Mi

이러한 현상은 주로 cgroup v2에서 API 및 구조적 변화로 인해 Pod의 리소스 사용이 cgroup v2의 더 엄격한 메모리 제한을 따르기에 발생합니다.

cgroup 의 구조 및 API 변화

cgroup v1의 경우 프로세스 내 스레드의 cgroup 멤버십을 독립적으로 조작하는 것이 가능했습니다.

예를 들면 cgroup 내의 cpu는 /sys/fs/cgroup/cpu/foo, memory는 /sys/fs/cgroup/memory/bar 와 같이, 이는 유연성을 제공했지만 문제도 있었고 예상보다 유용하지 않았습니다. [3]

# cgroup v1 pod

$ kubectl exec -it al2 -- bash
root@al2:/# ls -al /sys/fs/cgroup/
total 0
...
lrwxrwxrwx  1 root root  11 Apr 15 12:09 cpu -> cpu,cpuacct
drwxr-xr-x  2 root root   0 Apr 15 12:09 cpu,cpuacct
lrwxrwxrwx  1 root root  11 Apr 15 12:09 cpuacct -> cpu,cpuacct
drwxr-xr-x  2 root root   0 Apr 15 12:09 cpuset
drwxr-xr-x  2 root root   0 Apr 15 12:09 devices
drwxr-xr-x  2 root root   0 Apr 15 12:09 freezer
drwxr-xr-x  2 root root   0 Apr 15 12:09 hugetlb
drwxr-xr-x  2 root root   0 Apr 15 12:09 memory

cgroup v2 에서는 단순성에 중점을 두어 cgroup v1에서 /sys/fs/cgroup/cpu/$GROUP_NAME, /sys/fs/cgroup/memory/$GROUP_NAME/sys/fs/cgroup/$GROUP_NAME 으로 통합되어 프로세스는 하나의 컨트롤러에서 제어되도록 변경되었습니다.

# cgroup v2 pod

$ kubectl exec -it al2 -- bash
root@al2023:/# ls -al /sys/fs/cgroup/
total 0
-rw-r--r--. 1 root root 0 Apr 15 12:09 cpu.pressure
-r--r--r--. 1 root root 0 Apr 15 12:09 cpu.stat
-rw-r--r--. 1 root root 0 Apr 15 12:09 cpu.weight
-rw-r--r--. 1 root root 0 Apr 15 12:09 cpu.weight.nice
-rw-r--r--. 1 root root 0 Apr 15 12:09 cpuset.cpus
-r--r--r--. 1 root root 0 Apr 15 12:09 cpuset.cpus.effective
-rw-r--r--. 1 root root 0 Apr 15 12:09 cpuset.cpus.partition
...
-r--r--r--. 1 root root 0 Apr 15 12:09 memory.current
-rw-r--r--. 1 root root 0 Apr 15 12:09 memory.max
-rw-r--r--. 1 root root 0 Apr 15 12:09 memory.min
-rw-r--r--. 1 root root 0 Apr 15 12:09 memory.oom.group
-rw-r--r--. 1 root root 0 Apr 15 12:09 memory.pressure
-r--r--r--. 1 root root 0 Apr 15 12:09 memory.swap.current
-r--r--r--. 1 root root 0 Apr 15 12:09 memory.swap.events
-rw-r--r--. 1 root root 0 Apr 15 12:09 memory.swap.max
...

이러한 구조적 변화와 더불어 API 또한 모든 컨트롤러에 대해 일관된 인터페이스를 제공하게 변경되었습니다.

cgroup 변경에 따른 대응 방법

위에서 설명한 것 처럼 API가 기존 cgroup v1와 달라지며 cgroup 파일 시스템을 이용하는 애플리케이션이 존재하는 경우 cgroup v2를 지원하는 최신 버전으로 업데이트해야만 정상적으로 동작하게 됩니다.

cgroup v2를 지원하는 버전은 다음과 같습니다. [4]

  • Java
    • OpenJDK/HotSpot: jdk8u372, 11.0.16, 15 혹은 그 이상의 버전
    • IBM Semeru Runtimes: 8.0.382.0, 11.0.20.0, 17.0.8.0
    • IBM Java: 8.0.8.6 이상
  • Node
    • Node.js 20.3.0 혹은 그 이상의 버전
  • .Net
    • .Net 5.0 이상 [5]

이 목록은 성능 저하가 발생할 수 있는 전체 목록이 아닙니다. 메모리 포화 또는 OOM이 발생하는 다른 환경이 있을 수 있습니다.

일부 모니터링 및 보안 에이전트는 cgroup 파일 시스템에 의존합니다. 따라서 이러한 에이전트들 또한 cgroup v2를 지원하는 버전으로 업데이트해야 합니다.

추가적으로 cgroup v2는 더 엄격한 메모리 관리 방식을 적용했기 때문에 Pod들이 예상보다 더 많은 메모리를 필요로 할 수 있습니다. 따라서 리소스 사용량에 여유를 두고 설정해주시기 바랍니다.


이슈 : Pod에서 잦은 OOM 발생

cgroup v2로 마이그레이션을 진행한 이후 아래와 같은 증상들이 발생할 수 있습니다.

  • 기존에 정상적으로 동작하던 Pod가 일정한 시점에 OOM 에러가 발생
  • Pod의 OOM 현상이 주기적으로 발생

이 현상은 cgroup v2에서 Memory 컨트롤러의 interface files(control files)에 추가된 memory.oom.group에 의해 발생할 수 있습니다. [6]

memory.oom.group 이란

성숙한 멀티 프로세스 시스템에서는 이러한 child 프로세스의 OOM을 올바르게 처리할 수 있지만, 그렇지 않은 많은 시스템에서는 OOM이 발생했을 때 일부 프로세스가 죽어서 불안정해지는 것보다 컨테이너 전체를 OOM으로 강제 종료하는 것이 더 안정적일 수 있습니다.

memory.oom.group은 parent 프로세스 아래의 child 프로세스 에서 OOM이 발생했을 경우 프로세스 그룹 전체를 강제 종료하도록 하는 설정 값입니다.

Kubernetes는 v1.28부터 이 memory.oom.group 을 활성화하여 컨테이너 내 프로세스에서 어떤 프로세스던 OOM이 발생하면 컨테이너 내 모든 PID를 OOM으로 강제 종료하도록 변경되었습니다. [7]

따라서 기존에 자식 프로세스에서 OOM이 발생했지만 이를 회수해 정상적으로 동작하던 워크로드의 경우 OOM이 발생하며 종료될 수 있습니다.

이 구성은 매니페스트의 수정만으로 해결되지 않고 애플리케이션의 구조변경 혹은 Kubernetes v1.32의 경우 kubelet에서 singleProcessOOMKill 플래그를 설정해야 합니다. [8]

memory.oom.group 기능의 동작 확인

기존 Amazon Linux 2 노드와 Amazon Linux 2023 노드에 동일하게 OOM이 발생하는 애플리케이션을 실행해 둘의 차이점을 보여드리겠습니다.

사전 준비

  • 테스트용 노드 생성

    ip-10-30-65-163.ap-northeast-2.compute.internal
      - AMI Name : amazon-eks-node-1.30-v20250304
      - OS Image : Amazon Linux 2
    
    ip-10-30-77-132.ap-northeast-2.compute.internal
      - AMI Name : amazon-eks-node-al2023-x86_64-standard-1.30-v20250228
      - OS Image : Amazon Linux 2023.6.20250218
    
  • 테스트 방법

    각 노드에 nginx pod를 생성한 이후 내부에서 동일한 방식으로 stress-ng 설치해 OOM 발생을 유도했습니다.

    apt-get update
    apt-get install procps -y
    
    apt update
    apt install stress-ng -y
    
    stress-ng -vm 1 --vm-bytes 200m -t 5s
    stress-ng -vm 1 --vm-bytes 256m -t 5s

테스트

  • Amazon Linux 2 노드 상의 Pod

    $ kubectl exec -it al2-nginx -- bash
    
    root@al2-nginx:/# stress-ng -vm 1 --vm-bytes 200m -t 5s
    ...
    stress-ng: debug: [501] starting stressors
    stress-ng: debug: [501] 1 stressor started
    stress-ng: debug: [502] vm: can't set oom_score_adj
    stress-ng: debug: [502] vm: started [502] (instance 0)
    stress-ng: debug: [502] vm: using method 'all'
    stress-ng: debug: [502] vm: exited [502] (instance 0)
    stress-ng: debug: [501] process [502] terminated
    stress-ng: metrc: [501] stressor       bogo ops real time  usr time  sys time   bogo ops/s     bogo ops/s CPU used per       RSS Max
    stress-ng: metrc: [501]                           (secs)    (secs)    (secs)   (real time) (usr+sys time) instance (%)          (KB)
    stress-ng: metrc: [501] vm                16866      5.04      0.59      0.43      3344.71       16529.70        20.23        206684
    stress-ng: debug: [501] metrics-check: all stressor metrics validated and sane
    stress-ng: info:  [501] successful run completed in 5.04s
    
    root@al2-nginx:/# stress-ng -vm 1 --vm-bytes 300m -t 5s
    ...
    stress-ng: debug: [514] starting stressors
    stress-ng: debug: [514] 1 stressor started
    stress-ng: debug: [515] vm: can't set oom_score_adj
    stress-ng: debug: [515] vm: started [515] (instance 0)
    stress-ng: debug: [515] vm: using method 'all'
    stress-ng: debug: [515] vm: child died: signal 9 'SIGKILL' (instance 0)
    stress-ng: debug: [515] vm: assuming killed by OOM killer, restarting again (instance 0)
    stress-ng: debug: [515] vm: child died: signal 9 'SIGKILL' (instance 0)
    stress-ng: debug: [515] vm: assuming killed by OOM killer, restarting again (instance 0)
    stress-ng: debug: [515] vm: child died: signal 9 'SIGKILL' (instance 0)
    stress-ng: debug: [515] vm: assuming killed by OOM killer, restarting again (instance 0)
    stress-ng: debug: [515] vm: exited [515] (instance 0)
    stress-ng: debug: [514] process [515] terminated
    stress-ng: metrc: [514] stressor       bogo ops real time  usr time  sys time   bogo ops/s     bogo ops/s CPU used per       RSS Max
    stress-ng: metrc: [514]                           (secs)    (secs)    (secs)   (real time) (usr+sys time) instance (%)          (KB)
    stress-ng: metrc: [514] vm                    0      7.58      0.21      1.21         0.00           0.00        18.75        255408
    stress-ng: debug: [514] metrics-check: all stressor metrics validated and sane
    stress-ng: info:  [514] successful run completed in 7.58s
    
    root@al2-nginx:/#

    => 기존 OOM 처리 방식에 따라서 memory를 초과 할당시 OOM이 발생하지만 child process만 종료되고 stress-ng의 작업은 완료되는 것을 확인할 수 있습니다.

  • Amazon Linux 2023 노드 상의 Pod

    $ kubectl exec -it al2023-nginx -- bash
    
    root@al2023-nginx:/# stress-ng -vm 1 --vm-bytes 200m -t 5s
    ...
    stress-ng: debug: [501] starting stressors
    stress-ng: debug: [501] 1 stressor started
    stress-ng: debug: [502] vm: can't set oom_score_adj
    stress-ng: debug: [502] vm: started [502] (instance 0)
    stress-ng: debug: [502] vm: using method 'all'
    stress-ng: debug: [502] vm: exited [502] (instance 0)
    stress-ng: debug: [501] process [502] terminated
    stress-ng: metrc: [501] stressor       bogo ops real time  usr time  sys time   bogo ops/s     bogo ops/s CPU used per       RSS Max
    stress-ng: metrc: [501]                           (secs)    (secs)    (secs)   (real time) (usr+sys time) instance (%)          (KB)
    stress-ng: metrc: [501] vm                 6310      5.09      0.44      0.59      1239.54        6138.32        20.19        206708
    stress-ng: debug: [501] metrics-check: all stressor metrics validated and sane
    stress-ng: info:  [501] successful run completed in 5.09s
    
    root@al2023-nginx:/# stress-ng -vm 1 --vm-bytes 300m -t 5s
    ...
    stress-ng: debug: [505] starting stressors
    stress-ng: debug: [505] 1 stressor started
    stress-ng: debug: [506] vm: can't set oom_score_adj
    stress-ng: debug: [506] vm: started [506] (instance 0)
    stress-ng: debug: [506] vm: using method 'all'
    command terminated with exit code 137

    Amazon Linux 2 노드에서의 테스트와는 다르게 실행중 OOM 발생하며 강제종료되었습니다.

    $ kubectl get pod
    NAME           READY   STATUS    RESTARTS     AGE
    al2-nginx      1/1     Running   0            18m
    al2023-nginx   1/1     Running   2 (6s ago)   18m
    
    $ kubetl describe pod al2023-nginx
    ...
    Containers:
      nginx:
        Container ID:   containerd://7d674aba688b3aa4f7c7ac603ca8ecb363186dd30a2e81e19fcdb6dd1db929e8
        Image:          nginx:latest
        Image ID:       docker.io/library/nginx@sha256:9d6b58feebd2dbd3c56ab5853333d627cc6e281011cfd6050fa4bcf2072c9496
        Port:           80/TCP
        Host Port:      0/TCP
        State:          Running
          Started:      Sat, 15 Mar 2025 12:34:53 +0900
        Last State:     Terminated
          Reason:       OOMKilled
          Exit Code:    137
          Started:      Sat, 15 Mar 2025 12:22:24 +0900
          Finished:     Sat, 15 Mar 2025 12:34:49 +0900
        Ready:          True

    => memory를 초과 할당시 AL2에서의 테스트와는 다르게 exit code 137와 함께 Pod가 종료되었습니다. 이를 describe pod로 확인해보자 OOMKilled로 인해서 Pod가 종료된 것을 확인할 수 있었습니다. 즉, memory.oom.group 기능이 활성화되어있어 child process에서 OOM이 발생시 Pod 자체를 OOMKilled 시키는 것을 확인할 수 있습니다.

memory.oom.group 기능을 비활성화 하는 방법

memory.oom.group 기능이 워크로드에 영향 주는 경우 에서 언급했듯이 기능은 Kubernetes v1.32 부터 kubelet의 singleProcessOOMKill 플래그를 활성화 함으로써 비활성화시킬 수 있습니다. [8]

이를 활성화하기 위해서는 시작 템플릿을 사용해 노드 그룹을 생성해주셔야 하는데 [9], 이때 NodeConfig 지정시 아래와 같이 singleProcessOOMKill를 활성화해주시면 됩니다.

apiVersion: node.eks.aws/v1alpha1
kind: NodeConfig
spec:
  cluster:
    name: test-cluster
    apiServerEndpoint: ...
    cidr: 172.20.0.0/16
  kubelet:
    config:
      singleProcessOOMKill: true

singleProcessOOMKill 플래그 설정시 memory.oom.group 동작 확인

위에서 설명한 NodeConfig를 활용해 al2023-with-flag 노드를 생성했습니다.

$ kubectl get nodes -L eks.amazonaws.com/nodegroup
NAME                                               STATUS   ROLES    AGE     VERSION               NODEGROUP
ip-10-31-105-169.ap-northeast-2.compute.internal   Ready    <none>   2m33s   v1.32.0-eks-aeac579   al2023-with-flag

노드의 정보를 확인해보면 아래와 같이 singleProcessOOMKill 플래그가 잘 설정된 것을 확인할 수 있습니다.

$ kubectl get --raw "/api/v1/nodes/ip-10-31-105-169.ap-northeast-2.compute.internal/proxy/configz" | jq | grep singleProcessOOMKill
  "singleProcessOOMKill": true,

이후 기존과 동일하게 노드에 Pod를 생성해 테스트를 진행했습니다.

  • Amazon Linux 2023 (singleProcessOOMKill 설정) 노드 상의 Pod

    $ kubectl exec -it al2023-with-flag -- bash
    
    root@al2023-with-flag:/# stress-ng -vm 1 --vm-bytes 300m -t 5s
    stress-ng: debug: [575] invoked with 'stress-ng -vm 1 --vm-bytes 300m -t 5s' by user 0 'root'
    ...
    stress-ng: debug: [575] starting stressors
    stress-ng: debug: [575] 1 stressor started
    stress-ng: debug: [576] vm: can't set oom_score_adj
    stress-ng: debug: [576] vm: started [576] (instance 0)
    stress-ng: debug: [576] vm: using method 'all'
    stress-ng: debug: [576] vm: child died: signal 9 'SIGKILL' (instance 0)
    stress-ng: debug: [576] vm: assuming killed by OOM killer, restarting again (instance 0)
    ...
    stress-ng: debug: [576] vm: child died: signal 9 'SIGKILL' (instance 0)
    stress-ng: debug: [576] vm: assuming killed by OOM killer, restarting again (instance 0)
    stress-ng: debug: [576] vm: exited [576] (instance 0)
    stress-ng: debug: [575] process [576] terminated
    stress-ng: metrc: [575] stressor       bogo ops real time  usr time  sys time   bogo ops/s     bogo ops/s CPU used per       RSS Max
    stress-ng: metrc: [575]                           (secs)    (secs)    (secs)   (real time) (usr+sys time) instance (%)          (KB)
    stress-ng: metrc: [575] vm                    0      5.05      0.67      4.15         0.00           0.00        95.41        248816
    stress-ng: debug: [575] metrics-check: all stressor metrics validated and sane
    stress-ng: info:  [575] successful run completed in 5.05s

=> 기존 AL2023 노드에서 OOM이 발생해 Pod가 종료된것과 달리 child process만 종료시키며 정상적으로 동작이 완료된 것을 확인할 수 있습니다.


결론 및 정리

EKS의 AL2에서 AL2023으로의 전환은 cgroup v2 도입으로 인한 중요한 변화를 수반합니다. 특히 애플리케이션의 cgroup 호환성을 고려해야 하며, memory.oom.group 기능으로 인해 OOM 발생 시 개별 프로세스가 아닌 컨테이너 전체가 종료되는 등 처리 방식이 크게 달라집니다.

이러한 변화에 대응하기 위해서는 애플리케이션의 cgroup 호환성 검토와 충분한 테스트가 필수적이며, memory.oom.group 기능으로 인한 영향이 우려되는 경우 Kubernetes v1.32부터 제공되는 singleProcessOOMKill 플래그를 활용해 기존 동작 방식을 유지할 수 있습니다.


참고 자료

[1] 최적화된 Amazon Linux AMI를 사용한 노드 생성 | https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/eks-optimized-ami.html?icmpid=docs_eks_help_panel_hp_nodes_mng_details_ami_type
[2] cgroups | https://en.wikipedia.org/wiki/Cgroups
[3] CGROUP VERSION 1 | https://man7.org/linux/man-pages/man7/cgroups.7.html#CGROUPS_VERSION_1
[4] Migrating to cgroup v2 | https://kubernetes.io/docs/concepts/architecture/cgroups/#migrating-cgroupv2
[5] Announcing .NET 5.0 | https://devblogs.microsoft.com/dotnet/announcing-net-5-0/#containers
[6] Control Group v2 - Memory | https://docs.kernel.org/admin-guide/cgroup-v2.html#memory
[7] Kubernetes CHANGELOG 1.28 | https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.28.md?plain=1#L2143
[8] Kubernetes CHANGELOG 1.32 | https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.32.md?plain=1#L647
[9] 시작 템플릿을 사용한 관리형 노드 지정 | https://docs.aws.amazon.com/ko_kr/eks/latest/userguide/launch-templates.html

profile pictureAWS
지원 엔지니어
게시됨 한 달 전73회 조회