관리 메뉴

근묵자흑

Kubernetes cgroups (2025) 본문

k8s/K8s Annotated

Kubernetes cgroups (2025)

Luuuuu 2025. 12. 21. 02:34

"Pod에 설정한 requests와 limits가 실제로 어떻게 동작하는 걸까?"

 

Kubernetes를 운영하다 보면 이런 의문이 들 때가 있습니다. YAML에 memory: 512Mi를 적으면 컨테이너가 512MB만 쓰게 되는 건 알지만, 정확히 어떤 메커니즘으로 이런 제한이 가능한 걸까요? 그 답은 Linux 커널의 cgroups에 있습니다.

 


1. 사전 지식: 컨테이너 런타임 스택 이해하기

cgroups를 이해하기 전에, 먼저 Kubernetes가 컨테이너를 실행하는 전체 구조를 파악해야 합니다. kubectl apply를 실행하면 실제로 어떤 컴포넌트들이 관여하는지 살펴보겠습니다.

컨테이너 런타임 계층 구조

flowchart TB
    subgraph Kubernetes["Kubernetes Control Plane"]
        API[API Server]
        Scheduler[Scheduler]
    end

    subgraph Node["Worker Node"]
        Kubelet[kubelet]

        subgraph CRI["Container Runtime Interface (CRI)"]
            Containerd[containerd]
            CRIO[CRI-O]
        end

        subgraph OCI["OCI Runtime"]
            Runc[runc]
            Crun[crun]
        end

        subgraph Kernel["Linux Kernel"]
            Cgroups[cgroups]
            Namespaces[namespaces]
        end
    end

    API --> Scheduler
    Scheduler --> Kubelet
    Kubelet --> Containerd
    Kubelet --> CRIO
    Containerd --> Runc
    CRIO --> Crun
    Runc --> Cgroups
    Runc --> Namespaces
    Crun --> Cgroups
    Crun --> Namespaces

    style Cgroups fill:#e1f5fe
    style Namespaces fill:#e1f5fe

각 컴포넌트의 역할

kubelet은 각 노드에서 실행되는 Kubernetes 에이전트입니다. API Server로부터 Pod 스펙을 받아서 실제로 컨테이너를 실행하는 역할을 담당합니다. 하지만 kubelet이 직접 컨테이너를 만들지는 않습니다. 대신 Container Runtime Interface(CRI)를 통해 컨테이너 런타임에게 작업을 위임합니다.

 

CRI(Container Runtime Interface)는 Kubernetes와 컨테이너 런타임 사이의 표준 인터페이스입니다. 이 인터페이스 덕분에 Kubernetes는 특정 런타임에 종속되지 않고 다양한 런타임을 지원할 수 있습니다. 현재 프로덕션에서 주로 사용되는 CRI 구현체는  containerdCRI-O 두 가지입니다.

 

containerd는 Docker에서 분리되어 나온 컨테이너 런타임으로, 현재 가장 널리 사용됩니다. Docker를 설치하면 내부적으로 containerd가 함께 설치되며, Kubernetes 1.24부터 Docker 지원이 중단되면서 containerd를 직접 사용하는 것이 표준이 되었습니다.

 

CRI-O는 Kubernetes를 위해 처음부터 설계된 경량 컨테이너 런타임입니다. Red Hat이 주도하여 개발했으며, OpenShift에서 기본 런타임으로 사용됩니다. containerd보다 가볍고 Kubernetes에 특화되어 있다는 장점이 있습니다.

 

OCI(Open Container Initiative) Runtime은 실제로 컨테이너 프로세스를 생성하는 저수준 런타임입니다. containerd나 CRI-O는 이 OCI 런타임을 호출해서 컨테이너를 만듭니다. 가장 대표적인 구현체가 runc이며, 최근에는 더 가볍고 빠른 crun도 사용됩니다.

 

runc는 Docker가 개발한 OCI 런타임의 레퍼런스 구현체입니다. Go 언어로 작성되었으며, 대부분의 컨테이너 환경에서 기본으로 사용됩니다. runc가 하는 핵심 작업은 Linux 커널의 namespacescgroups를 설정하여 격리된 프로세스 환경을 만드는 것입니다.

 

crun은 C 언어로 작성된 경량 OCI 런타임입니다. runc보다 시작 시간이 빠르고 메모리 사용량이 적어서, CRI-O 1.31부터 기본 런타임으로 채택되었습니다. 기능적으로는 runc와 동일하지만 성능이 더 좋습니다.

컨테이너 격리의 두 기둥: namespaces와 cgroups

컨테이너가 "격리된 환경"을 제공할 수 있는 것은 Linux 커널의 두 가지 기능 덕분입니다.

 

namespaces"무엇을 볼 수 있는가"를 제어합니다. 프로세스가 볼 수 있는 시스템 리소스의 범위를 제한합니다. 예를 들어 PID namespace를 사용하면 컨테이너 내부에서는 자신의 프로세스만 보이고, 호스트의 다른 프로세스는 보이지 않습니다. Network namespace를 사용하면 컨테이너마다 독립적인 네트워크 스택을 가질 수 있습니다.

 

cgroups"얼마나 쓸 수 있는가"를 제어합니다. 프로세스 그룹이 사용할 수 있는 리소스의 양을 제한합니다. CPU 시간, 메모리, 디스크 I/O, 네트워크 대역폭 등을 제한할 수 있습니다.

