Linux cgroup v2 리소스 제어 심화

cgroup v2란?

Linux cgroup(control group) v2는 프로세스 그룹의 CPU, 메모리, I/O, PID 등 시스템 리소스를 제어하는 커널 메커니즘입니다. Kubernetes, Docker, systemd 모두 내부적으로 cgroup을 사용하여 컨테이너 리소스를 격리합니다. cgroup v1에서 v2로의 전환은 통합 계층 구조(unified hierarchy)를 도입하여 리소스 관리를 단순화하고, 새로운 컨트롤러(PSI, memory.high 등)를 추가했습니다.

이 글에서는 cgroup v2의 핵심 구조, 각 컨트롤러별 실전 설정, Kubernetes와의 관계, 그리고 운영 시 트러블슈팅까지 심층적으로 다룹니다. Linux eBPF 관측성 심화 가이드와 함께 읽으면 리소스 제어 + 관측의 전체 그림을 파악할 수 있습니다.

cgroup v1 vs v2 핵심 차이

항목 cgroup v1 cgroup v2
계층 구조 컨트롤러별 독립 계층 (cpu, memory 별도 트리) 단일 통합 계층 (모든 컨트롤러 한 트리)
프로세스 배치 컨트롤러마다 다른 그룹 가능 프로세스는 하나의 그룹에만 소속
스레드 제어 제한적 threaded cgroup 지원
PSI(Pressure Stall Info) 미지원 cpu.pressure, memory.pressure, io.pressure
메모리 보호 memory.limit_in_bytes만 memory.min(보장), memory.low(소프트), memory.high(스로틀), memory.max(킬)
I/O 제어 blkio (직접 I/O만) io (buffered I/O까지 제어)
위임(Delegation) 보안 이슈 있음 안전한 위임 모델 (rootless 컨테이너)

cgroup v2 파일 시스템 구조

cgroup v2는 /sys/fs/cgroup에 단일 통합 계층으로 마운트됩니다:

# cgroup v2 확인
mount | grep cgroup2
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

# 루트 cgroup 구조
ls /sys/fs/cgroup/
# cgroup.controllers      - 사용 가능한 컨트롤러 목록
# cgroup.subtree_control  - 하위에 활성화할 컨트롤러
# cgroup.procs            - 이 그룹의 프로세스 PID들
# cpu.stat                - CPU 사용 통계
# memory.current          - 현재 메모리 사용량
# io.stat                 - I/O 통계
# pids.current            - 현재 PID 수

# 하위 cgroup 생성
mkdir /sys/fs/cgroup/myapp

# 컨트롤러 활성화 (부모에서)
echo "+cpu +memory +io +pids" > /sys/fs/cgroup/cgroup.subtree_control

# 프로세스 배치
echo $PID > /sys/fs/cgroup/myapp/cgroup.procs

CPU 컨트롤러 심화

cgroup v2의 CPU 컨트롤러는 가중치 기반 분배대역폭 제한 두 가지 메커니즘을 제공합니다:

# cpu.weight: 상대적 가중치 (1-10000, 기본값 100)
# CPU 경합 시 가중치 비율로 분배
echo 200 > /sys/fs/cgroup/myapp/cpu.weight      # 기본의 2배
echo 50  > /sys/fs/cgroup/background/cpu.weight  # 기본의 0.5배
# → 경합 시 myapp이 4배 더 CPU를 할당받음

# cpu.max: 대역폭 제한 (quota period 형식, 마이크로초)
echo "200000 100000" > /sys/fs/cgroup/myapp/cpu.max
# → 100ms 주기 중 200ms 사용 가능 = 2 CPU 코어 제한

# 무제한
echo "max 100000" > /sys/fs/cgroup/myapp/cpu.max

# cpu.pressure: PSI (Pressure Stall Information)
cat /sys/fs/cgroup/myapp/cpu.pressure
# some avg10=0.00 avg60=0.00 avg300=0.00 total=0
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
# → some: 일부 태스크가 CPU 대기, full: 모든 태스크가 대기