이 두 가지가 결합되어 컨테이너라는 격리 환경이 만들어집니다. namespaces 없이는 컨테이너가 호스트 시스템을 볼 수 있고, cgroups 없이는 한 컨테이너가 모든 리소스를 독점할 수 있습니다. 이 글에서는 리소스 제한을 담당하는 cgroups에 집중합니다.


2. Cgroups란 무엇인가

기본 개념

Control Groups(cgroups)는 Linux 커널 2.6.24(2008년)에 도입된 기능으로, 프로세스 그룹의 리소스 사용을 제한하고 모니터링하는 메커니즘입니다. Google 엔지니어들이 개발했으며, 원래 이름은 "process containers"였지만 기존의 container 개념과 혼동을 피하기 위해 cgroups로 이름이 바뀌었습니다.

cgroups가 제공하는 네 가지 핵심 기능은 다음과 같습니다.

 

첫째, 리소스 제한(Resource Limiting)입니다. 프로세스 그룹이 사용할 수 있는 리소스의 상한선을 설정합니다. "이 그룹은 메모리를 512MB까지만 쓸 수 있다"와 같은 제한이 가능합니다.

 

둘째, 우선순위 지정(Prioritization)입니다. 리소스 경합이 발생했을 때 어떤 그룹이 우선권을 가질지 결정합니다. CPU 시간을 분배할 때 특정 그룹에 더 많은 비중을 줄 수 있습니다.

 

셋째, 계정(Accounting)입니다. 프로세스 그룹이 실제로 얼마나 리소스를 사용하고 있는지 추적합니다. 이 정보는 모니터링과 과금에 활용됩니다.

 

넷째, 제어(Control)입니다. 프로세스 그룹 전체를 일시 중지하거나 재개할 수 있습니다. 컨테이너를 pause/unpause하는 기능이 이를 통해 구현됩니다.

cgroups의 계층 구조

cgroups는 파일시스템 형태로 /sys/fs/cgroup/ 경로에 마운트됩니다. 이를 cgroupfs라고 부릅니다. cgroups는 부모-자식 관계의 계층 구조(hierarchy)를 가지며, 자식 그룹은 부모 그룹의 제한을 넘어설 수 없습니다.

flowchart TB
    subgraph cgroupfs["/sys/fs/cgroup (cgroups v2)"]
        Root["/ (root cgroup)"]

        Root --> System["system.slice<br/>시스템 서비스"]
        Root --> User["user.slice<br/>사용자 세션"]
        Root --> Kubepods["kubepods.slice<br/>Kubernetes Pods"]

        Kubepods --> Guaranteed["kubepods-pod*.slice<br/>Guaranteed QoS"]
        Kubepods --> Burstable["kubepods-burstable.slice"]
        Kubepods --> BestEffort["kubepods-besteffort.slice"]

        Burstable --> BurstablePod["kubepods-burstable-pod*.slice"]
        BurstablePod --> Container1["cri-containerd-*.scope<br/>Container A"]
        BurstablePod --> Container2["cri-containerd-*.scope<br/>Container B"]
    end

    style Kubepods fill:#e3f2fd
    style Guaranteed fill:#c8e6c9
    style Burstable fill:#fff9c4
    style BestEffort fill:#ffcdd2

위 다이어그램에서 볼 수 있듯이, Kubernetes의 모든 Pod는 kubepods.slice 아래에 위치합니다. 만약 kubepods.slice에 8GB 메모리 제한이 있다면, 그 아래의 모든 Pod가 사용하는 메모리 총합은 8GB를 넘을 수 없습니다.

cgroups 파일 구조 살펴보기

실제 노드에서 cgroups 파일시스템을 살펴보면 어떻게 생겼는지 이해할 수 있습니다. cgroups v2 환경에서 특정 컨테이너의 cgroup 디렉토리를 보면 다음과 같은 파일들이 있습니다.

/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/
└── kubepods-burstable-pod<UUID>.slice/
    └── cri-containerd-<CONTAINER_ID>.scope/
        ├── cgroup.controllers    # 활성화된 컨트롤러 목록
        ├── cgroup.procs          # 이 그룹에 속한 프로세스 ID들
        ├── cpu.weight            # CPU 가중치 (requests)
        ├── cpu.max               # CPU 상한 (limits)
        ├── memory.current        # 현재 메모리 사용량
        ├── memory.min            # 보장된 최소 메모리
        ├── memory.max            # 메모리 상한 (limits)
        ├── memory.high           # 메모리 스로틀링 임계값
        ├── io.weight             # I/O 가중치
        └── pids.current          # 현재 프로세스 수

각 파일은 텍스트 형식으로 되어 있어서 cat 명령으로 직접 읽을 수 있고, echo 명령으로 값을 변경할 수도 있습니다(물론 실제로는 Kubernetes가 이 작업을 대신합니다).

Pod의 requests/limits가 cgroups로 변환되는 과정

이제 핵심 질문에 답할 차례입니다. Pod manifest에 작성한 리소스 설정이 어떻게 cgroups 파일로 변환될까요?

apiVersion: v1
kind: Pod
metadata:
  name: web-server
spec:
  containers:
  - name: nginx
    image: nginx
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"