# PSI 알림 설정 (500ms 윈도우에서 100ms 이상 stall 시 알림)
echo "some 100000 500000" > /sys/fs/cgroup/myapp/cpu.pressure

Kubernetes와의 매핑

# K8s Pod spec
resources:
  requests:
    cpu: "500m"    # → cpu.weight 계산에 사용 (상대적)
  limits:
    cpu: "2000m"   # → cpu.max = "200000 100000" (2코어)

# 실제 cgroup 경로 (containerd 기준)
# /sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/
#   kubepods-burstable-pod{uid}.slice/
#     cri-containerd-{container-id}.scope/
#       cpu.max

메모리 컨트롤러 심화

cgroup v2 메모리 컨트롤러의 핵심은 4단계 메모리 제어입니다:

# memory.min: 절대 보장 (이 양은 절대 회수 안됨, OOM killer도 무시)
echo 256M > /sys/fs/cgroup/myapp/memory.min

# memory.low: 소프트 보장 (시스템 전체 메모리 부족 시에만 회수)
echo 512M > /sys/fs/cgroup/myapp/memory.low

# memory.high: 스로틀 경계 (초과 시 direct reclaim으로 느려짐, 킬은 안됨)
echo 1G > /sys/fs/cgroup/myapp/memory.high

# memory.max: 하드 리밋 (초과 시 OOM kill)
echo 2G > /sys/fs/cgroup/myapp/memory.max

# 메모리 사용량 확인
cat /sys/fs/cgroup/myapp/memory.current    # 현재 사용량
cat /sys/fs/cgroup/myapp/memory.peak       # 피크 사용량 (v6.1+)

# 상세 통계
cat /sys/fs/cgroup/myapp/memory.stat
# anon 104857600          - 익명 메모리 (heap, stack)
# file 52428800           - 파일 캐시
# slab 8388608            - 커널 슬랩
# sock 1048576            - 소켓 버퍼
# pgfault 12345           - 페이지 폴트 수
# pgmajfault 0            - 메이저 폴트 수 (디스크 I/O 발생)

# OOM 이벤트 모니터링
cat /sys/fs/cgroup/myapp/memory.events
# low 0         - memory.low 경계 도달 횟수
# high 3        - memory.high 경계 도달 횟수
# max 1         - memory.max 경계 도달 횟수
# oom 0         - OOM 발생 횟수
# oom_kill 0    - OOM kill 횟수
# oom_group_kill 0

실전 팁: memory.highmemory.max의 80~90%로 설정하면, OOM kill 전에 프로세스가 느려지는 “경고 단계”를 만들 수 있어 갑작스러운 kill을 방지할 수 있습니다.

I/O 컨트롤러 심화

cgroup v2의 I/O 컨트롤러는 v1의 blkio를 대체하며, buffered I/O까지 제어할 수 있는 것이 가장 큰 차이입니다:

# 디바이스 번호 확인
lsblk -o NAME,MAJ:MIN
# sda   8:0

# io.max: 대역폭·IOPS 절대 제한
# 형식: MAJ:MIN rbps=바이트 wbps=바이트 riops=횟수 wiops=횟수
echo "8:0 rbps=104857600 wbps=52428800 riops=1000 wiops=500" 
  > /sys/fs/cgroup/myapp/io.max
# → 읽기 100MB/s, 쓰기 50MB/s, 읽기 1000 IOPS, 쓰기 500 IOPS

# io.weight: 상대적 가중치 (1-10000, 기본값 100)
echo "8:0 200" > /sys/fs/cgroup/myapp/io.weight

# io.latency: 지연 시간 목표 (마이크로초)
echo "8:0 target=5000" > /sys/fs/cgroup/myapp/io.latency
# → 5ms 지연 목표, 다른 cgroup의 I/O를 스로틀하여 보장