위와 같은 Pod를 배포하면, 다음과 같은 과정을 거칩니다.

sequenceDiagram
    participant User as 사용자
    participant API as API Server
    participant Sched as Scheduler
    participant Kubelet as kubelet
    participant CRI as containerd/CRI-O
    participant OCI as runc/crun
    participant Kernel as Linux Kernel

    User->>API: kubectl apply -f pod.yaml
    API->>Sched: Pod 스케줄링 요청
    Sched->>API: 노드 할당 결정
    API->>Kubelet: Pod 스펙 전달
    Kubelet->>CRI: CreateContainer (OCI 스펙)
    Note over CRI: resources를 OCI 스펙으로 변환<br/>cpu: 250m → shares: 256<br/>memory: 128Mi → 134217728 bytes
    CRI->>OCI: Container 생성 요청
    OCI->>Kernel: cgroups 디렉토리 생성<br/>cgroups 파일에 값 기록
    Kernel-->>OCI: 완료
    OCI-->>Kubelet: Container ID 반환

변환 과정에서 가장 중요한 부분은 CRI가 Kubernetes의 리소스 스펙을 OCI 스펙으로 변환하는 단계입니다. 이 과정에서 우리가 익숙한 250m, 128Mi 같은 값들이 전혀 다른 숫자로 바뀝니다.

리소스 값 변환 규칙

CPU requests → cpu.weight

CPU requests는 "경합 시 보장받을 CPU 비율"을 의미합니다. cgroups에서는 이를 weight(가중치) 값으로 표현합니다. cgroups v2에서 weight의 범위는 1부터 10000까지입니다.

변환 공식은 다음과 같습니다: weight = 1 + ((cpu_millicores - 2) × 9999) / 262142

실제 예시를 보면, 250m(0.25 CPU)은 약 10의 weight로 변환됩니다. 만약 같은 노드에 weight 10인 컨테이너와 weight 40인 컨테이너가 있고 CPU 경합이 발생하면, 전자는 20%, 후자는 80%의 CPU 시간을 얻습니다.

중요한 점은 CPU에 여유가 있을 때는 weight가 적용되지 않는다는 것입니다. weight는 경합 상황에서만 의미가 있고, 여유 CPU가 있으면 weight가 낮은 컨테이너도 더 많은 CPU를 사용할 수 있습니다.

 

CPU limits → cpu.max

CPU limits는 "사용할 수 있는 CPU 시간의 절대적 상한"입니다. cgroups에서는 cpu.max 파일에 $QUOTA $PERIOD 형식으로 저장됩니다.

500m(0.5 CPU)은 50000 100000으로 변환됩니다. 이는 "100,000 마이크로초(100ms) 동안 최대 50,000 마이크로초(50ms)의 CPU 시간을 사용할 수 있다"는 의미입니다. 계산하면 50000/100000 = 0.5 CPU가 됩니다.

limits에 도달하면 어떻게 될까요? CPU throttling이 발생합니다. 프로세스가 죽지는 않지만, 할당된 quota를 다 쓰면 다음 period까지 CPU 스케줄링에서 제외됩니다. 이것이 Java 애플리케이션에서 "CPU limits를 너무 빡빡하게 잡으면 안 된다"고 하는 이유입니다.

 

Memory requests → memory.min (조건부)

메모리 requests는 원래 스케줄링에만 사용되고 런타임에는 영향을 주지 않았습니다. 하지만 Memory QoS 기능이 활성화되면 memory.min 파일에 값이 설정됩니다. 이 값은 "시스템에 메모리 압박이 있어도 절대 회수되지 않는 보장된 최소 메모리"를 의미합니다.

64Mi67108864(bytes)로 변환됩니다.

 

Memory limits → memory.max

메모리 limits는 "사용할 수 있는 메모리의 절대적 상한"입니다. memory.max 파일에 바이트 단위로 저장됩니다.

128Mi134217728(bytes)로 변환됩니다.

limits에 도달하면 어떻게 될까요? OOM(Out of Memory) Kill이 발생합니다. CPU와 달리 메모리는 스로틀링할 수 없으므로, 제한을 초과하면 커널의 OOM Killer가 컨테이너 내 프로세스를 종료시킵니다.


3. Kubernetes의 cgroups 구조

QoS 클래스와 cgroups 계층 구조

Kubernetes는 Pod의 리소스 설정에 따라 세 가지 QoS(Quality of Service) 클래스를 자동으로 부여합니다. 이 분류는 단순한 라벨이 아니라, 실제로 cgroups 계층 구조에서 Pod가 어디에 위치하는지를 결정합니다.

flowchart TB
    subgraph QoS["QoS 클래스별 cgroups 구조"]
        Kubepods["kubepods.slice"]

        Kubepods --> G["Guaranteed Pod<br/>(kubepods-pod*.slice)"]
        Kubepods --> BurstableSlice["kubepods-burstable.slice"]
        Kubepods --> BestEffortSlice["kubepods-besteffort.slice"]

        BurstableSlice --> B["Burstable Pod<br/>(kubepods-burstable-pod*.slice)"]
        BestEffortSlice --> BE["BestEffort Pod<br/>(kubepods-besteffort-pod*.slice)"]
    end

    subgraph Legend["QoS 클래스 조건"]
        L1["🟢 Guaranteed<br/>모든 컨테이너에 requests = limits"]
        L2["🟡 Burstable<br/>최소 하나의 컨테이너에 requests 또는 limits 설정"]
        L3["🔴 BestEffort<br/>requests, limits 모두 미설정"]
    end

    style G fill:#c8e6c9
    style B fill:#fff9c4
    style BE fill:#ffcdd2

Guaranteed Pod는 kubepods.slice 바로 아래에 직접 위치합니다. 모든 컨테이너에 CPU와 메모리 모두 requests와 limits가 설정되어 있고, 그 값이 동일해야 합니다. 가장 높은 우선순위를 가지며, 노드에 리소스 압박이 있어도 가장 마지막에 축출됩니다.

Burstable Pod는 kubepods-burstable.slice 아래에 위치합니다. 최소 하나의 컨테이너에 requests나 limits가 설정되어 있지만, Guaranteed 조건을 충족하지 못하는 Pod입니다. 중간 우선순위를 가집니다.

BestEffort Pod는 kubepods-besteffort.slice 아래에 위치합니다. 어떤 컨테이너에도 requests나 limits가 설정되지 않은 Pod입니다. 가장 낮은 우선순위를 가지며, 리소스 압박 시 가장 먼저 축출 대상이 됩니다.

cgroups가 QoS에 미치는 실제 영향

이 계층 구조가 실제로 어떤 영향을 미칠까요? 세 가지 시나리오를 살펴보겠습니다.

시나리오 1: 메모리 부족

노드의 메모리가 부족해지면 kubelet은 Pod를 축출(evict)하기 시작합니다. 이때 QoS 클래스에 따라 축출 순서가 결정됩니다. BestEffort Pod가 가장 먼저 축출되고, 그 다음 Burstable, 마지막으로 Guaranteed 순입니다.

시나리오 2: CPU 경합

모든 Pod가 CPU를 최대로 사용하려고 할 때, cgroups의 cpu.weight에 따라 CPU 시간이 분배됩니다. Guaranteed Pod는 requests에 해당하는 weight를 가지므로 안정적인 CPU를 보장받습니다. BestEffort Pod는 최소 weight(1)를 가지므로 가장 적은 CPU를 얻습니다.

시나리오 3: OOM Killer 발동

컨테이너가 메모리 limit을 초과하면 OOM Killer가 발동합니다. cgroups v2에서는 memory.oom.group 설정에 따라 컨테이너 내 모든 프로세스가 함께 종료됩니다. 이는 Kubernetes 1.28부터 기본 동작입니다.

Pod 내 다중 컨테이너의 cgroups 구조

하나의 Pod에 여러 컨테이너가 있으면 어떻게 될까요? 각 컨테이너는 Pod의 cgroup 아래에 별도의 scope로 생성됩니다.

kubepods-burstable-pod<UUID>.slice/           # Pod 레벨 cgroup
├── cri-containerd-<CONTAINER_A_ID>.scope/    # 메인 컨테이너
│   ├── cpu.weight    # 컨테이너 A의 CPU 설정
│   └── memory.max    # 컨테이너 A의 메모리 설정
└── cri-containerd-<CONTAINER_B_ID>.scope/    # 사이드카 컨테이너
    ├── cpu.weight    # 컨테이너 B의 CPU 설정
    └── memory.max    # 컨테이너 B의 메모리 설정

Pod 레벨의 cgroup은 모든 컨테이너의 리소스 합계에 대한 상한을 설정합니다. 예를 들어, Pod에 overhead가 설정되어 있으면 Pod 레벨 cgroup에 반영됩니다.


4. cgroups v1 vs v2: 무엇이 달라졌나

cgroups의 역사

cgroups는 두 가지 버전이 존재합니다. cgroups v1은 2008년 Linux 2.6.24에 도입되었고, cgroups v2는 2016년 Linux 4.5에서 도입되었습니다. 두 버전은 상당히 다른 아키텍처를 가지고 있습니다.

구조적 차이

flowchart TB
    subgraph V1["cgroups v1 (레거시)"]
        direction TB
        V1Root["루트"]
        V1Root --> V1CPU["/sys/fs/cgroup/cpu/"]
        V1Root --> V1Mem["/sys/fs/cgroup/memory/"]
        V1Root --> V1IO["/sys/fs/cgroup/blkio/"]
        V1Root --> V1Etc["... (기타 컨트롤러)"]

        V1CPU --> V1CPU_Pod["kubepods/pod-xxx"]
        V1Mem --> V1Mem_Pod["kubepods/pod-xxx"]
        V1IO --> V1IO_Pod["kubepods/pod-xxx"]
    end

    subgraph V2["cgroups v2 (통합)"]
        direction TB
        V2Root["/sys/fs/cgroup/"]
        V2Root --> V2Kubepods["kubepods.slice/"]
        V2Kubepods --> V2Pod["kubepods-burstable-pod-xxx.slice/"]
        V2Pod --> V2Container["cri-containerd-xxx.scope/"]
        V2Container --> V2Files["cpu.weight<br/>cpu.max<br/>memory.max<br/>io.weight<br/>(모든 컨트롤러 통합)"]
    end

    style V1 fill:#ffebee
    style V2 fill:#e8f5e9

 

cgroups v1의 문제점

cgroups v1은 리소스 종류별로 별도의 계층 구조를 가집니다. CPU는 /sys/fs/cgroup/cpu/, 메모리는 /sys/fs/cgroup/memory/, I/O는 /sys/fs/cgroup/blkio/에 각각 독립적인 디렉토리 트리가 있습니다.

이 구조의 문제는 프로세스가 각 컨트롤러마다 다른 그룹에 속할 수 있다는 점입니다. 예를 들어, 프로세스 A가 CPU 컨트롤러에서는 그룹 X에, 메모리 컨트롤러에서는 그룹 Y에 속할 수 있습니다. 이런 복잡성은 관리를 어렵게 만들고 예측하기 힘든 동작을 유발합니다.

 

cgroups v2의 개선

cgroups v2는 통합된 단일 계층 구조를 가집니다. 모든 컨트롤러가 하나의 트리를 공유하며, 프로세스는 정확히 하나의 그룹에만 속합니다. 이로 인해 관리가 훨씬 단순해지고 동작이 예측 가능해집니다.

기능 비교

cgroups v2에서 추가되거나 개선된 주요 기능들을 살펴보겠습니다.

memory.high (메모리 스로틀링)

cgroups v1에서는 메모리 제한을 초과하면 바로 OOM Kill이 발생했습니다. cgroups v2에서는 memory.high 임계값을 설정할 수 있습니다. 이 값에 도달하면 OOM Kill 대신 메모리 할당 속도가 느려지는 스로틀링이 발생합니다. 프로세스에게 메모리를 정리할 기회를 주는 것입니다.

 

memory.min (보장된 메모리)

memory.min은 시스템 전체에 메모리 압박이 있어도 절대 회수되지 않는 보장된 메모리 양입니다. Kubernetes의 memory requests 개념을 커널 레벨에서 강제할 수 있게 됩니다.

 

PSI (Pressure Stall Information)

PSI는 CPU, 메모리, I/O 각각에 대해 리소스 부족으로 인해 작업이 지연되는 정도를 측정합니다. 단순히 "사용량 80%"가 아니라 "리소스 부족으로 인해 실제로 10%의 시간이 대기 상태"와 같은 정보를 제공합니다. 이를 통해 더 정교한 오토스케일링과 문제 진단이 가능합니다.

 

memory.oom.group (cgroup 인식 OOM Killer)

cgroups v1에서 OOM Killer는 프로세스 단위로 동작했습니다. 컨테이너에 메인 프로세스와 여러 자식 프로세스가 있을 때, OOM Killer가 일부 프로세스만 죽여서 컨테이너가 불안정한 상태가 될 수 있었습니다.

cgroups v2의 memory.oom.group을 활성화하면 cgroup 전체가 하나의 단위로 처리됩니다. 메모리 제한 초과 시 cgroup 내 모든 프로세스가 함께 종료되어, "절반만 죽은" 불안정한 상태를 방지합니다.

 

Swap 제어 개선

cgroups v2에서는 swap 사용량을 메모리와 별도로 제어할 수 있습니다. cgroups v1에서는 memory+swap을 합산한 값만 제한할 수 있었습니다.

버전 확인 방법

현재 시스템이 어떤 cgroups 버전을 사용하는지 확인하는 방법입니다.

# 파일시스템 타입으로 확인
stat -fc %T /sys/fs/cgroup/

# cgroup2fs 출력 → cgroups v2
# tmpfs 또는 cgroup 출력 → cgroups v1 (또는 하이브리드)
# 마운트 정보로 확인
mount | grep cgroup

# cgroups v2: cgroup2 on /sys/fs/cgroup type cgroup2
# cgroups v1: cgroup on /sys/fs/cgroup/cpu type cgroup (rw,nosuid,...)

5. Kubernetes 버전 업그레이드에 따른 Deprecation

cgroups v1 Deprecation 타임라인

Kubernetes 프로젝트는 cgroups v1에서 v2로의 전환을 위해 명확한 타임라인을 발표했습니다. 이 타임라인을 이해하는 것은 클러스터 운영자에게 매우 중요합니다.

timeline
    title Kubernetes cgroups v2 전환 타임라인

    2022년 8월 : Kubernetes 1.25
                : cgroups v2 GA
                : v1, v2 모두 완전 지원

    2024년 8월 : Kubernetes 1.31
                : cgroups v1 유지보수 모드
                : 새 기능은 v2 전용
                : 보안 수정만 제공

    2025년 12월 : Kubernetes 1.35
                 : cgroups v1 Deprecated
                 : failCgroupV1=true 기본값
                 : kubelet 시작 거부

    2026년 이후 : Kubernetes 1.36+
                 : cgroups v1 완전 제거
                 : v2만 지원

Kubernetes 1.25 (2022년 8월): cgroups v2 지원이 GA(General Availability)에 도달했습니다. 이 시점부터 cgroups v2를 프로덕션에서 안심하고 사용할 수 있게 되었습니다. v1과 v2 모두 완전히 지원됩니다.

 

Kubernetes 1.31 (2024년 8월): KEP-4569에 따라 cgroups v1이 유지보수 모드(maintenance mode)에 진입했습니다. 이는 다음을 의미합니다.

첫째, 새로운 기능은 cgroups v2에서만 개발됩니다. Memory QoS, PSI 메트릭, 개선된 swap 지원 등 모든 새 기능은 v2 전용입니다.

둘째, cgroups v1에 대해서는 보안 취약점 수정만 제공됩니다. 일반적인 버그 수정도 "최선의 노력(best effort)" 수준으로만 제공됩니다.

 

Kubernetes 1.35 (2025년 12월): cgroups v1이 공식적으로 deprecated됩니다. failCgroupV1 플래그가 기본값 true로 설정되어, cgroups v1 노드에서 kubelet이 시작을 거부합니다. 관리자가 명시적으로 failCgroupV1: false를 설정하면 여전히 사용할 수 있지만, 권장되지 않습니다.

 

Kubernetes 1.36+ (2026년 이후): cgroups v1 지원이 완전히 제거될 예정입니다. 정확한 시점은 커뮤니티 논의에 따라 결정되지만, 표준 deprecation 정책에 따르면 deprecated 후 2~3개 릴리스 내에 제거됩니다.

컨테이너 런타임 요구사항

Kubernetes만 업그레이드해서는 안 됩니다. 컨테이너 런타임과 OCI 런타임도 cgroups v2를 지원하는 버전이어야 합니다.

 

containerd

containerd 1.4(2020년 8월)부터 cgroups v2를 지원합니다. 현재 프로덕션에서는 containerd 1.6 이상, 가능하면 2.x 사용을 권장합니다. 특히 Kubernetes 1.35는 containerd 1.x를 지원하는 마지막 버전입니다. Kubernetes 1.36부터는 containerd 2.x가 필수입니다.

containerd 2.0에서는 cgroups v1 지원이 deprecated되었고, 2029년 5월경 완전히 제거될 예정입니다.

 

CRI-O

CRI-O 1.20부터 cgroups v2를 지원합니다. CRI-O 1.31(2024년 9월)에서 중요한 변화가 있었는데, 기본 OCI 런타임이 runc에서 crun으로 변경되었습니다. crun은 C로 작성되어 runc보다 시작 시간이 빠르고 메모리 사용량이 적습니다.

 

runc

runc 1.1부터 cgroups v2를 완전히 지원합니다. runc 1.3.0(2025년)에서 cgroups v1 지원이 공식적으로 deprecated되었습니다.

운영체제 요구사항

cgroups v2를 사용하려면 Linux 커널 5.8 이상이 필요합니다. 주요 배포판별 기본 cgroups 버전은 다음과 같습니다.

Ubuntu 21.10 이상, Fedora 31 이상, RHEL 9 이상, Debian 11 이상에서 cgroups v2가 기본값입니다. Amazon Linux 2023도 cgroups v2를 기본으로 사용합니다. 반면 RHEL 8, Amazon Linux 2, Ubuntu 20.04 이하는 cgroups v1이 기본값이며, 커널 파라미터를 수정해야 v2를 사용할 수 있습니다.

cgroups v2를 활성화하려면 커널 부트 파라미터에 다음을 추가합니다.

# /etc/default/grub 수정
GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"

# 또는 커널 파라미터로 직접
cgroup_no_v1=all

6. Kubernetes 1.28-1.35 주요 cgroups 변경사항

Kubernetes 1.28 (2023년 8월)

1.28은 cgroups 관련 중요한 변경이 많았던 릴리스입니다.

cgroups v1에서 Swap 지원 제거

가장 큰 변화는 cgroups v1에서 swap 기능이 완전히 제거된 것입니다. swap을 사용하려면 반드시 cgroups v2로 마이그레이션해야 합니다.

cgroup 인식 OOM Killer 활성화

memory.oom.group이 기본으로 활성화되었습니다. 컨테이너가 메모리 제한을 초과하면 해당 cgroup 내의 모든 프로세스가 함께 종료됩니다. 이전에는 OOM Killer가 개별 프로세스를 선택적으로 종료했기 때문에, 컨테이너가 "절반만 죽은" 불안정한 상태가 될 수 있었습니다.

이 변화로 인해 일부 애플리케이션의 동작이 달라질 수 있습니다. 특히 메인 프로세스와 자식 프로세스가 있는 경우, 이전에는 자식 프로세스만 죽고 메인 프로세스가 이를 감지해 복구할 수 있었지만, 이제는 모두 함께 종료됩니다.

LimitedSwap 모드 개선

swap을 사용할 수 있는 양이 더 정교하게 계산됩니다. 공식은 다음과 같습니다: 컨테이너 swap 한도 = (메모리 requests / 노드 메모리 용량) × 노드 swap 용량

Kubernetes 1.30 (2024년 4월)

Swap 기본값 변경

NoSwap이 기본 swap 정책이 되었습니다. UnlimitedSwap 모드는 완전히 제거되었습니다. 이제 swap을 사용하려면 명시적으로 LimitedSwap 모드를 설정해야 합니다.

NoSwap 모드에서 kubelet은 swap이 활성화된 노드에서도 실행될 수 있지만, Pod는 swap을 사용할 수 없습니다. 이는 swap에 대한 제어를 유지하면서 호환성을 제공하기 위한 설계입니다.

또한, node-critical 또는 cluster-critical 우선순위 클래스를 가진 Pod는 설정과 관계없이 swap을 사용할 수 없게 되었습니다.

Kubernetes 1.31 (2024년 8월)

cgroups v1 유지보수 모드 진입

앞서 설명한 대로, 이 릴리스부터 cgroups v1이 유지보수 모드에 들어갔습니다.

자동 cgroup 드라이버 감지 (Beta)

KubeletCgroupDriverFromCRI 기능이 Beta로 승격되었습니다. kubelet이 컨테이너 런타임에게 cgroup 드라이버 설정을 자동으로 질의합니다. 이전에는 kubelet과 컨테이너 런타임에서 각각 수동으로 cgroup 드라이버를 설정해야 했고, 두 설정이 불일치하면 문제가 발생했습니다.

Kubernetes 1.34 (2025년 8월)

PSI 메트릭 Beta

KubeletPSI feature gate가 Beta로 승격되어 기본 활성화되었습니다. 노드, Pod, 컨테이너 레벨에서 CPU, 메모리, I/O에 대한 압력 메트릭을 수집할 수 있습니다.

Kubernetes 1.35 (2025년 12월)

cgroups v1 Deprecated

failCgroupV1 플래그가 기본값 true가 됩니다. cgroups v1 노드에서 kubelet이 시작을 거부합니다.

자동 cgroup 드라이버 감지 GA

KubeletCgroupDriverFromCRI 기능이 GA에 도달했습니다. kubelet 설정에서 cgroupDriver를 명시적으로 설정할 필요가 없어졌습니다.

containerd 1.x 지원 종료

Kubernetes 1.35가 containerd 1.x를 지원하는 마지막 버전입니다. 1.36 업그레이드 전에 containerd 2.x로 업그레이드해야 합니다.

Memory QoS 현황 (Alpha 정체)

Memory QoS(KEP-2570)는 2021년에 Alpha로 도입되었지만, 2025년 현재까지 Alpha 상태에 머물러 있습니다. Kubernetes 1.28에서 Beta 승격이 시도되었지만 미해결 이슈로 인해 중단되었습니다.

Memory QoS가 활성화되면 Kubernetes의 memory requests가 실제로 cgroups의 memory.min에 반영됩니다. 또한 memory.high를 사용한 스로틀링도 가능해집니다. 하지만 Alpha 기능이므로 프로덕션 사용은 권장되지 않습니다.

Memory QoS와 혼동하기 쉬운 Memory Manager(KEP-1769)는 별개의 기능입니다. Memory Manager는 Kubernetes 1.32에서 GA에 도달했으며, Guaranteed QoS Pod에 대한 NUMA 인식 메모리 토폴로지 할당을 담당합니다. cgroups의 소프트 리밋과는 관련이 없습니다.


7. cgroups 모니터링: 실무에서 활용하기

cgroups 파일 직접 조회

가장 기본적인 방법은 cgroupfs를 직접 조회하는 것입니다. 트러블슈팅 시 유용합니다.

# 컨테이너 ID 확인
CONTAINER_ID=$(crictl ps --name <container-name> -q)

# cgroup 경로 확인
CGROUP_PATH=$(crictl inspect $CONTAINER_ID | jq -r '.info.runtimeSpec.linux.cgroupsPath')
# 예: kubepods-burstable-pod<UUID>.slice:cri-containerd:<ID>

# cgroups v2 경로로 변환 (slice와 scope 구분자 변환)
cd /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/kubepods-burstable-pod<UUID>.slice/cri-containerd-<ID>.scope

# 현재 메모리 사용량
cat memory.current
# 출력: 52428800 (bytes, 약 50MB)

# 메모리 limit
cat memory.max
# 출력: 134217728 (bytes, 128MB)

# 메모리 피크 사용량 (runc 1.1.11+, cgroups v2)
cat memory.peak
# 출력: 67108864 (bytes, 이전 최대 사용량)

# CPU 사용 통계
cat cpu.stat
# usage_usec 12345678      # 총 CPU 사용 시간 (마이크로초)
# user_usec 10000000       # 사용자 모드 CPU 시간
# system_usec 2345678      # 커널 모드 CPU 시간
# nr_periods 1000          # CPU period 수
# nr_throttled 50          # 스로틀링된 period 수
# throttled_usec 5000000   # 총 스로틀링 시간

cAdvisor와 kubelet 메트릭

kubelet에는 cAdvisor가 내장되어 있어 cgroups 데이터를 수집하고 메트릭으로 노출합니다.

# 노드에서 직접 cAdvisor 메트릭 조회
curl -sk https://localhost:10250/metrics/cadvisor \
  --key /etc/kubernetes/pki/apiserver-kubelet-client.key \
  --cacert /etc/kubernetes/pki/ca.crt \
  --cert /etc/kubernetes/pki/apiserver-kubelet-client.crt

# kubectl proxy를 통한 조회
kubectl proxy &
curl http://localhost:8001/api/v1/nodes/<node-name>/proxy/metrics/cadvisor

주요 cAdvisor 메트릭들을 살펴보겠습니다.

메모리 관련 메트릭

container_memory_usage_bytes는 컨테이너의 현재 메모리 사용량입니다. cgroups의 memory.current와 동일한 값입니다.

container_memory_working_set_bytes는 활성 메모리(working set) 사용량입니다. 전체 사용량에서 비활성 파일 캐시를 뺀 값으로, OOM Killer가 참조하는 값과 더 가깝습니다. 메모리 limit과 비교할 때는 이 메트릭을 사용해야 합니다.

container_spec_memory_limit_bytes는 컨테이너에 설정된 메모리 limit입니다. cgroups의 memory.max와 동일합니다.

container_memory_rss는 RSS(Resident Set Size), 즉 실제 물리 메모리에 있는 데이터 크기입니다.

container_memory_cache는 페이지 캐시 크기입니다. 파일 I/O를 많이 하는 컨테이너에서 높게 나타납니다.

CPU 관련 메트릭

container_cpu_usage_seconds_total은 컨테이너가 사용한 총 CPU 시간(초)입니다. Counter 타입이므로 rate()를 적용해서 사용합니다.

container_cpu_cfs_throttled_periods_total은 CPU 스로틀링이 발생한 period 수입니다. 이 값이 증가하면 컨테이너가 CPU limit에 걸려 대기하고 있다는 의미입니다.

container_cpu_cfs_throttled_seconds_total은 CPU 스로틀링으로 인해 대기한 총 시간(초)입니다.

container_spec_cpu_shares는 CPU shares(weight) 값입니다. cgroups v1의 cpu.shares 또는 v2의 cpu.weight에서 변환된 값입니다.

container_spec_cpu_quotacontainer_spec_cpu_period는 CPU limit 설정입니다. quota/period = CPU 코어 수입니다.

PSI (Pressure Stall Information) 메트릭

PSI는 cgroups v2에서 제공하는 고급 모니터링 기능으로, Kubernetes 1.34부터 Beta로 제공됩니다. 단순한 사용량 퍼센트가 아니라 리소스 부족으로 인한 실제 지연을 측정합니다.

# PSI 메트릭 직접 조회 (cgroups v2)
cat /sys/fs/cgroup/kubepods.slice/.../cpu.pressure
# some avg10=0.50 avg60=0.30 avg300=0.10 total=12345678
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

cat /sys/fs/cgroup/kubepods.slice/.../memory.pressure
# some avg10=1.20 avg60=0.80 avg300=0.50 total=98765432
# full avg10=0.10 avg60=0.05 avg300=0.02 total=1234567

some일부 작업이 리소스를 기다리는 시간 비율입니다. 예를 들어 some avg10=0.50은 "최근 10초 동안 평균적으로 시간의 0.5%가 일부 작업이 CPU를 기다리는 데 사용되었다"는 의미입니다.

full모든 작업이 리소스를 기다리는 시간 비율입니다. 이 값이 높으면 심각한 리소스 부족 상태입니다.

Kubernetes는 PSI 데이터를 다음 메트릭으로 노출합니다.

# Summary API를 통한 PSI 메트릭
curl -k https://localhost:10250/stats/summary

# Prometheus 메트릭
container_pressure_cpu_waiting_seconds_total
container_pressure_memory_waiting_seconds_total
container_pressure_io_waiting_seconds_total

PSI 메트릭이 유용한 이유는 "리소스 사용량 80%"와 "리소스 부족으로 인한 대기 시간 10%"는 완전히 다른 정보이기 때문입니다. 전자는 여유가 20% 있다고 말하지만, 후자는 실제로 성능에 영향을 주고 있는지 알려줍니다.

Prometheus + Grafana 대시보드

실무에서는 Prometheus와 Grafana를 조합해서 cgroups 메트릭을 시각화합니다. 유용한 쿼리 예시를 소개합니다.

메모리 사용률 (limit 대비)

# working_set 기준 메모리 사용률
sum(container_memory_working_set_bytes{container!=""}) by (namespace, pod, container)
/
sum(container_spec_memory_limit_bytes{container!=""}) by (namespace, pod, container)
* 100

CPU 스로틀링 비율

# 스로틀링된 period 비율
rate(container_cpu_cfs_throttled_periods_total{container!=""}[5m])
/
rate(container_cpu_cfs_periods_total{container!=""}[5m])
* 100

이 값이 5% 이상이면 CPU limit 증가를 고려해야 합니다. 20% 이상이면 심각한 CPU 부족 상태입니다.

OOM Kill 발생 횟수

# OOM으로 종료된 컨테이너 수
increase(kube_pod_container_status_restarts_total{reason="OOMKilled"}[1h])

PSI 기반 리소스 압력

# CPU 압력이 높은 Pod (1% 이상 대기)
container_pressure_cpu_waiting_seconds_total > 0.01

트러블슈팅 체크리스트

컨테이너가 예상과 다르게 동작할 때 확인할 항목들입니다.

메모리 문제 진단

  1. OOM Kill 발생 여부: dmesg | grep -i "killed process" 또는 kubectl describe pod에서 OOMKilled 확인
  2. 현재 메모리 사용량 vs limit: memory.current vs memory.max 비교
  3. working_set vs 전체 사용량: 캐시를 제외한 실제 메모리 확인
  4. memory.high 스로틀링 여부: Memory QoS 활성화 시 확인

CPU 문제 진단

  1. 스로틀링 발생 여부: cpu.statnr_throttled 값 확인
  2. 스로틀링 시간: throttled_usec 값이 증가하는지 확인
  3. CPU limit vs 실제 사용: container_cpu_usage_seconds_total rate와 limit 비교
  4. weight 확인: 경합 시 적절한 CPU 비율을 받고 있는지 확인

참고 자료

Kubernetes 공식 문서

Linux cgroups 문서

원본 블로그