# I/O 통계 확인
cat /sys/fs/cgroup/myapp/io.stat
# 8:0 rbytes=1048576 wbytes=524288 rios=100 wios=50 dbytes=0 dios=0

# I/O 압력 모니터링
cat /sys/fs/cgroup/myapp/io.pressure
# some avg10=2.50 avg60=1.20 avg300=0.80 total=45678
# full avg10=0.10 avg60=0.05 avg300=0.02 total=1234

PID 컨트롤러와 포크 폭탄 방지

# pids.max: 최대 프로세스 수 제한
echo 1000 > /sys/fs/cgroup/myapp/pids.max

# pids.current: 현재 프로세스 수
cat /sys/fs/cgroup/myapp/pids.current

# pids.events: 제한 도달 횟수
cat /sys/fs/cgroup/myapp/pids.events
# max 0  → pids.max에 도달한 횟수

Kubernetes에서는 --pod-max-pids kubelet 옵션으로 Pod별 PID 제한을 설정합니다. 이는 내부적으로 cgroup의 pids.max를 설정합니다.

PSI (Pressure Stall Information) 활용

PSI는 cgroup v2의 킬러 피처로, 시스템 리소스 부족 상태를 정량적으로 측정합니다. K8s 리소스 Right-Sizing의 근거 데이터로 활용할 수 있습니다:

# 시스템 전체 PSI
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/io

# cgroup별 PSI
cat /sys/fs/cgroup/myapp/cpu.pressure
cat /sys/fs/cgroup/myapp/memory.pressure
cat /sys/fs/cgroup/myapp/io.pressure

# 형식: {type} avg10={%} avg60={%} avg300={%} total={us}
# some: 하나 이상의 태스크가 리소스 대기 (부분 stall)
# full: 모든 태스크가 리소스 대기 (완전 stall)

# PSI 기반 모니터링 스크립트
#!/bin/bash
while true; do
  CPU_SOME=$(awk '/some/ {print $2}' /sys/fs/cgroup/myapp/cpu.pressure | cut -d= -f2)
  MEM_SOME=$(awk '/some/ {print $2}' /sys/fs/cgroup/myapp/memory.pressure | cut -d= -f2)
  IO_FULL=$(awk '/full/ {print $2}' /sys/fs/cgroup/myapp/io.pressure | cut -d= -f2)
  
  echo "$(date) CPU=$CPU_SOME% MEM=$MEM_SOME% IO_FULL=$IO_FULL%"
  
  # 임계값 초과 시 알림
  if (( $(echo "$MEM_SOME > 25.0" | bc -l) )); then
    echo "ALERT: Memory pressure high!"
  fi
  sleep 10
done

systemd와 cgroup v2 통합

systemd는 cgroup v2의 기본 관리자입니다. 서비스 유닛에서 직접 리소스 제한을 선언할 수 있습니다:

# /etc/systemd/system/myapp.service
[Service]
ExecStart=/usr/bin/myapp

# CPU 제어
CPUWeight=200               # cpu.weight (기본 100)
CPUQuota=200%                # cpu.max (2코어)

# 메모리 제어
MemoryMin=256M               # memory.min
MemoryLow=512M               # memory.low
MemoryHigh=1536M             # memory.high
MemoryMax=2G                 # memory.max
MemorySwapMax=0              # memory.swap.max (스왑 비활성화)

# I/O 제어
IOWeight=200                 # io.weight
IOReadBandwidthMax=/dev/sda 100M   # io.max rbps
IOWriteBandwidthMax=/dev/sda 50M   # io.max wbps
IOReadIOPSMax=/dev/sda 1000        # io.max riops

# PID 제한
TasksMax=1000                # pids.max

# 적용 확인
systemctl daemon-reload
systemctl restart myapp
systemd-cgtop  # 실시간 cgroup 리소스 모니터링

Kubernetes에서의 cgroup v2

Kubernetes 1.25+에서 cgroup v2가 GA(정식)로 지원됩니다. kubelet이 cgroup v2를 자동 감지하여 Pod 리소스를 관리합니다:

# cgroup v2 지원 확인
stat -fc %T /sys/fs/cgroup/
# cgroup2fs → v2 사용 중

# kubelet 설정 (cgroup v2 전용 기능)
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd          # 권장 (cgroup v2 + systemd)
memoryThrottlingFactor: 0.9    # memory.high = memory.max * 0.9
podPidsLimit: 4096             # Pod당 PID 제한

# K8s 1.27+: MemoryQoS 기능 (memory.min/memory.high 활용)
# Guaranteed Pod → memory.min = requests
# Burstable Pod → memory.high = limits * memoryThrottlingFactor

# Pod의 실제 cgroup 경로 확인
CONTAINER_ID=$(crictl ps --name mycontainer -q)
crictl inspect $CONTAINER_ID | jq '.info.runtimeSpec.linux.cgroupsPath'
# "kubepods-burstable-podXXX.slice:cri-containerd:YYY"

# 해당 cgroup 리소스 확인
CGROUP_PATH="/sys/fs/cgroup/kubepods.slice/kubepods-burstable.slice/..."
cat $CGROUP_PATH/cpu.max
cat $CGROUP_PATH/memory.max
cat $CGROUP_PATH/memory.high
cat $CGROUP_PATH/memory.min

트러블슈팅 실전

1. OOM Kill 원인 분석

# dmesg에서 OOM 로그 확인
dmesg | grep -A 20 "oom-kill"

# cgroup OOM 이벤트 확인
cat /sys/fs/cgroup/myapp/memory.events
# oom_kill 3 → 3번 OOM kill 발생

# 메모리 상세 분석
cat /sys/fs/cgroup/myapp/memory.stat | grep -E "^(anon|file|slab|sock)"
# anon이 크면 → 힙 메모리 누수 의심
# file이 크면 → 파일 캐시 과다 (보통 정상)
# sock이 크면 → 소켓 버퍼 과다 (커넥션 수 확인)

2. CPU 스로틀링 진단

# CPU 스로틀링 확인
cat /sys/fs/cgroup/myapp/cpu.stat
# usage_usec 1234567890    - 총 CPU 사용 시간
# user_usec 1000000000     - 유저 공간
# system_usec 234567890    - 커널 공간
# nr_periods 50000         - 스케줄링 주기 수
# nr_throttled 1200        - 스로틀된 주기 수
# throttled_usec 60000000  - 스로틀된 총 시간

# 스로틀 비율 계산
# throttle_ratio = nr_throttled / nr_periods
# 1200 / 50000 = 2.4% → 2.4% 시간 동안 CPU 제한됨
# 5% 이상이면 cpu.max 증가 고려

3. I/O 병목 진단

# I/O 압력 확인
cat /sys/fs/cgroup/myapp/io.pressure
# some avg10=15.30 → 10초 평균 15.3% 시간 I/O 대기
# full avg10=5.20  → 10초 평균 5.2% 시간 완전 I/O stall

# some > 20%: I/O 병목 가능성
# full > 10%: 심각한 I/O 병목

# I/O 사용량 확인
cat /sys/fs/cgroup/myapp/io.stat
# 읽기/쓰기 바이트와 IOPS 확인하여 io.max 조정

정리

cgroup v2는 Linux 리소스 관리의 근본 메커니즘입니다. Kubernetes의 requests/limits, Docker의 --memory/--cpus, systemd의 MemoryMax 모두 결국 cgroup v2 인터페이스 파일을 조작하는 것입니다. cgroup v2를 직접 이해하면 컨테이너 리소스 문제의 근본 원인을 파악하고, PSI를 활용한 선제적 모니터링까지 가능합니다. 특히 memory.high의 스로틀 경계와 PSI 압력 모니터링은 OOM kill을 예방하는 핵심 도구입니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux