cgroups v1/v2 (Control Groups)

Linux cgroups(v1/v2)를 리소스 제어와 멀티테넌트 안정성 보장 관점에서 심층 분석합니다. CPU/memory/io/pids/cpuset 컨트롤러의 동작 원리와 계층 정책, cgroup v2 통합 트리 모델과 delegation 규칙, OOM 제어와 memory.high/max 실전 설정, throttling과 latency tradeoff, PSI(Pressure Stall Information) 기반 과부하 탐지, systemd·container runtime 연동 경로, 운영 중 runaway workload 격리와 디버깅 절차까지 생산 환경에서 필요한 리소스 거버넌스 핵심을 다룹니다.

역할 분리 안내: 이 문서는 cgroup 공통 모델과 컨트롤러 전반을 다룹니다. CPU 격리 실전(iso/nohz/rcu_nocbs)은 cpusets & CPU Isolation 문서로 분리해 운영합니다.
전제 조건: 프로세스 스케줄러메모리 관리 기초 문서를 먼저 읽으세요. cgroups는 CPU/메모리/IO 한도를 정책으로 강제하므로, 자원 회계와 스로틀링이 어떻게 연결되는지 먼저 보는 것이 중요합니다.
일상 비유: 이 주제는 부서별 예산 배정과 비슷합니다. 총 예산은 같아도 부서별 한도를 다르게 두듯이, cgroups도 동일한 호스트 자원을 워크로드별로 통제합니다.

핵심 요약

  • cgroup v2 — 단일 통합 계층으로 정책 일관성 강화
  • cpu.max — quota/period 기반 CPU 상한
  • memory.max — 하드 메모리 제한
  • PSI — 자원 압력 기반 관측 지표
  • Delegation — 하위 트리에 안전하게 권한 위임

단계별 이해

  1. 트리 구조 이해
    부모-자식 누적 제한 원리를 먼저 이해합니다.
  2. CPU/Memory 제한 적용
    cpu.max, memory.max를 순서대로 적용합니다.
  3. 관측 지표 확인
    memory.events, cpu.stat, PSI를 점검합니다.
  4. 운영 정책 고도화
    위임 모델과 systemd 연동으로 정책 자동화를 구성합니다.
관련 표준: OCI Runtime Specification 1.0 (cgroups 리소스 제한 요구사항) — 컨테이너 런타임의 리소스 격리 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

빠른 시작: cgroup v2 실습

cgroup v2를 처음 사용하는 독자를 위해, 기본적인 리소스 제한을 단계별로 실습합니다.

# Step 1: cgroup v2가 활성화되어 있는지 확인
mount | grep cgroup2
# cgroup2 on /sys/fs/cgroup type cgroup2 (...)
# ↑ 이 줄이 없으면 v2가 비활성화된 상태

# 활성화 방법: /etc/default/grub에 추가 후 update-grub && reboot
# GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1"

# Step 2: cgroup 생성
sudo mkdir /sys/fs/cgroup/test-app

# Step 3: 컨트롤러 활성화 (부모에서)
# 루트 cgroup에서 사용 가능한 컨트롤러 확인
cat /sys/fs/cgroup/cgroup.controllers
# cpuset cpu io memory hugetlb pids rdma misc

# 자식에게 cpu와 memory 컨트롤러 위임
echo "+cpu +memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control

# Step 4: 리소스 제한 설정
# 메모리 256MB 제한
echo 268435456 | sudo tee /sys/fs/cgroup/test-app/memory.max

# CPU 50% 제한 (100ms period, 50ms quota)
echo "50000 100000" | sudo tee /sys/fs/cgroup/test-app/cpu.max

# Step 5: 현재 셸을 cgroup에 배치
echo $$ | sudo tee /sys/fs/cgroup/test-app/cgroup.procs

# Step 6: 리소스 사용 확인
cat /sys/fs/cgroup/test-app/memory.current
cat /sys/fs/cgroup/test-app/cpu.stat
cat /sys/fs/cgroup/test-app/memory.pressure

# Step 7: 정리 (셸을 먼저 루트로 이동)
echo $$ | sudo tee /sys/fs/cgroup/cgroup.procs
sudo rmdir /sys/fs/cgroup/test-app
Q: 왜 subtree_control에 먼저 컨트롤러를 활성화해야 하나요?

cgroup v2에서는 부모 cgroup의 cgroup.subtree_control에 컨트롤러를 활성화해야만 자식 cgroup에서 해당 컨트롤러의 인터페이스 파일(예: memory.max)이 생성됩니다. 이는 불필요한 파일 생성을 방지하고, 어떤 컨트롤러가 어디서 활성화되는지 명시적으로 관리하기 위한 설계입니다.

💡

systemd-run 활용: 위 과정을 자동화하려면 systemd-run을 사용하세요: systemd-run --scope -p MemoryMax=256M -p CPUQuota=50% -- your-command. systemd가 자동으로 cgroup을 생성하고 제한을 설정합니다.

스트레스 테스트로 cgroup 제한 확인

# stress 도구로 cgroup 제한 효과 확인

# 메모리 제한 테스트: 256MB cgroup에서 512MB 할당 시도
systemd-run --scope -p MemoryMax=256M -- stress --vm 1 --vm-bytes 512M --timeout 10s
# → OOM kill 발생

# CPU 제한 테스트: 50% CPU cgroup에서 CPU 풀로드
systemd-run --scope -p CPUQuota=50% -- stress --cpu 2 --timeout 10s
# → top에서 50% CPU만 사용하는 것을 확인

# PID 제한 테스트: 포크 폭탄 방어
systemd-run --scope -p TasksMax=50 -- bash -c ":(){ :|:& };:"
# → 50개 프로세스에서 멈춤, 시스템 영향 없음

# IO 제한 테스트: 10MB/s 쓰기 제한
DEVICE=$(lsblk -no MAJ:MIN /dev/sda | head -1 | tr -d ' ')
systemd-run --scope -p IOWriteBandwidthMax="/dev/sda 10M" \
  -- dd if=/dev/zero of=/tmp/test bs=1M count=100 oflag=direct
# → 약 10MB/s로 제한됨

프로그래밍 언어별 cgroup v2 API

언어cgroup 라이브러리컨테이너 인식비고
Clibcgroup, 직접 파일 I/O수동커널 API 직접 사용
Gocontainerd/cgroups/v3자동 (1.19+)GOMAXPROCS 자동 조정
Rustcgroups-rs수동타입 안전 cgroup 관리
Pythoncgroupspy수동os.cpu_count() 미인식
JavaJVM 내장자동 (JDK 10+)-XX:+UseContainerSupport
.NET런타임 내장자동 (.NET 6+)GC/ThreadPool 자동 조정
Node.js없음부분적--max-old-space-size 수동 필요
/* Go: containerd cgroups v3 라이브러리 사용 예시 */
// import "github.com/containerd/cgroups/v3/cgroup2"

// cgroup 생성 및 리소스 제한
// res := cgroup2.Resources{
//     Memory: &cgroup2.Memory{
//         Max:  pointerInt64(2 * 1024 * 1024 * 1024), // 2GB
//         High: pointerInt64(1536 * 1024 * 1024),      // 1.5GB
//     },
//     CPU: &cgroup2.CPU{
//         Max:    cgroup2.NewCPUMax(pointerInt64(200000), pointerUint64(100000)),
//         Weight: pointerUint64(200),
//     },
//     Pids: &cgroup2.Pids{
//         Max: 1024,
//     },
// }
// m, _ := cgroup2.NewManager("/sys/fs/cgroup", "/myapp", &res)
// m.AddProc(uint64(os.Getpid()))

cgroups 개요

Control Groups(cgroups)는 프로세스 그룹의 시스템 리소스 사용을 제한(limit), 계량(accounting), 격리(isolation)하는 커널 메커니즘입니다. 컨테이너 런타임, systemd 등에서 리소스 관리의 기반으로 사용됩니다.

Process Group task migrate cgroup v2 cpu/memory/io Kernel Enforcement throttle / reclaim / OOM

리소스 제어 원리

cgroups의 핵심 설계 원리는 "커널의 리소스 할당 경로에 cgroup 검사를 삽입한다"는 것입니다. 프로세스가 리소스(메모리, CPU 시간, I/O 대역폭)를 요청할 때마다 커널은 해당 프로세스가 속한 cgroup의 제한을 확인합니다:

CPU 제어 원리

CPU 컨트롤러는 CFS(Completely Fair Scheduler) 스케줄러에 통합되어 동작합니다. 두 가지 메커니즘이 있습니다:

CPU 제어 메커니즘: Weight vs Quota/Period cpu.weight (상대적 가중치) 0ms 100ms Group A (weight=200) 66.7ms (2/3) Group B (100) 33.3ms (1/3) Idle 특징 ✓ CPU 경쟁 시에만 제한 적용 ✓ CPU 여유 시 제한 없이 사용 가능 ✓ 비례 배분 (상대적 공정성) 사용처: 웹 서버, 배치 작업 부하 변동이 큰 워크로드 cpu.max (quota / period) 0ms period=100ms 200ms Group C 실행 quota=50ms 소진 ⏸ Throttled period 끝까지 대기 period 경계 특징 ✓ 절대적 상한 강제 (hard limit) ✓ CPU 여유 여부와 무관하게 제한 ✓ 예측 가능한 성능 보장 사용처: 실시간 처리, 멀티테넌트 확정적인 리소스 격리 필요 시 두 방식은 독립적이며 동시 사용 가능 (cpu.weight + cpu.max)

메모리 제어 원리

메모리 컨트롤러는 charge/uncharge 메커니즘으로 동작합니다:

  1. 프로세스가 페이지를 할당하면, 해당 페이지의 비용을 프로세스의 cgroup에 charge(부과)합니다.
  2. 페이지가 해제되면 uncharge(환급)합니다.
  3. cgroup의 누적 사용량이 memory.max에 도달하면, 커널은 해당 cgroup 내에서 페이지 회수를 시도합니다.
  4. 회수로도 부족하면 OOM killer가 해당 cgroup 내의 프로세스를 종료합니다.

계층 구조와 리소스 누적 원리

cgroup v2의 계층 구조에서 리소스 제한은 부모가 자식을 포함합니다:

cgroup v2 계층 구조와 리소스 누적 원리 / (root cgroup) memory.max = 4GB 실제 사용: 2.5GB /workloads memory.max = 2GB 실제 사용: 1.8GB (자식 합산) 누적 ↓ /workloads/web memory.max = 1GB 실제 사용: 800MB /workloads/db memory.max = 1.5GB 실제 사용: 1GB /workloads/batch memory.max = 3GB (부모 제한에 막힘) 리소스 누적 원리 ① 자식 사용량은 부모에 누적: web(800MB) + db(1GB) = 1.8GB ≤ parent(2GB) ✓ ⚠️ 부모 제한이 우선 batch는 3GB 설정이지만 부모의 2GB 한도에 막힘
💡

cgroup v2 설계 원칙: v1에서는 컨트롤러별로 독립된 계층이 존재하여 CPU cgroup과 메모리 cgroup의 트리 구조가 달라 정책 불일치가 발생했습니다. v2는 단일 통합 계층으로 모든 컨트롤러가 같은 트리를 공유하여 일관된 리소스 정책을 보장합니다.

cgroup v1 vs v2

cgroup v1과 v2는 설계 철학, 보안 모델, 기능 범위에서 근본적으로 다릅니다. v2는 v1의 설계 결함을 해결하기 위해 완전히 새로 설계되었으며, 커널 4.5에서 정식 릴리스되었습니다.

특성cgroup v1cgroup v2
계층 구조컨트롤러별 독립 계층 (여러 트리)단일 통합 계층 (하나의 트리)
스레드 지원프로세스 단위만스레드 모드 지원 (threaded cgroup)
위임 모델제한적, 보안 취약안전한 위임 (nsdelegate)
압력 통지없음PSI (Pressure Stall Information)
마운트/sys/fs/cgroup/<controller>//sys/fs/cgroup (단일)
내부 프로세스제한 없음No Internal Process Constraint
Buffered I/O 제어불가 (direct I/O만)writeback 포함 전체 I/O
커널 메모리별도 계정 (kmem.limit)통합 계정 (memory.max에 포함)
디바이스 제어화이트/블랙리스트 파일eBPF 프로그램
빈 cgroup 통지release_agent (보안 취약)cgroup.events + inotify
동결(freeze)별도 freezer 컨트롤러통합 cgroup.freeze
전체 종료직접 구현 필요cgroup.kill (커널 5.14+)
메모리 보호없음memory.min / memory.low
메모리 스로틀링soft_limit (비효율적)memory.high (직접 회수 강제)
능동적 회수없음memory.reclaim (커널 5.19+)
CPU 버스트없음cpu.max.burst
UCLAMP없음cpu.uclamp.min / cpu.uclamp.max
SCHED_IDLE없음cpu.idle
IO QoS 모델단순 throttleio.latency + io.cost 모델
마이그레이션 주의: cgroup v1과 v2는 같은 컨트롤러를 동시에 사용할 수 없습니다. 하이브리드 모드에서는 v2에 바인딩되지 않은 컨트롤러만 v1에서 사용할 수 있습니다. 완전한 v2 전환을 위해서는 cgroup_no_v1=all 부트 옵션을 사용하세요.

cgroup v2 채택 현황

배포판/플랫폼cgroup v2 기본 버전비고
Fedora31+ (2019)최초 기본 v2 배포판
Ubuntu21.10+22.04 LTS에서 안정화
Debian11 (bullseye)하이브리드 모드 가능
RHEL/CentOS9+8.x는 수동 활성화
Arch Linux2021+기본 v2
Android12+cgroup v2 필수
Docker20.10+systemd 드라이버 권장
Kubernetes1.25+GA 지원
containerd1.4+v2 지원
Podman3.0+rootless + v2 네이티브

주요 컨트롤러

CPU 컨트롤러

# cgroup v2: CPU 제한 (quota/period)
echo "100000 1000000" > /sys/fs/cgroup/mygroup/cpu.max
# → 1초(1000000us) 중 100ms(100000us)만 사용 = 10% CPU

# CPU weight (상대적 가중치, 1-10000)
echo 150 > /sys/fs/cgroup/mygroup/cpu.weight

Memory 컨트롤러

# 메모리 제한 (256MB)
echo 268435456 > /sys/fs/cgroup/mygroup/memory.max

# 메모리 사용량 확인
cat /sys/fs/cgroup/mygroup/memory.current

# OOM kill 카운트
cat /sys/fs/cgroup/mygroup/memory.events

I/O 컨트롤러

# I/O 대역폭 제한 (장치 8:0에 대해 읽기 10MB/s)
echo "8:0 rbps=10485760" > /sys/fs/cgroup/mygroup/io.max

# I/O weight
echo "default 100" > /sys/fs/cgroup/mygroup/io.weight
v1 blkio와 v2 io의 결정적 차이: v1의 blkio 컨트롤러는 direct I/O만 제한하고 buffered I/O(페이지 캐시 경유)는 추적하지 못했습니다. v2의 io 컨트롤러는 writeback 경로에서 원래 cgroup을 추적하여 buffered I/O도 정확하게 제한합니다. 이것이 cgroup v2 IO 제어가 실질적으로 동작하는 핵심 이유입니다.

PID 컨트롤러

cgroup v2의 PID 컨트롤러는 프로세스/스레드 생성 수를 제한하여 포크 폭탄을 방지합니다:

# PID 제한: 최대 500개 프로세스/스레드
echo 500 > /sys/fs/cgroup/mygroup/pids.max

# 현재 프로세스 수 확인
cat /sys/fs/cgroup/mygroup/pids.current

# 제한 초과 시도 횟수
cat /sys/fs/cgroup/mygroup/pids.events
# max 0  ← fork()가 EAGAIN으로 실패한 횟수

cpuset 컨트롤러 상세

cpuset은 프로세스 그룹을 특정 CPU와 NUMA 메모리 노드에 바인딩합니다. 파티션 모드를 사용하면 전용 CPU를 확보할 수 있습니다:

파일설명예시
cpuset.cpus허용 CPU 목록0-3,8-11
cpuset.cpus.effective실제 유효 CPU (부모 교집합)0-3
cpuset.mems허용 NUMA 노드0-1
cpuset.mems.effective실제 유효 NUMA 노드0
cpuset.cpus.partition파티션 모드member / root / isolated
cpuset.cpus.exclusive전용 CPU 지정4-7
# cpuset 파티션 모드: CPU 전용 할당
# Step 1: 전용 CPU 범위 지정
echo "4-7" > /sys/fs/cgroup/realtime/cpuset.cpus
echo "0" > /sys/fs/cgroup/realtime/cpuset.mems

# Step 2: 파티션 루트로 설정 → 다른 cgroup에서 CPU 4-7 제외
echo "root" > /sys/fs/cgroup/realtime/cpuset.cpus.partition

# Step 3: isolated 모드 (커널 스레드도 제외, 커널 6.2+)
echo "isolated" > /sys/fs/cgroup/realtime/cpuset.cpus.partition
# → CPU 4-7에서 워크큐, 타이머 등 커널 활동 최소화

# NUMA 메모리 통계 확인
cat /sys/fs/cgroup/realtime/memory.numa_stat
# anon N0=134217728 N1=0
# file N0=67108864 N1=0

RDMA 컨트롤러

RDMA(Remote Direct Memory Access) 컨트롤러는 InfiniBand/RoCE 리소스를 cgroup별로 제한합니다:

# RDMA 리소스 제한
echo "mlx5_0 hca_handle=10 hca_object=1000" > /sys/fs/cgroup/hpc/rdma.max

# 현재 사용량
cat /sys/fs/cgroup/hpc/rdma.current
# mlx5_0 hca_handle=3 hca_object=150

memory.events 상세 분석

memory.events는 메모리 컨트롤러의 핵심 관측 포인트입니다. 각 이벤트 카운터의 의미를 정확히 이해해야 효과적인 모니터링이 가능합니다:

카운터트리거 조건의미대응 방법
lowmemory.low 보호 활성화글로벌 회수에서 이 cgroup이 보호됨정상 동작, 보호가 작동 중
highmemory.high 초과직접 회수(direct reclaim) 스로틀링 시작memory.high 상향 검토
maxmemory.max 도달할당 실패 직전, 강제 회수 시도메모리 부족 경고
oomOOM 상황 진입회수 실패, OOM 핸들링 시작즉시 대응 필요
oom_killOOM kill 실행실제 프로세스 종료 발생워크로드 분석, 제한 조정
oom_group_killoom.group=1 kill그룹 전체 종료의도적 설정 확인
# memory.events 모니터링 원라이너
while true; do
    echo "=== $(date) ==="
    cat /sys/fs/cgroup/myapp/memory.events
    echo "current: $(cat /sys/fs/cgroup/myapp/memory.current)"
    echo "pressure: $(cat /sys/fs/cgroup/myapp/memory.pressure | head -1)"
    sleep 5
done

# inotify 기반 이벤트 감지 (즉각 반응)
inotifywait -m -e modify /sys/fs/cgroup/myapp/memory.events

# memory.events.local: 해당 cgroup만의 이벤트 (자식 제외)
# memory.events: 자식 포함 전체 이벤트
cat /sys/fs/cgroup/myapp/memory.events.local

memory.stat 심층 분석

memory.stat은 메모리 사용의 세부 구성을 보여줍니다. 각 필드를 이해하면 메모리 문제의 원인을 정확히 파악할 수 있습니다:

필드설명튜닝 포인트
anon익명 메모리 (malloc, mmap MAP_ANONYMOUS)메모리 누수 의심 시 주요 확인 대상
file파일 캐시 (page cache)회수 가능한 메모리, 높을수록 I/O 성능 좋음
kernel커널 메모리 총합 (slab + page tables + ...)커널 메모리 증가 시 slab 분석
slabslab 할당자 메모리dentry/inode 캐시가 주요 사용처
sockTCP/UDP 소켓 버퍼네트워크 워크로드에서 중요
shmem공유 메모리 + tmpfsIPC 사용 워크로드에서 확인
zswapzswap 압축 메모리스왑 대역폭 절약 효과 확인
zswappedzswap으로 압축 전 원본 크기압축률 = zswapped/zswap
file_mappedmmap으로 매핑된 파일 메모리공유 라이브러리, mmap I/O
file_dirty수정되었지만 디스크 미기록writeback 지연 확인
file_writeback현재 디스크에 쓰기 중I/O 병목 확인
pgfault페이지 폴트 총 횟수성능 지표
pgmajfault메이저 페이지 폴트 (디스크 I/O)높으면 메모리 부족
workingset_refault_anon재참조된 anon 페이지 (thrashing)스래싱 지표
workingset_refault_file재참조된 file 페이지 (thrashing)스래싱 지표
pgscan스캔된 페이지 수회수 압력 지표
pgsteal회수된 페이지 수실제 회수된 양
thp_fault_allocTHP(Transparent Huge Page) 할당 성공THP 활용도
thp_collapse_allocTHP 병합 성공khugepaged 효과
# memory.stat 분석 실전
cat /sys/fs/cgroup/myapp/memory.stat
# anon 536870912          ← 512MB 익명 메모리
# file 268435456          ← 256MB 파일 캐시 (회수 가능)
# kernel 33554432         ← 32MB 커널 메모리
# slab 16777216           ← 16MB slab
# sock 8388608            ← 8MB 소켓 버퍼
# shmem 4194304           ← 4MB 공유 메모리
# pgfault 1000000         ← 100만 페이지 폴트
# pgmajfault 50           ← 50번 메이저 폴트 (낮음=좋음)
# workingset_refault_file 200  ← 파일 스래싱 있음

# 메모리 구성 비율 분석
# total ≈ 512 + 256 + 32 + 8 + 4 = 812MB
# anon/total = 63% → 대부분 프로세스 메모리
# file/total = 31% → 파일 캐시 (회수로 확보 가능)
# kernel/total = 4% → 커널 오버헤드 적정

cpu.stat 심층 분석

필드설명분석 포인트
usage_usec총 CPU 사용 시간 (μs)워크로드 크기 지표
user_usec사용자 모드 CPU 시간애플리케이션 연산 시간
system_usec커널 모드 CPU 시간syscall/인터럽트 시간
nr_periods경과한 period 수대역폭 제한 활성 여부
nr_throttled스로틀된 period 수CPU 부족 지표
throttled_usec스로틀된 총 시간 (μs)성능 영향 크기
nr_bursts버스트 사용 횟수버스트 설정 효과
burst_usec버스트 사용 총 시간버스트 크기 적정성
# CPU 스로틀링 비율 계산
# nr_throttled / nr_periods × 100 = 스로틀링 비율 (%)
# 예: 150/5000 × 100 = 3% → 양호
# 10% 이상이면 cpu.max 조정 필요

# system/user 비율 분석
# system_usec/usage_usec > 30% → 커널 오버헤드 높음
# → syscall 최적화, io_uring 도입 검토

# 스로틀링 실시간 모니터링
while true; do
    awk '/nr_throttled/ {print $2}' /sys/fs/cgroup/myapp/cpu.stat
    sleep 1
done
💡

CPU 스로틀링 해석: nr_throttled가 증가하고 있지만 throttled_usec가 작다면, period 경계에서 잠깐씩 스로틀되는 것이므로 성능 영향은 미미합니다. 반면 throttled_usec가 크게 증가한다면 실질적인 CPU 부족 상태이므로 cpu.max의 quota를 늘리거나 cpu.max.burst를 설정해야 합니다.

커널 cgroup API

#include <linux/cgroup.h>

/* 현재 태스크의 cgroup 접근 */
struct cgroup *cgrp;
rcu_read_lock();
cgrp = task_cgroup(current, memory_cgrp_id);
rcu_read_unlock();

/* cgroup 서브시스템 등록 (커널 컨트롤러 개발 시) */
struct cgroup_subsys my_cgrp_subsys = {
    .css_alloc  = my_css_alloc,
    .css_free   = my_css_free,
    .attach     = my_attach,
    .name       = "my_controller",
};

PSI (Pressure Stall Information)

cgroup v2에서는 PSI를 통해 CPU, 메모리, I/O의 리소스 압력을 실시간 모니터링할 수 있습니다. PSI의 핵심 원리는 "일할 수 있는데 리소스를 기다리느라 못 하는 시간의 비율"을 측정하는 것입니다:

커널은 각 CPU에서 태스크 상태 전환(running → waiting)을 추적하고, 지수 이동 평균(EMA)으로 10초/60초/300초 윈도우의 압력 비율을 계산합니다. 이 값은 0.00(압력 없음)~100.00(완전 정체) 사이입니다.

# CPU 압력 확인
cat /proc/pressure/cpu
# some avg10=0.50 avg60=0.30 avg300=0.20 total=12345678

# cgroup별 메모리 압력
cat /sys/fs/cgroup/mygroup/memory.pressure
ℹ️

systemd는 cgroup v2를 기본으로 사용합니다. systemd-cgtop으로 cgroup별 리소스 사용량을 실시간 모니터링할 수 있습니다.

PSI 압력 수준 해석

PSI 유형수준avg10 범위의미대응
CPU some정상0 ~ 5%CPU 여유 충분별도 조치 불필요
경고5 ~ 25%CPU 경쟁 시작cpu.weight 검토, 워크로드 분산
심각25 ~ 50%의미 있는 CPU 부족cpu.max 확대 또는 코어 추가
위험50%+심각한 CPU 부족즉시 워크로드 조정 필요
Memory some정상0 ~ 10%메모리 여유 충분별도 조치 불필요
경고10 ~ 25%페이지 회수 시작memory.high/max 검토
심각25 ~ 60%빈번한 direct reclaim메모리 증설, 워크로드 축소
위험60%+지속적 회수 부담OOM 임박, 즉시 대응
Memory full경고0 ~ 10%모든 태스크 일시 정체memory.reclaim 트리거
심각10 ~ 30%심각한 성능 저하프로세스 축소 또는 메모리 확대
위험30%+워크로드 사실상 마비긴급 대응 (kill/freeze)
IO some정상0 ~ 10%I/O 원활별도 조치 불필요
경고10 ~ 40%I/O 대기 증가io.weight/io.max 검토
심각40%+I/O 병목스토리지 업그레이드, I/O 스케줄러 튜닝

PSI 기반 자동 스케일링 구현

PSI 트리거를 활용하면 리소스 압력에 따라 자동으로 워크로드를 조절하는 시스템을 구축할 수 있습니다:

#!/bin/bash
# PSI 기반 자동 스케일링 스크립트 (메모리)
# memory.pressure의 some avg10이 25%를 초과하면 워크로드 축소

CGROUP_PATH="/sys/fs/cgroup/myapp"
THRESHOLD=25

while true; do
    # PSI 값 파싱
    avg10=$(awk '/some/ {gsub(/avg10=/,""); print $2}' "$CGROUP_PATH/memory.pressure")

    # 소수점 비교
    if (echo "$avg10 > $THRESHOLD" | bc -l) > /dev/null 2>&1; then
        echo "[WARN] 메모리 압력 $(avg10)% > ${THRESHOLD}%"
        # 대응 1: 능동적 메모리 회수
        echo 104857600 > "$CGROUP_PATH/memory.reclaim"  # 100MB 회수

        # 대응 2: 워커 수 축소 (애플리케이션별 구현)
        # curl -s http://localhost:8080/admin/scale-down

        # 대응 3: 낮은 우선순위 프로세스 중지
        # echo 1 > /sys/fs/cgroup/myapp/batch/cgroup.freeze
    fi
    sleep 5
done
/* PSI 기반 메모리 관리 데몬 (C 구현) */
/* 단계별 PSI 트리거로 점진적 대응 */

/* Level 1: 경고 (some 150ms/1s) → 로깅 + 능동 회수 */
write(psi_fd, "some 150000 1000000", 20);

/* Level 2: 심각 (some 500ms/1s) → 워크로드 축소 */
write(psi_fd2, "some 500000 1000000", 20);

/* Level 3: 위험 (full 200ms/1s) → 프로세스 종료 */
write(psi_fd3, "full 200000 1000000", 20);

/* poll()로 다중 트리거 감시 */
struct pollfd fds[3] = {
    { .fd = psi_fd,  .events = POLLPRI },
    { .fd = psi_fd2, .events = POLLPRI },
    { .fd = psi_fd3, .events = POLLPRI },
};

while (1) {
    int ret = poll(fds, 3, -1);
    if (fds[2].revents & POLLPRI) {
        /* Level 3: 위험 → 즉시 프로세스 종료 */
        handle_critical_pressure();
    } else if (fds[1].revents & POLLPRI) {
        /* Level 2: 심각 → 워크로드 축소 */
        handle_high_pressure();
    } else if (fds[0].revents & POLLPRI) {
        /* Level 1: 경고 → 능동 회수 */
        handle_warning_pressure();
    }
}
💡

Facebook oomd: Facebook의 oomd는 PSI 트리거 기반 userspace OOM 데몬으로, 커널 OOM killer보다 더 지능적인 프로세스 종료 결정을 합니다. 프로세스의 중요도, 메모리 사용 패턴, 최근 성장률 등을 종합적으로 고려하여 종료 대상을 선택합니다. Android의 lmkd도 PSI 기반으로 동작합니다.

cgroup 트러블슈팅 가이드

프로덕션 환경에서 자주 발생하는 cgroup 관련 문제와 해결 방법을 체계적으로 정리합니다.

문제 1: 예상치 못한 CPU 스로틀링

# 증상: 애플리케이션 응답 시간 증가, cpu.stat에 throttled 증가

# 1단계: 스로틀링 현황 확인
cat /sys/fs/cgroup/myapp/cpu.stat
# nr_periods 10000
# nr_throttled 500      ← 5% 스로틀링
# throttled_usec 25000000 ← 25초 총 스로틀 시간

# 2단계: 현재 설정 확인
cat /sys/fs/cgroup/myapp/cpu.max
# 100000 100000         ← 100ms period, 100ms quota (1 CPU)

# 3단계: 해결 방법
# 방법 A: quota 증가
echo "200000 100000" > /sys/fs/cgroup/myapp/cpu.max  # 2 CPU

# 방법 B: burst 설정 (순간 스파이크 허용)
echo 50000 > /sys/fs/cgroup/myapp/cpu.max.burst  # 50ms burst

# 방법 C: weight만 사용 (하드 제한 제거)
echo "max 100000" > /sys/fs/cgroup/myapp/cpu.max  # 무제한
echo 200 > /sys/fs/cgroup/myapp/cpu.weight  # 상대적 가중치

문제 2: 반복적 OOM Kill

# 증상: 프로세스가 주기적으로 종료됨

# 1단계: OOM 이벤트 확인
cat /sys/fs/cgroup/myapp/memory.events
# oom_kill 5    ← OOM kill 5회
# high 1000    ← memory.high 초과 1000회

# 2단계: 메모리 사용 분석
cat /sys/fs/cgroup/myapp/memory.current
# 1073741824   ← 1GB 사용 중
cat /sys/fs/cgroup/myapp/memory.max
# 1073741824   ← 1GB 제한 → 꽉 참

# 3단계: 무엇이 메모리를 사용하는지 분석
cat /sys/fs/cgroup/myapp/memory.stat | grep -E "^(anon|file|kernel|slab|sock)"
# anon 805306368    ← 768MB (75%) ← 주요 사용처!
# file 214748364   ← 205MB (20%) ← 파일 캐시 (회수 가능)
# kernel 53687091  ← 51MB (5%)

# 4단계: 해결 방법
# 방법 A: 제한 확대
echo 2G > /sys/fs/cgroup/myapp/memory.max

# 방법 B: 스로틀링으로 점진적 관리 (OOM 방지)
echo 1536M > /sys/fs/cgroup/myapp/memory.high  # max 전에 스로틀

# 방법 C: 스왑 허용
echo 1G > /sys/fs/cgroup/myapp/memory.swap.max

# 방법 D: 능동적 회수 스케줄링
echo 209715200 > /sys/fs/cgroup/myapp/memory.reclaim  # 200MB 회수

문제 3: I/O 지연 증가

# 증상: 파일 읽기/쓰기가 느려짐

# 1단계: IO 압력 확인
cat /sys/fs/cgroup/myapp/io.pressure
# some avg10=45.00 avg60=30.00 avg300=20.00
# full avg10=15.00 avg60=10.00 avg300=5.00
# → some 45%: 심각한 I/O 대기

# 2단계: I/O 사용량 확인
cat /sys/fs/cgroup/myapp/io.stat
# 259:0 rbytes=10737418240 wbytes=5368709120 rios=1000000 wios=500000

# 3단계: I/O 제한 확인
cat /sys/fs/cgroup/myapp/io.max
# 259:0 rbps=104857600 wbps=52428800   ← 100MB/s 읽기, 50MB/s 쓰기

# 4단계: 해결 방법
# 방법 A: 제한 확대
echo "259:0 rbps=524288000 wbps=209715200" > /sys/fs/cgroup/myapp/io.max

# 방법 B: 가중치 기반으로 전환 (유연한 배분)
echo "259:0 rbps=max wbps=max" > /sys/fs/cgroup/myapp/io.max
echo "default 500" > /sys/fs/cgroup/myapp/io.weight

# 방법 C: 지연 목표 설정
echo "259:0 target=5" > /sys/fs/cgroup/myapp/io.latency  # 5ms 목표

문제 4: 컨테이너 내부에서 리소스 정보 불일치

# 증상: 컨테이너 내부 free/top이 호스트 전체 리소스를 표시

# 원인: /proc/meminfo, /proc/stat은 cgroup을 인식하지 않음

# 해결 방법 1: 애플리케이션에서 cgroup 파일 직접 읽기
# 메모리 사용량
cat /sys/fs/cgroup/memory.current
# 메모리 제한
cat /sys/fs/cgroup/memory.max
# CPU 제한
cat /sys/fs/cgroup/cpu.max

# 해결 방법 2: LXCFS 사용 (컨테이너용 /proc 가상화)
# LXCFS는 /proc/meminfo, /proc/cpuinfo 등을
# cgroup 제한에 맞게 가상화하여 제공
# docker run -v /var/lib/lxcfs/proc/meminfo:/proc/meminfo:rw ...

# 해결 방법 3: 프로그래밍 언어별 cgroup 인식
# Java: -XX:+UseContainerSupport (JDK 10+, 기본 활성화)
# Go: runtime.GOMAXPROCS()는 cpu.max 자동 인식 (Go 1.19+)
# Node.js: --max-old-space-size는 수동 설정 필요
# Python: os.cpu_count()는 cgroup 미인식 (수동 계산 필요)
JVM과 cgroup v2: Java 17+는 cgroup v2를 완전히 인식합니다. -XX:+UseContainerSupport (기본 활성화)를 사용하면 memory.max를 기반으로 힙 크기를 자동 결정하고, cpu.max를 기반으로 GC 스레드 수를 조절합니다. -XX:MaxRAMPercentage=75.0으로 cgroup 메모리 제한의 75%를 힙으로 사용하는 것이 일반적입니다.

문제 5: cgroup 생성/삭제 실패

# 증상: mkdir이나 rmdir 실패

# 원인 1: 계층 깊이/자손 수 초과
cat /sys/fs/cgroup/cgroup.max.depth
cat /sys/fs/cgroup/cgroup.max.descendants

# 원인 2: No Internal Process Constraint 위반
# subtree_control에 컨트롤러를 활성화하려는데 프로세스가 있는 경우
# EBUSY 에러 발생 → 프로세스를 자식 cgroup으로 이동

# 원인 3: 삭제 시 프로세스가 남아있음
cat /sys/fs/cgroup/myapp/cgroup.procs
# 프로세스가 있으면 삭제 불가 → 먼저 프로세스 이동 또는 종료

# 원인 4: dying cgroup (삭제 후 해제 안 됨)
cat /sys/fs/cgroup/cgroup.stat
# nr_dying_descendants가 높으면 page cache 드롭 필요
echo 3 > /proc/sys/vm/drop_caches

Freezer 컨트롤러

Freezer 컨트롤러는 cgroup 내 모든 프로세스를 일시 중지(freeze)하고 재개(thaw)할 수 있습니다:

# cgroup v2 freezer
echo 1 > /sys/fs/cgroup/myapp/cgroup.freeze
# myapp 그룹의 모든 프로세스가 TASK_FROZEN 상태

echo 0 > /sys/fs/cgroup/myapp/cgroup.freeze
# 프로세스 재개

# 상태 확인
cat /sys/fs/cgroup/myapp/cgroup.events
# frozen 1

cpuset 컨트롤러

CPU와 메모리 노드를 프로세스 그룹에 할당합니다:

# cgroup v2 cpuset
echo "0-3" > /sys/fs/cgroup/myapp/cpuset.cpus      # CPU 0~3 할당
echo "0" > /sys/fs/cgroup/myapp/cpuset.mems         # NUMA 노드 0
echo "root" > /sys/fs/cgroup/myapp/cpuset.cpus.partition  # 전용 파티션

cgroup 위임 (Delegation)

# 비특권 사용자에게 cgroup 관리 위임
mkdir /sys/fs/cgroup/user_slice/user-1000
chown -R 1000:1000 /sys/fs/cgroup/user_slice/user-1000

# systemd에서 위임 설정
# /etc/systemd/system/myservice.service
# [Service]
# Delegate=cpu memory io

cgroup 커널 내부 구조

/* 커널 내부: cgroup 서브시스템 등록 */
struct cgroup_subsys memory_cgrp_subsys = {
    .css_alloc   = mem_cgroup_css_alloc,
    .css_online  = mem_cgroup_css_online,
    .css_offline = mem_cgroup_css_offline,
    .css_free    = mem_cgroup_css_free,
    .can_attach  = mem_cgroup_can_attach,
    .attach      = mem_cgroup_move_task,
    .name        = "memory",
};

/* task_struct에서 cgroup 접근 */
struct css_set *cset = task->cgroups;
/* css_set은 프로세스가 속한 모든 cgroup subsystem state를 참조 */

PSI 상세 분석

PSI(Pressure Stall Information)는 시스템 자원 압박 수준을 정량적으로 측정합니다:

# 시스템 전체 PSI 확인
cat /proc/pressure/cpu
# some avg10=0.25 avg60=0.10 avg300=0.05 total=123456
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0

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

# PSI 트리거 (임계값 기반 알림)
# /proc/pressure/memory에 트리거 설정:
# "some 150000 1000000" → 1초 window에서 150ms 이상 some 압력 시 이벤트
/* PSI 트리거를 커널에서 사용 */
int fd = open("/proc/pressure/memory", O_RDWR | O_NONBLOCK);
struct pollfd fds = { .fd = fd, .events = POLLPRI };

/* 트리거 설정: 1초 window에서 150ms 이상 stall */
write(fd, "some 150000 1000000", 20);

/* 이벤트 대기 */
poll(&fds, 1, -1);  /* 압력 임계값 초과 시 반환 */

커널 버전별 변경사항

버전변경
2.6.24cgroup v1 도입
3.16cgroup v2 통합 계층 초기 구현
4.5cgroup v2 정식 릴리스
4.20PSI (Pressure Stall Information) 도입
5.0cgroup v2 cpuset 컨트롤러
5.2cgroup v2 freezer 통합
5.7cgroup v1의 cpuacct를 cpu에 통합 (v2)
6.1PSI per-cgroup 트리거 개선

cgroup 관련 주요 취약점

cgroup은 컨테이너 리소스 격리의 핵심 메커니즘이지만, 특히 v1의 설계에서 보안 취약점이 반복적으로 발견되었습니다. cgroup v2로의 전환이 이러한 문제의 근본적 해결을 위해 가속화되고 있습니다.

CVE-2022-0492 — cgroup v1 release_agent 컨테이너 탈출 (CVSS 7.0):

cgroup v1의 release_agent 파일에 호스트 경로의 스크립트를 지정하면, cgroup 내 마지막 프로세스가 종료될 때 해당 스크립트가 호스트의 root 권한으로 실행됩니다. CAP_SYS_ADMIN + cgroup 마운트 권한이 있는 컨테이너에서 트리거 가능합니다.

/* cgroup v1 release_agent 탈출 vs v2의 개선 */

/* cgroup v1: release_agent는 호스트의 init cgroup ns에서 실행 */
/* → 컨테이너 내부에서 호스트 명령 실행 가능 */
echo /host/path/exploit.sh > /sys/fs/cgroup/rdma/release_agent
echo 1 > /sys/fs/cgroup/rdma/child/notify_on_release

/* cgroup v2: release_agent 메커니즘 제거 */
/* 대신 cgroup.events 파일의 "populated 0" 이벤트를 */
/* 유저스페이스 데몬(systemd 등)이 처리 */
/* → 커널이 직접 사용자 프로그램을 실행하지 않음 */

/* 커널 5.17+ 수정: cgroup namespace 내에서 release_agent 쓰기 차단 */
/* cgroup_release_agent_write()에 ns_capable(init_ns, CAP_SYS_ADMIN) 검사 추가 */
CVE-2021-4154 — cgroup v1 파일 디스크립터 UAF (CVSS 8.8):

cgroup v1의 파일 디스크립터 처리에서 cgroup_get_from_fd()가 cgroup에 대한 참조를 적절히 관리하지 않아, 해제된 cgroup 구조체에 접근하는 Use-After-Free가 발생합니다. 이를 통해 로컬 권한 상승이 가능합니다.

cgroup 리소스 제한 우회 패턴:

Memory 제한 우회: cgroup v1의 memory.limit_in_bytes는 커널 메모리(slab, page tables)를 별도로 계산하지 않아, kmem.limit_in_bytes 미설정 시 커널 메모리를 무제한 사용 가능. cgroup v2에서는 memory.max가 커널/사용자 메모리를 통합 관리
CPU 제한 우회: cpu.cfs_quota_us/cpu.cfs_period_us에서 짧은 period와 높은 quota 조합 시 버스트(burst)가 발생하여 순간적으로 제한 초과 가능. cgroup v2의 cpu.max.burst로 버스트 크기를 명시적으로 제어
I/O 제한 우회: cgroup v1의 blkio 컨트롤러는 direct I/O만 제한하고 buffered I/O(페이지 캐시 경유)는 제한하지 않음. cgroup v2의 io.max는 writeback 포함 전체 I/O를 제한

/* cgroup v1 vs v2 보안 비교 */

/*
 * cgroup v1 보안 문제:
 * 1. release_agent: 커널이 사용자 프로그램 직접 실행 → 탈출 벡터
 * 2. 다중 계층(hierarchy): 컨트롤러별 독립 트리 → 정책 불일치 가능
 * 3. 위임 모델 미비: 하위 cgroup에 대한 세밀한 권한 제어 부재
 * 4. kmem accounting 분리: 커널 메모리 제한 우회 가능
 *
 * cgroup v2 보안 개선:
 * 1. release_agent 제거: 유저스페이스 데몬으로 대체
 * 2. 단일 통합 계층: 모든 컨트롤러가 하나의 트리 → 일관된 정책
 * 3. 안전한 위임: subtree_control로 하위 cgroup 권한 제어
 * 4. 통합 메모리 accounting: 커널 + 사용자 메모리 통합 제한
 * 5. nsdelegate 마운트 옵션: cgroup NS 경계에서 위임 강제
 */

# cgroup v2 보안 권장 설정
# /etc/default/grub에 추가
GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1 cgroup_no_v1=all"

# nsdelegate로 안전한 위임 활성화
mount -t cgroup2 none /sys/fs/cgroup -o nsdelegate,memory_recursiveprot

Android cgroup 구성

Android는 cgroup을 활용하여 foreground/background 앱의 CPU, 메모리, I/O 자원을 정교하게 관리한다. Android 12부터 cgroup v2가 필수이며, task_profiles.json이 cgroup 설정을 추상화한다.

컨트롤러마운트 경로Android 용도
cpuctl/dev/cpuctl/foreground/background CPU 가중치 (cpu.weight)
cpuset/dev/cpuset/빅/리틀 코어 핀닝 (foreground→전체, background→리틀)
memory/dev/memcg/앱별 메모리 제한, lmkd(PSI 기반) 연동
freezer/sys/fs/cgroup/cgroup v2 freezer로 백그라운드 앱 동결 (배터리 절약)
# Android cgroup 계층 구조
/dev/cpuset/foreground     → cpus: 0-7   # 빅+리틀 전체 코어
/dev/cpuset/background     → cpus: 0-3   # 리틀 코어만
/dev/cpuctl/foreground     → cpu.weight: 1024
/dev/cpuctl/background     → cpu.weight: 50

# UCLAMP: 스케줄러에 utilization 힌트 (EAS 연동)
/dev/cpuctl/foreground/cpu.uclamp.min: 20
/dev/cpuctl/foreground/cpu.uclamp.max: 1024

Android init의 task_profiles 지시어, UCLAMP 스케줄링, lmkd PSI 연동 등 심화 내용은 Android 커널 — cgroup 구성을 참고하라.

cgroup v2 통합 계층 상세

cgroup v2의 가장 중요한 설계 결정은 단일 통합 계층(Unified Hierarchy)입니다. v1에서는 CPU, 메모리, I/O 등 컨트롤러별로 독립된 계층 트리를 마운트할 수 있어, 같은 프로세스가 CPU cgroup에서는 A 그룹, 메모리 cgroup에서는 B 그룹에 속하는 불일치가 발생했습니다. v2는 이를 하나의 트리로 통합하여 정책 일관성을 보장합니다.

cgroup v1 다중 계층 vs v2 통합 계층 cgroup v1: 컨트롤러별 독립 계층 CPU 계층 group-A group-B Memory 계층 group-X group-Y ⚠ 정책 불일치 문제 PID 1234: CPU→group-A, Memory→group-Y → 리소스 정책 간 상관관계 추적 불가 v1의 추가 문제점 • release_agent: 커널이 임의 프로그램 실행 → 보안 취약점 • 위임 모델 부재: 비특권 사용자 관리 어려움 • 내부 프로세스 제한: leaf 노드에만 프로세스 배치 불가 • 통지 없음: PSI 등 압력 관측 수단 부재 cgroup v2: 단일 통합 계층 / (root) /workloads cpu+memory+io /system cpu+memory+io /web /db ✓ v2 개선 사항 • 모든 컨트롤러가 하나의 트리 공유 → 정책 일관성 • 안전한 위임(delegation) 모델 내장 • PSI 기반 리소스 압력 관측 • No Internal Process Constraint 강제 • release_agent 제거 → 보안 강화 마운트 방법 mount -t cgroup2 none /sys/fs/cgroup 부트 옵션: systemd.unified_cgroup_hierarchy=1 v1 비활성화: cgroup_no_v1=all

No Internal Process Constraint

cgroup v2의 중요한 구조적 규칙은 내부 프로세스 제약(No Internal Process Constraint)입니다. 자식 cgroup이 존재하는 cgroup에는 프로세스를 직접 배치할 수 없습니다. 이 규칙은 리소스 분배의 모호성을 제거합니다:

# No Internal Process Constraint 예시
# /sys/fs/cgroup/workloads/ 에 자식이 있으면
# workloads/cgroup.procs에 직접 PID를 쓸 수 없음

# 올바른 구조:
/sys/fs/cgroup/workloads/
├── web/           # 프로세스는 여기에 (leaf)
│   └── cgroup.procs
├── db/            # 프로세스는 여기에 (leaf)
│   └── cgroup.procs
├── cgroup.subtree_control   # 자식 컨트롤러 활성화
└── cgroup.procs             # ← 비어있어야 함 (자식이 있으므로)

# 컨트롤러 활성화: 부모에서 subtree_control에 작성
echo "+cpu +memory +io" > /sys/fs/cgroup/workloads/cgroup.subtree_control
주의: cgroup.subtree_control에 컨트롤러를 활성화하면, 해당 cgroup에 이미 있는 프로세스는 자식 cgroup으로 이동해야 합니다. 그렇지 않으면 EBUSY 에러가 발생합니다. 이는 리소스 분배의 모호성을 방지하기 위한 의도적 설계입니다.

주요 인터페이스 파일

파일유형설명
cgroup.typeR/Wcgroup 유형: domain, domain threaded, domain invalid, threaded
cgroup.procsR/W소속 프로세스 PID 목록 / PID 쓰기로 프로세스 이동
cgroup.threadsR/W소속 스레드 TID 목록 (threaded cgroup에서 사용)
cgroup.controllersR이 cgroup에서 사용 가능한 컨트롤러 목록
cgroup.subtree_controlR/W자식 cgroup에서 활성화할 컨트롤러 (+cpu -memory 형식)
cgroup.eventsRpopulated/frozen 상태 모니터링 (inotify/poll 가능)
cgroup.max.descendantsR/W허용되는 최대 자손 cgroup 수
cgroup.max.depthR/W허용되는 최대 계층 깊이
cgroup.statRnr_descendants, nr_dying_descendants 통계
cgroup.freezeR/W1 쓰기로 모든 자손 프로세스 동결
cgroup.killW1 쓰기로 모든 자손 프로세스에 SIGKILL
cgroup.pressureR/WPSI 모니터링 활성화/비활성화 (0/1)

Memory 컨트롤러 심화

cgroup v2 메모리 컨트롤러는 사용자 메모리(anon, file cache, tmpfs), 커널 메모리(slab, page tables, sock buffers), 스왑을 통합 관리합니다. v1과 달리 커널 메모리가 별도가 아닌 통합 계정으로 처리되어 제한 우회가 불가능합니다.

Memory 컨트롤러 제한 계층 (memory.*) memory.max (하드 제한) 초과 시 cgroup 내부 OOM killer 호출 memory.events의 oom/oom_kill 카운터 증가 memory.high (스로틀링 임계값) 초과 시 메모리 할당 경로에서 직접 회수(direct reclaim) 강제 프로세스가 잠시 멈추며 자체 메모리를 회수 → 점진적 감속 OOM은 발생하지 않음 (best-effort 제한) memory.low (소프트 보호) 글로벌 메모리 압력 시 이 값 이하 메모리는 회수 대상에서 제외 best-effort: 다른 모든 메모리가 소진되면 보호 해제됨 memory.min (하드 보호) 절대적 보호: 어떤 상황에서도 이 값 이하는 회수하지 않음 OOM이 발생하더라도 보호됨 (단, 자체 cgroup OOM은 예외) OOM 구간 스로틀링 구간 보호 구간 절대 보호 memory.swap.max / memory.swap.high 스왑 사용량 제한 (memory.max와 별도) memory.swap.max=0 → 스왑 사용 금지 memory.zswap.max → zswap 풀 크기 제한 (커널 6.5+)

Memory 인터페이스 파일 상세

파일유형설명기본값
memory.currentR현재 메모리 사용량 (바이트)-
memory.minR/W하드 메모리 보호 하한0
memory.lowR/W소프트 메모리 보호 하한 (best-effort)0
memory.highR/W메모리 스로틀링 임계값max
memory.maxR/W메모리 하드 제한 (초과 시 OOM)max
memory.reclaimW능동적 메모리 회수 트리거 (바이트 지정)-
memory.peakR최고 메모리 사용량 기록-
memory.oom.groupR/WOOM 시 그룹 전체 종료 여부 (0/1)0
memory.swap.currentR현재 스왑 사용량-
memory.swap.maxR/W스왑 하드 제한max
memory.swap.highR/W스왑 스로틀링 임계값max
memory.zswap.currentRzswap 사용량-
memory.zswap.maxR/Wzswap 제한max
memory.pressureRPSI 메모리 압력 (some/full)-
memory.numa_statRNUMA 노드별 메모리 통계-
memory.statR상세 메모리 통계 (anon, file, slab 등)-
memory.eventsR이벤트 카운터 (low, high, max, oom, oom_kill, oom_group_kill)-
memory.events.localR해당 cgroup만의 이벤트 (자식 미포함)-

능동적 메모리 회수 (memory.reclaim)

커널 5.19에서 도입된 memory.reclaim은 OOM을 기다리지 않고 능동적으로 메모리를 회수하는 인터페이스입니다. 프로액티브 메모리 관리에 핵심적인 역할을 합니다:

# 500MB 회수 시도
echo 524288000 > /sys/fs/cgroup/myapp/memory.reclaim

# swappiness 조정과 함께 회수 (커널 6.1+)
echo "524288000 swappiness=0" > /sys/fs/cgroup/myapp/memory.reclaim
# swappiness=0: anon 페이지를 swap하지 않고 file 캐시만 회수

# memory.stat으로 상세 사용량 확인
cat /sys/fs/cgroup/myapp/memory.stat
# anon 134217728        ← 익명 메모리 (malloc 등)
# file 67108864         ← 파일 캐시
# kernel 16777216       ← 커널 메모리 (slab 등)
# shmem 8388608         ← 공유 메모리 / tmpfs
# sock 4194304          ← TCP/UDP 소켓 버퍼
# slab 12582912         ← slab 할당자
# pgfault 50000         ← 페이지 폴트 횟수
# pgmajfault 100        ← 메이저 페이지 폴트
memory.reclaim 활용 사례:
  • 사용자 공간 OOM 핸들러: PSI 트리거로 압력 감지 → memory.reclaim으로 선제 회수
  • VM 벌룬 드라이버: 호스트에서 게스트 cgroup의 메모리를 능동 회수
  • 컨테이너 런타임: 스케줄링 결정 전에 메모리 여유 확보

메모리 과금 메커니즘 (Charge Accounting)

메모리 컨트롤러의 핵심은 charge/uncharge 과금 메커니즘입니다. 모든 메모리 할당 경로에서 cgroup 검사가 수행됩니다:

/* mm/memcontrol.c — 메모리 과금 핵심 경로 */

/* 1. 페이지 할당 시 charge */
int charge_memcg(struct folio *folio, struct mem_cgroup *memcg, gfp_t gfp) {
    unsigned long nr_pages = folio_nr_pages(folio);

    /* memory.max 검사 */
    if (!try_charge(memcg, gfp, nr_pages)) {
        /* 제한 초과 → direct reclaim 시도 */
        if (mem_cgroup_oom(memcg, gfp, get_order(nr_pages)))
            return -ENOMEM;  /* OOM 발생 */
    }

    /* memory.high 검사 — 스로틀링 */
    if (page_counter_read(&memcg->memory) > READ_ONCE(memcg->memory.high))
        schedule_work(&memcg->high_work);  /* 비동기 회수 */

    /* folio에 cgroup 연결 */
    folio->memcg_data = (unsigned long)memcg;
    return 0;
}

/* 2. 페이지 해제 시 uncharge */
void uncharge_folio(struct folio *folio) {
    struct mem_cgroup *memcg = folio_memcg(folio);
    if (memcg) {
        page_counter_uncharge(&memcg->memory, folio_nr_pages(folio));
        folio->memcg_data = 0;
    }
}

/* 3. page_counter 구조체 — 계층적 카운터 */
struct page_counter {
    atomic_long_t       usage;     /* 현재 사용량 */
    unsigned long       min;       /* memory.min */
    unsigned long       low;       /* memory.low */
    unsigned long       high;      /* memory.high */
    unsigned long       max;       /* memory.max */
    struct page_counter *parent;   /* 부모 카운터 (계층 누적) */
};
Q: charge/uncharge가 어떻게 계층적으로 동작하나요?
  • L4-12charge_memcg()는 folio(페이지 묶음)를 cgroup에 과금합니다. try_charge()는 해당 memcg뿐만 아니라 모든 조상 cgroup의 카운터도 함께 증가시킵니다 (page_counter의 parent 체인 순회).
  • L14-15memory.high 초과 시 OOM 대신 비동기 회수 작업을 스케줄합니다. 프로세스는 schedule_work()를 통해 점진적으로 감속됩니다.
  • L18folio->memcg_data에 cgroup 포인터를 저장하여 나중에 uncharge 시 어떤 cgroup에서 차감할지 추적합니다.
  • L22-27페이지 해제 시 uncharge_folio()가 카운터를 감소시킵니다. 이 역시 부모 체인을 따라 올라가며 모든 조상의 사용량을 함께 감소시킵니다.
cgroup 메모리 회수 파이프라인 메모리 할당 요청 alloc_pages(gfp) usage > high? (스로틀링 검사) direct reclaim yes usage > max? (하드 제한 검사) no cgroup OOM yes oom_score 기반 프로세스 선택 및 kill 할당 성공 charge 완료 no 글로벌 메모리 압력 시 보호 메커니즘 memory.min (하드 보호) 절대 회수하지 않음 Usage ≤ min → skip effective_min 계산 시 부모 비율 적용 memory.low (소프트 보호) 회수 우선순위 낮춤 Usage ≤ low → deprioritize 다른 cgroup 메모리 우선 회수 memory.reclaim (능동) 사용자 공간에서 직접 트리거 바이트 단위 지정 가능 swappiness 매개변수 지원 (6.1+)

CPU 컨트롤러 심화

cgroup v2 CPU 컨트롤러는 CFS 스케줄러와 긴밀히 통합되어 가중치 기반 비례 배분대역폭 제한을 동시에 제공합니다. 여기서는 내부 메커니즘을 깊이 분석합니다.

대역폭 스로틀링 내부 구조

CPU 대역폭 스로틀링(cpu.max)은 커널 내부에서 CFS bandwidth control로 구현됩니다. 핵심 구조체는 struct cfs_bandwidth이며, period 타이머와 quota 관리를 담당합니다:

CFS 대역폭 스로틀링 타임라인 0ms period (100ms) 200ms 300ms 실행 (50ms) ⏸ Throttled (50ms) 실행 (70ms, burst 사용) ⏸ (30ms) cpu.max.burst=20ms quota 리필 struct cfs_bandwidth • quota: 50ms (period 당 허용 CPU 시간) • period: 100ms (quota 리필 주기) • burst: 20ms (미사용 quota 누적 한도) • runtime: 남은 quota (실시간 감소) • nr_throttled: 스로틀된 엔티티 수 • period_timer: hrtimer (period 경계 관리) Per-CPU 런타임 풀 글로벌 quota를 CPU별로 분배: CPU 0: 12ms CPU 1: 12ms CPU 2: 12ms ... • CPU별 slice (5ms) 단위로 할당/반환 • 소진 시 글로벌 풀에서 재할당 요청 • 글로벌 풀 소진 → 스로틀링
/* kernel/sched/fair.c — CFS 대역폭 제어 핵심 */

struct cfs_bandwidth {
    raw_spinlock_t  lock;
    ktime_t         period;        /* 100ms 기본 */
    u64             quota;         /* period 당 허용 ns */
    u64             runtime;       /* 남은 런타임 */
    u64             burst;         /* 미사용 quota 누적 한도 */
    s64             hierarchical_quota;
    u8              idle;
    u8              period_active;
    struct hrtimer  period_timer;  /* period 경계 타이머 */
    struct hrtimer  slack_timer;   /* 미사용 런타임 반환 */
    struct list_head throttled_cfs_rq;
    int             nr_periods;
    int             nr_throttled;
    u64             throttled_time;
};

/* 스로틀링 결정: runtime이 0 이하이면 스로틀 */
static bool cfs_rq_throttled(struct cfs_rq *cfs_rq) {
    return cfs_rq->throttled;
}

/* period 타이머 콜백: quota 리필 */
static enum hrtimer_restart sched_cfs_period_timer(...) {
    /* runtime을 quota로 리필 (burst 누적 고려) */
    cfs_b->runtime = min(cfs_b->quota + cfs_b->runtime,
                         cfs_b->quota + cfs_b->burst);
    /* 스로틀된 cfs_rq들 해제 */
    distribute_cfs_runtime(cfs_b);
    return HRTIMER_RESTART;
}

UCLAMP (Utilization Clamping)

UCLAMP은 스케줄러에게 CPU 활용도의 상한/하한 힌트를 제공하여 EAS(Energy Aware Scheduling)와 연동합니다:

파일범위설명
cpu.uclamp.min0.00 ~ 100.00최소 활용도 보장 → 주파수 하한 결정 (부스트 효과)
cpu.uclamp.max0.00 ~ 100.00최대 활용도 제한 → 주파수 상한 결정 (전력 절약)
cpu.idle0 / 11 설정 시 SCHED_IDLE 정책 적용 (극히 낮은 우선순위)
# 웹 서버: CPU 성능 보장 (최소 30% 활용도)
echo "30.00" > /sys/fs/cgroup/webserver/cpu.uclamp.min
echo "100.00" > /sys/fs/cgroup/webserver/cpu.uclamp.max

# 배치 작업: 전력 절약 (최대 60% 활용도)
echo "0.00" > /sys/fs/cgroup/batch/cpu.uclamp.min
echo "60.00" > /sys/fs/cgroup/batch/cpu.uclamp.max

# 극저 우선순위 배경 작업
echo 1 > /sys/fs/cgroup/background/cpu.idle

# CPU 통계 확인
cat /sys/fs/cgroup/webserver/cpu.stat
# usage_usec 1234567        ← 총 CPU 사용 시간
# user_usec 1000000         ← 사용자 모드 시간
# system_usec 234567        ← 커널 모드 시간
# nr_periods 5000           ← 경과 period 수
# nr_throttled 150          ← 스로틀된 횟수
# throttled_usec 7500000    ← 스로틀된 총 시간
# nr_bursts 30              ← 버스트 사용 횟수
# burst_usec 600000         ← 버스트 사용 총 시간
💡

UCLAMP과 cpufreq 연동: cpu.uclamp.min=30.00을 설정하면 schedutil 거버너가 해당 cgroup의 태스크가 실행될 때 최소 30%에 해당하는 CPU 주파수를 보장합니다. ARM big.LITTLE이나 Intel Hybrid 아키텍처에서 특히 유용합니다. 자세한 내용은 CPU 주파수 스케일링 문서를 참고하세요.

IO 컨트롤러 심화

cgroup v2 IO 컨트롤러는 세 가지 제어 메커니즘을 제공합니다: 가중치 기반 비례 배분, 절대적 BPS/IOPS 제한, 지연 시간 목표 제어. v1의 blkio와 달리 buffered I/O(writeback)까지 포함하여 제한합니다.

cgroup v2 IO 컨트롤러 다이어그램 Application I/O write() / read() / io_uring Page Cache buffered → writeback Direct I/O O_DIRECT bypass cache cgroup v2 IO Controller io.weight | io.max | io.latency | io.cost writeback 포함 전체 I/O 계량 (v1과의 핵심 차이) io.weight (비례 배분) default 100 [1-10000] io.cost 모델 기반 가중치 디바이스별 가중치 설정 가능 "8:0 200" (특정 디바이스) io.max (절대 제한) rbps, wbps, riops, wiops 하드 제한 (초과 시 I/O 큐 대기) 디바이스별 설정 "8:0 rbps=10M wbps=5M" io.latency (지연 목표) target=Nms 형식 큐 깊이 조절로 지연 제어 부모-자식 계층적 적용 "8:0 target=10" Block Layer (blk-cgroup, blk-throttle, blk-iocost) Block Device (NVMe/SSD/HDD)

io.latency 지연 목표 모델

io.latency는 Facebook이 개발한 I/O 지연 보장 메커니즘으로, 목표 지연을 초과하면 해당 cgroup의 I/O 큐 깊이(queue depth)를 동적으로 줄여 다른 cgroup에 I/O 대역폭을 양보합니다:

# 디바이스 8:0에 10ms 지연 목표 설정
echo "8:0 target=10" > /sys/fs/cgroup/latency-sensitive/io.latency

# 배치 작업에는 느슨한 지연 목표
echo "8:0 target=100" > /sys/fs/cgroup/batch-io/io.latency

# I/O 통계 확인
cat /sys/fs/cgroup/latency-sensitive/io.stat
# 8:0 rbytes=1234567 wbytes=7654321 rios=100 wios=200 dbytes=0 dios=0

io.cost QoS 모델

io.cost는 장치의 실제 성능 특성(순차/랜덤, 읽기/쓰기)을 모델링하여 정밀한 가중치 기반 I/O 분배를 제공합니다:

# io.cost 모델 파라미터 확인
cat /sys/fs/cgroup/io.cost.model
# 8:0 ctrl=auto model=linear rbps=2000000000 rseqiops=300000
#   rrandiops=70000 wbps=1500000000 wseqiops=200000 wrandiops=50000

# QoS 파라미터 조정
echo "8:0 enable=1 ctrl=auto" > /sys/fs/cgroup/io.cost.qos
# rpct: 읽기 완료 지연 백분위 목표
# rlat: 읽기 지연 목표 (us)
# wpct: 쓰기 완료 지연 백분위 목표
# wlat: 쓰기 지연 목표 (us)
# min: 최소 가중치 비율 (%)
# max: 최대 가중치 비율 (%)
v1 blkio vs v2 io 핵심 차이:

cgroup v1의 blkio 컨트롤러는 direct I/O만 제한하고 buffered I/O(페이지 캐시 경유 writeback)는 제한하지 못했습니다. 이는 대부분의 워크로드가 buffered I/O를 사용하므로 실질적으로 제한이 무력화되는 문제였습니다. cgroup v2의 io 컨트롤러는 writeback I/O를 원래 cgroup에 정확히 귀속시켜 이 문제를 해결합니다.

PID 컨트롤러

PID 컨트롤러는 cgroup 내에서 생성 가능한 프로세스/스레드 수를 제한하여 포크 폭탄(fork bomb)을 방지합니다. 시스템 전체의 PID 소진을 막는 중요한 안전장치입니다.

# PID 제한 설정 (최대 500개 프로세스/스레드)
echo 500 > /sys/fs/cgroup/myapp/pids.max

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

# 최고 기록 확인
cat /sys/fs/cgroup/myapp/pids.peak
# 128

# 이벤트 확인 (제한 초과 시도 횟수)
cat /sys/fs/cgroup/myapp/pids.events
# max 3    ← fork()가 3번 EAGAIN으로 실패
PID 컨트롤러: 포크 폭탄 방어 정상 동작 (pids.current < pids.max) PID 1 PID 2 PID 3 ... fork() → 성공 ✓ 포크 폭탄 (pids.current ≥ pids.max) pids.max=500 도달 fork() → EAGAIN ✗ 커널 경로: copy_process() → cgroup_can_fork() pids_can_fork() → pids_try_charge() → page_counter_try_charge() 계층적 검사: 모든 조상 cgroup의 pids.max도 함께 확인 clone3()/fork()/vfork()/pthread_create() 모두 적용
PID 컨트롤러와 스레드: pids.max는 프로세스뿐만 아니라 스레드(clone의 CLONE_THREAD)도 포함합니다. 따라서 Java/Go 같은 다중 스레드 런타임에서는 충분히 큰 값을 설정해야 합니다. 일반적으로 컨테이너 런타임은 4096을 기본값으로 사용합니다.

디바이스 컨트롤러

cgroup v2에서는 eBPF를 통해 디바이스 접근을 제어합니다. v1의 devices 컨트롤러(화이트리스트/블랙리스트 방식)와 달리, v2는 BPF_PROG_TYPE_CGROUP_DEVICE 타입의 BPF 프로그램을 cgroup에 부착하여 mknod()와 장치 파일 open()을 필터링합니다.

/* BPF 디바이스 필터 프로그램 예시 */
SEC("cgroup/dev")
int device_filter(struct bpf_cgroup_dev_ctx *ctx) {
    /* ctx->major, ctx->minor: 장치 번호 */
    /* ctx->access_type: BPF_DEVCG_ACC_MKNOD/READ/WRITE */
    /* ctx->major == 0 이면 허용 (null 장치) */

    if (ctx->major == 1 && ctx->minor == 3)
        return 1;  /* /dev/null 허용 */
    if (ctx->major == 1 && ctx->minor == 5)
        return 1;  /* /dev/zero 허용 */
    if (ctx->major == 1 && ctx->minor == 8)
        return 1;  /* /dev/random 허용 */
    if (ctx->major == 1 && ctx->minor == 9)
        return 1;  /* /dev/urandom 허용 */
    if (ctx->major == 5 && ctx->minor == 0)
        return 1;  /* /dev/tty 허용 */
    if (ctx->major == 136)
        return 1;  /* /dev/pts/* 허용 */

    return 0;  /* 나머지 모두 거부 */
}

PSI 내부 연산 심화

PSI(Pressure Stall Information)는 "자원을 기다리느라 일하지 못하는 시간의 비율"을 정량화합니다. 여기서는 커널 내부에서 PSI가 어떻게 계산되는지 상세히 분석합니다.

PSI 태스크 상태 추적과 압력 계산 RUNNING CPU에서 실행 중 RUNNABLE 실행 가능하지만 대기 IOWAIT I/O 완료 대기 MEMSTALL 메모리 회수 대기 PSI 압력 계산 방법 SOME 압력 1개 이상 태스크가 stall 상태인 시간 비율 CPU 4개, 3개 실행 + 1개 IOWAIT → some=25% cpu/memory/io 모두 해당 FULL 압력 모든 태스크가 stall 상태인 시간 비율 CPU 4개, 0개 실행 + 4개 IOWAIT → full=100% memory/io만 해당 (CPU는 full 없음) 지수 이동 평균 (EMA) 계산 avg[t] = avg[t-1] × decay + sample × (1 - decay) avg10: decay=e^(-2s/10s) avg60: decay=e^(-2s/60s) avg300: decay=e^(-2s/300s) 2초 주기로 샘플링 → 10초/60초/300초 윈도우의 가중 평균
/* kernel/sched/psi.c — PSI 핵심 구현 */

/* 태스크 상태 플래그 */
#define TSK_IOWAIT    (1 << 0)  /* I/O 대기 */
#define TSK_MEMSTALL  (1 << 1)  /* 메모리 회수 대기 */
#define TSK_RUNNING   (1 << 2)  /* 실행 가능 */
#define TSK_ONCPU     (1 << 3)  /* 실제 CPU에서 실행 */

/* PSI 그룹: 시스템 전체 + cgroup별 */
struct psi_group {
    struct psi_group_cpu __percpu *pcpu;
    u64  total[NR_PSI_STATES];      /* 누적 stall 시간 */
    u64  avg[NR_PSI_STATES][3];    /* EMA: avg10/60/300 */
    struct delayed_work avgs_work;  /* 2초 주기 갱신 */
};

/* 상태 전환 시 호출 */
void psi_task_change(struct task_struct *task,
                     int clear, int set) {
    /* clear: 해제할 상태 비트 */
    /* set: 설정할 상태 비트 */
    /* → per-CPU 카운터 갱신 → 글로벌 집계 */
}

/* some/full 판정 로직 */
/* some: nr_running < nr_online_cpus 이면 some 상태 */
/* full: nr_running == 0 이면 full 상태 */

PSI 트리거 프로그래밍

PSI 트리거는 userspace에서 리소스 압력 이벤트를 효율적으로 감지하는 메커니즘입니다. poll() 시스템 콜과 결합하여 이벤트 기반 리소스 관리를 구현할 수 있습니다:

/* PSI 트리거 기반 메모리 관리 데몬 예시 */
#include <fcntl.h>
#include <poll.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    int fd;
    struct pollfd fds[1];
    char trigger[] = "some 150000 1000000";
    /* "some 150000 1000000" 의미:
     * 1초(1000000us) 윈도우에서 150ms(150000us) 이상
     * some 메모리 압력이 발생하면 이벤트 발생 */

    fd = open("/proc/pressure/memory", O_RDWR | O_NONBLOCK);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    /* 트리거 등록 */
    if (write(fd, trigger, sizeof(trigger) - 1) < 0) {
        perror("write trigger");
        return 1;
    }

    fds[0].fd = fd;
    fds[0].events = POLLPRI;

    /* 이벤트 루프 */
    while (1) {
        int ret = poll(fds, 1, -1);
        if (ret > 0 && (fds[0].revents & POLLPRI)) {
            printf("메모리 압력 감지! 회수 작업 시작...\n");
            /* memory.reclaim 트리거 또는 프로세스 종료 등 */
        }
    }
    close(fd);
    return 0;
}
💡

PSI 활용 사례: Facebook의 oomd(userspace OOM daemon)와 Android의 lmkd(Low Memory Killer Daemon)는 PSI 트리거를 사용하여 커널 OOM killer보다 지능적인 메모리 관리를 수행합니다. PSI 임계값을 단계별로 설정하여 경고→회수→종료 순으로 대응할 수 있습니다.

cgroup-aware OOM Killer

cgroup v2에서 OOM killer는 cgroup 경계를 인식하여 동작합니다. memory.max를 초과한 cgroup 내에서만 프로세스를 선택하여 종료하므로, 다른 cgroup의 워크로드에 영향을 주지 않습니다.

cgroup OOM Killer 결정 트리 memory.max 초과 direct reclaim 시도 회수 성공? 할당 진행 ✓ yes memory.oom.group == 1 ? no 그룹 전체 Kill cgroup 내 모든 프로세스 종료 yes oom_score_adj 기반 프로세스 선택 (cgroup 내부에서만 탐색) no oom_score_adj == -1000 ? 건너뛰기 (보호) yes SIGKILL 전송 memory.events.oom_kill++ no
# memory.oom.group 설정: 그룹 전체 종료 모드
# 데이터베이스처럼 프로세스 간 강한 의존성이 있을 때 유용
echo 1 > /sys/fs/cgroup/database/memory.oom.group

# OOM 이벤트 모니터링
cat /sys/fs/cgroup/database/memory.events
# low 0
# high 15          ← memory.high 초과 횟수
# max 3            ← memory.max 도달 횟수
# oom 2            ← OOM 발생 횟수
# oom_kill 1       ← OOM kill 발생 횟수
# oom_group_kill 0 ← 그룹 OOM kill 횟수

# cgroup.kill로 긴급 전체 종료 (커널 5.14+)
echo 1 > /sys/fs/cgroup/runaway-workload/cgroup.kill
# cgroup 내 모든 프로세스에 SIGKILL 전송
주의: memory.oom.group=1은 cgroup 내 모든 프로세스를 종료합니다. oom_score_adj=-1000인 프로세스도 예외 없이 종료됩니다. 이 설정은 프로세스 간 의존성이 강한 워크로드(데이터베이스, 분산 시스템)에만 사용하세요.

systemd cgroup 연동

systemd는 cgroup v2의 주요 소비자로, 모든 서비스, 사용자 세션, 시스템 리소스를 cgroup 계층으로 체계적으로 관리합니다.

systemd cgroup 계층 구조 -.slice (root) system.slice 시스템 서비스 user.slice 사용자 세션 machine.slice VM / 컨테이너 nginx.service sshd.service user-1000.slice user-1001.slice session-1.scope systemd 리소스 관리 단위 .service (서비스) systemd가 직접 시작/관리 MemoryMax, CPUQuota 등 유닛 파일로 설정 예: nginx.service .scope (스코프) 외부에서 시작된 프로세스 그룹 런타임에 동적 생성 systemd-run으로 생성 예: session-1.scope .slice (슬라이스) 서비스/스코프를 그룹화하는 계층 리소스 분배의 계층 단위 CPUWeight, MemoryMax 상속 예: system.slice, user.slice

systemd 리소스 제어 지시어

systemd 지시어cgroup v2 파일설명예시
MemoryMaxmemory.max메모리 하드 제한MemoryMax=2G
MemoryHighmemory.high메모리 스로틀링 임계값MemoryHigh=1G
MemoryLowmemory.low메모리 소프트 보호MemoryLow=512M
MemoryMinmemory.min메모리 하드 보호MemoryMin=256M
MemorySwapMaxmemory.swap.max스왑 제한MemorySwapMax=1G
CPUQuotacpu.maxCPU 대역폭 제한CPUQuota=150%
CPUWeightcpu.weightCPU 가중치CPUWeight=200
IOWeightio.weightI/O 가중치IOWeight=500
IODeviceWeightio.weight디바이스별 I/O 가중치IODeviceWeight=/dev/sda 200
IOReadBandwidthMaxio.max (rbps)읽기 대역폭 제한IOReadBandwidthMax=/dev/sda 100M
IOWriteBandwidthMaxio.max (wbps)쓰기 대역폭 제한IOWriteBandwidthMax=/dev/sda 50M
TasksMaxpids.max최대 프로세스/스레드 수TasksMax=512
Delegate위임 활성화하위 트리 위임Delegate=cpu memory io
AllowedCPUscpuset.cpus허용 CPU 목록AllowedCPUs=0-3
AllowedMemoryNodescpuset.mems허용 NUMA 노드AllowedMemoryNodes=0
# systemd 서비스 유닛 파일 예시: 웹 서버 리소스 제한
# /etc/systemd/system/webapp.service
[Unit]
Description=Web Application Server
After=network.target

[Service]
ExecStart=/usr/bin/webapp --port 8080
Restart=always

# CPU 제한: 2코어 분량 (200%), 가중치 200
CPUQuota=200%
CPUWeight=200

# 메모리 제한: 스로틀링 1.5G, 하드 2G, 보호 512M
MemoryHigh=1536M
MemoryMax=2G
MemoryLow=512M

# I/O 제한
IOWeight=500
IOReadBandwidthMax=/dev/nvme0n1 500M
IOWriteBandwidthMax=/dev/nvme0n1 200M

# PID 제한
TasksMax=1024

# 위임 (컨테이너 런타임 등이 하위 cgroup 관리)
Delegate=cpu memory io pids

# OOM 설정
OOMPolicy=stop
OOMScoreAdjust=-100

[Install]
WantedBy=multi-user.target
# systemd-run으로 임시 리소스 제한 적용
systemd-run --scope -p MemoryMax=1G -p CPUQuota=50% -- ./heavy-computation

# 실행 중인 서비스의 리소스 제한 동적 변경
systemctl set-property webapp.service MemoryMax=3G CPUQuota=300%

# cgroup 계층 확인
systemd-cgls
# Control group /:
# ├─init.scope
# │ └─1 /usr/lib/systemd/systemd
# ├─system.slice
# │ ├─webapp.service
# │ │ └─1234 /usr/bin/webapp
# │ └─sshd.service
# └─user.slice
#   └─user-1000.slice

# 실시간 리소스 사용량 모니터링
systemd-cgtop

cgroup BPF 프로그램

eBPF(extended Berkeley Packet Filter)는 cgroup과 결합하여 강력한 네트워크, 디바이스, 소켓 제어를 제공합니다. cgroup에 BPF 프로그램을 부착하면 해당 cgroup의 모든 프로세스에 자동으로 적용됩니다.

cgroup BPF 프로그램 훅 포인트 Application 소켓 BPF BPF_CGROUP_INET_INGRESS BPF_CGROUP_INET_EGRESS BPF_CGROUP_SOCK_OPS BPF_CGROUP_INET4_BIND BPF_CGROUP_INET4_CONNECT 네트워크 BPF BPF_CGROUP_SKB (패킷 필터) BPF_CGROUP_INET_POST_BIND BPF_CGROUP_UDP4_SENDMSG BPF_CGROUP_UDP4_RECVMSG BPF_CGROUP_GETSOCKOPT 시스템 BPF BPF_CGROUP_DEVICE (장치 접근) BPF_CGROUP_SYSCTL (sysctl 필터) BPF_LSM (보안 모듈) → 읽기/쓰기 가로채기 → 허용/거부 결정 스케줄링 BPF sched_ext (6.12+) BPF 기반 스케줄러 cgroup 단위 스케줄링 정책 결정 BPF 프로그램 부착 방법 bpf(BPF_PROG_ATTACH, {target_fd=cgroup_fd, attach_bpf_fd=prog_fd, attach_type=BPF_CGROUP_*}) 부모 cgroup에 부착하면 모든 자식 cgroup에 자동 적용 (계층적 상속) BPF_F_ALLOW_MULTI: 같은 훅에 여러 프로그램 부착 가능
# bpftool로 cgroup BPF 프로그램 확인
bpftool cgroup list /sys/fs/cgroup/myapp/
# ID       AttachType      AttachFlags     Name
# 42       ingress         multi           filter_ingress
# 43       egress          multi           filter_egress
# 44       device          multi           device_filter

# BPF 프로그램 부착 (bpftool 사용)
bpftool cgroup attach /sys/fs/cgroup/myapp/ egress pinned /sys/fs/bpf/my_egress_filter multi

# ip 명령으로 네트워크 cgroup BPF 확인
ip link show dev eth0 | grep -i bpf

cgroup v1 → v2 마이그레이션

기존 cgroup v1 환경에서 v2로 전환하는 것은 단계적으로 진행해야 합니다. 하이브리드 모드를 거쳐 완전 v2로 전환하는 것이 권장됩니다.

cgroup v1 → v2 마이그레이션 경로 Phase 1: v1 Only cgroup v1만 사용 컨트롤러별 독립 계층 Docker/containerd v1 모드 현재 레거시 환경 Phase 2: Hybrid v1 + v2 동시 사용 systemd: cgroup v2 일부 컨트롤러: v1 전환 과도기 Phase 3: v2 Only cgroup v2만 사용 단일 통합 계층 PSI, 안전한 위임 사용 목표 상태 마이그레이션 단계별 체크리스트 1. systemd v2 활성화: GRUB에 systemd.unified_cgroup_hierarchy=1 추가 2. 컨테이너 런타임 확인: Docker 20.10+, containerd 1.4+, podman 3.0+ (v2 지원) 3. v1 인터페이스 매핑: memory.limit_in_bytes → memory.max, cpu.cfs_quota_us → cpu.max 등 4. v1 비활성화: cgroup_no_v1=all 부트 옵션으로 v1 완전 제거

v1 → v2 인터페이스 매핑

cgroup v1 인터페이스cgroup v2 인터페이스변경 사항
memory.limit_in_bytesmemory.max동일 기능
memory.soft_limit_in_bytesmemory.highv2는 스로틀링 추가
(없음)memory.low, memory.minv2에서 새로 추가된 보호 인터페이스
memory.memsw.limit_in_bytesmemory.swap.maxv2는 스왑만 별도 제한
memory.kmem.limit_in_bytes(통합)v2는 커널 메모리 통합 계정
cpu.cfs_quota_us / cpu.cfs_period_uscpu.max"quota period" 형식으로 통합
cpu.sharescpu.weight범위 변경: 2-262144 → 1-10000
blkio.throttle.read_bps_deviceio.maxrbps/wbps/riops/wiops 통합
blkio.weightio.weight범위 변경: 10-1000 → 1-10000
devices.allow / devices.denyBPF_CGROUP_DEVICEBPF 프로그램으로 대체
net_cls.classidBPF_CGROUP_INET_*BPF 기반 네트워크 제어로 대체
(없음)cpu.pressure, memory.pressure, io.pressurev2에서 PSI 추가
release_agentcgroup.events (populated)보안 개선: 커널이 직접 프로그램 실행하지 않음
# v1→v2 변환 예시: cpu.shares → cpu.weight
# v1: cpu.shares 범위 2-262144, 기본 1024
# v2: cpu.weight 범위 1-10000, 기본 100
# 변환 공식: weight = 1 + ((shares - 2) * 9999) / 262142

# v1: 2048 (기본의 2배) → v2: weight ≈ 200
echo 200 > /sys/fs/cgroup/myapp/cpu.weight

# v1: cpu.cfs_quota_us=50000, cpu.cfs_period_us=100000
# v2: cpu.max "50000 100000" (quota period 형식)
echo "50000 100000" > /sys/fs/cgroup/myapp/cpu.max

# 하이브리드 모드 확인: v1과 v2가 공존하는지 체크
mount | grep cgroup
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,...) ← v2
# cgroup on /sys/fs/cgroup/memory type cgroup (rw,...) ← v1 잔여

# v1 완전 비활성화 확인
cat /proc/cmdline | grep cgroup
# ... systemd.unified_cgroup_hierarchy=1 cgroup_no_v1=all

스레드 모드 (Threaded cgroups)

cgroup v2의 스레드 모드는 프로세스 내 개별 스레드를 다른 cgroup에 배치할 수 있는 기능입니다. 일반 cgroup(domain)은 프로세스 단위로만 이동 가능하지만, threaded cgroup에서는 스레드(TID) 단위로 이동할 수 있습니다.

# 스레드 모드 활성화
echo "threaded" > /sys/fs/cgroup/myapp/worker-pool/cgroup.type

# cgroup 유형 확인
cat /sys/fs/cgroup/myapp/cgroup.type
# domain threaded    ← 스레드 도메인 (자식이 threaded)

cat /sys/fs/cgroup/myapp/worker-pool/cgroup.type
# threaded           ← 스레드 모드 cgroup

# 스레드 단위 이동 (TID 기반)
echo 1234 > /sys/fs/cgroup/myapp/worker-pool/cgroup.threads

# 스레드 모드에서 사용 가능한 컨트롤러
# cpu, cpuset, perf_event, pids
# memory, io는 스레드 모드 미지원 (프로세스 단위)
특성domain cgroupthreaded cgroup
이동 단위프로세스 (PID)스레드 (TID)
프로세스 파일cgroup.procscgroup.threads
지원 컨트롤러전체cpu, cpuset, perf_event, pids
리소스 도메인자체부모 domain의 리소스 도메인 공유
사용 사례프로세스 단위 격리스레드풀 CPU 배분, 워커 스레드 제어
스레드 모드 사용 사례: 웹 서버에서 I/O 스레드와 CPU 집약적 스레드를 다른 cpuset에 배치하거나, 데이터베이스에서 쿼리 워커 스레드와 백그라운드 유지보수 스레드의 CPU 가중치를 다르게 설정할 수 있습니다.

cgroup 네임스페이스

cgroup 네임스페이스는 프로세스가 자신의 cgroup을 루트(/)로 인식하도록 가상화합니다. 컨테이너 내부에서 /proc/self/cgroup을 읽으면 호스트의 실제 cgroup 경로 대신 가상화된 경로를 보게 됩니다.

# 호스트에서 확인
cat /proc/self/cgroup
# 0::/system.slice/docker-abc123.scope

# 컨테이너 내부에서 확인 (cgroup namespace 적용)
cat /proc/self/cgroup
# 0::/
# → 자신의 cgroup이 루트로 보임

# unshare로 cgroup 네임스페이스 생성
unshare --cgroup bash
cat /proc/self/cgroup
# 0::/    ← 현재 cgroup이 루트로 가상화

# nsdelegate 마운트 옵션: cgroup NS 경계에서 위임 자동 적용
mount -t cgroup2 none /sys/fs/cgroup -o nsdelegate
nsdelegate의 중요성: nsdelegate 마운트 옵션을 사용하면 cgroup 네임스페이스 경계를 자동으로 delegation 경계로 인식합니다. 이는 컨테이너 내부에서 cgroup 설정을 변경하더라도 네임스페이스 외부의 cgroup에 영향을 줄 수 없도록 보장합니다. systemd 247+에서 기본 활성화됩니다.

cgroup 모니터링 및 디버깅

cgroup 관련 문제를 진단하고 리소스 사용을 모니터링하기 위한 도구와 기법을 정리합니다.

cgroup 모니터링 도구 생태계 CLI 도구 systemd-cgtop systemd-cgls cat /sys/fs/cgroup/*/ cgroup-utils cadvisor htop (tree view) 커널 트레이싱 ftrace:cgroup perf stat --cgroup bpftrace cgroup probes trace-cmd /sys/kernel/debug/cgroup BPF 관측성 bcc tools: cgroupstats bpftrace one-liners cgroup iter programs PSI 트리거 모니터 메트릭 수집 Prometheus node_exporter cAdvisor + Prometheus Grafana 대시보드 OpenTelemetry 핵심 진단 명령어 systemd-cgtop -m # 실시간 CPU/메모리/I/O per-cgroup 모니터링 systemd-cgls # cgroup 트리 구조 출력 cat memory.stat # 상세 메모리 통계 (anon, file, slab, sock, ...) cat cpu.stat # CPU 사용, 스로틀 통계 cat memory.pressure # PSI 메모리 압력 perf stat -G myapp.slice # cgroup별 perf 카운터
# === cgroup 디버깅 실전 가이드 ===

# 1. 스로틀링 확인: CPU 제한에 걸리고 있는지?
cat /sys/fs/cgroup/myapp/cpu.stat
# nr_throttled 가 증가하고 있으면 cpu.max 제한에 걸리는 중
# throttled_usec 가 크면 성능 영향이 큰 상태

# 2. 메모리 압력 확인: 메모리가 부족한지?
cat /sys/fs/cgroup/myapp/memory.pressure
# some avg10=25.00 → 10% 이상이면 메모리 압력 있음

cat /sys/fs/cgroup/myapp/memory.events
# high 값이 증가하면 memory.high 초과 (스로틀링)
# oom 값이 증가하면 OOM 발생

# 3. 메모리 사용 내역 분석
cat /sys/fs/cgroup/myapp/memory.stat | head -20
# anon: 프로세스 메모리 (heap, stack)
# file: 파일 캐시
# kernel: 커널 메모리 (slab, page tables)
# sock: 소켓 버퍼

# 4. I/O 통계 확인
cat /sys/fs/cgroup/myapp/io.stat
# 8:0 rbytes=... wbytes=... rios=... wios=...

# 5. dying cgroup 확인 (좀비 cgroup)
cat /sys/fs/cgroup/cgroup.stat
# nr_descendants 42
# nr_dying_descendants 5  ← 좀비 cgroup 수
# dying cgroup은 메모리를 점유하고 있지만 사라지지 않는 상태
# → 주로 page cache 참조로 인해 발생

# 6. perf로 cgroup별 성능 카운터
perf stat -a --cgroup=system.slice/nginx.service -- sleep 5

# 7. bpftrace로 cgroup 이벤트 추적
bpftrace -e 'tracepoint:cgroup:cgroup_attach_task {
    printf("PID %d → cgroup %s\n", args->pid, args->dst_path);
}'
💡

Dying cgroup 문제: nr_dying_descendants가 계속 증가하면 메모리 누수와 비슷한 증상이 나타납니다. 컨테이너를 자주 생성/삭제하는 환경에서 흔히 발생하며, memory.reclaim이나 echo 3 > /proc/sys/vm/drop_caches로 page cache를 드롭하면 dying cgroup이 해제됩니다.

cgroup 성능 최적화

cgroup은 리소스 격리를 위해 추가 커널 경로를 삽입하므로 약간의 오버헤드가 있습니다. 여기서는 성능 영향을 최소화하는 방법을 정리합니다.

최적화 영역문제점권장 설정
CPU 대역폭 period짧은 period(1ms)는 hrtimer 오버헤드 증가cpu.max의 period를 100ms 이상으로 설정
CPU burstburst=0이면 짧은 스파이크도 스로틀cpu.max.burst를 적절히 설정 (quota의 20-50%)
메모리 회수 빈도memory.high가 너무 낮으면 잦은 direct reclaimmemory.high = memory.max × 0.8 (여유 20%)
계층 깊이깊은 계층은 charge/uncharge 경로 증가3단계 이하 권장 (root→slice→app)
cgroup 수cgroup 수 증가 → 커널 메모리 증가불필요한 cgroup 정리, dying cgroup 모니터
IO 제어io.latency는 큐 깊이 조절 오버헤드latency-sensitive 워크로드에만 적용
PSI 모니터링PSI 계산은 per-CPU 오버헤드불필요한 cgroup에서 cgroup.pressure=0
메모리 보호memory.min/low은 reclaim 경로 복잡도 증가정말 필요한 워크로드에만 설정
# === 성능 최적화 실전 ===

# 1. CPU burst 설정으로 마이크로 스파이크 허용
echo "200000 100000" > /sys/fs/cgroup/webserver/cpu.max
# 100ms period, 200ms quota (2 CPU)
echo 50000 > /sys/fs/cgroup/webserver/cpu.max.burst
# 50ms burst: 유휴 시 미사용 quota를 최대 50ms 축적
# → 스파이크 시 250ms까지 사용 가능

# 2. 메모리 보호 계층 설정 (효율적인 3단계)
# root → system.slice → nginx.service
echo "+memory +cpu +io" > /sys/fs/cgroup/system.slice/cgroup.subtree_control
echo 2G > /sys/fs/cgroup/system.slice/nginx.service/memory.max
echo 1536M > /sys/fs/cgroup/system.slice/nginx.service/memory.high
echo 512M > /sys/fs/cgroup/system.slice/nginx.service/memory.low

# 3. 불필요한 PSI 비활성화 (오버헤드 감소)
echo 0 > /sys/fs/cgroup/batch-jobs/cgroup.pressure

# 4. dying cgroup 정리
echo 3 > /proc/sys/vm/drop_caches  # page cache 해제

# 5. cgroup 계층 깊이 제한
echo 3 > /sys/fs/cgroup/cgroup.max.depth
echo 256 > /sys/fs/cgroup/cgroup.max.descendants

Delegation 심화

cgroup v2의 위임(Delegation)은 비특권 사용자나 컨테이너 런타임이 자신에게 할당된 cgroup 하위 트리를 안전하게 관리할 수 있도록 하는 메커니즘입니다. v1에서 불가능했던 안전한 다중 사용자 cgroup 관리를 가능하게 합니다.

cgroup v2 Delegation 모델 관리자 영역 (root 권한) / (root cgroup) /user.slice/user-1000.slice (위임 경계) 위임된 영역 (uid=1000, 비특권 사용자) /app-1 /app-2 /app-3 위임받은 사용자가 할 수 있는 것: 하위 cgroup 생성/삭제, 프로세스 이동, subtree_control 조정 위임받은 사용자가 할 수 없는 것: 부모 cgroup 수정, 위임 경계 밖으로 프로세스 이동, 부모 제한 초과
# === Delegation 설정 방법 ===

# 방법 1: 파일 시스템 권한 기반
mkdir -p /sys/fs/cgroup/user.slice/user-1000.slice
# 위임할 파일들의 소유자를 변경
chown 1000:1000 /sys/fs/cgroup/user.slice/user-1000.slice/
chown 1000:1000 /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.procs
chown 1000:1000 /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.threads
chown 1000:1000 /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.subtree_control

# 방법 2: systemd Delegate 지시어
# [Service] 섹션에 추가
Delegate=cpu memory io pids
# → systemd가 자동으로 하위 트리 권한을 서비스에 위임

# 방법 3: nsdelegate 마운트 옵션 (컨테이너)
mount -t cgroup2 none /sys/fs/cgroup -o nsdelegate
# cgroup 네임스페이스 진입 시 자동으로 delegation 경계 생성

# Delegation 검증
# 위임받은 사용자(uid 1000)로 실행:
su - user1000
mkdir /sys/fs/cgroup/user.slice/user-1000.slice/my-app
echo $$ > /sys/fs/cgroup/user.slice/user-1000.slice/my-app/cgroup.procs
echo "+cpu +memory" > /sys/fs/cgroup/user.slice/user-1000.slice/cgroup.subtree_control

컨테이너 런타임 cgroup 연동

Docker, containerd, CRI-O 등 컨테이너 런타임은 cgroup v2를 사용하여 컨테이너별 리소스를 격리합니다. 런타임별 cgroup 계층 구조와 설정 방법을 정리합니다.

런타임cgroup v2 지원 버전cgroup 드라이버계층 구조
Docker20.10+systemd (권장) / cgroupfssystem.slice/docker-{id}.scope
containerd1.4+systemd (권장) / cgroupfssystem.slice/containerd-{id}.scope
CRI-O1.20+systemdsystem.slice/crio-{id}.scope
Podman3.0+systemd (기본)user.slice/user-{uid}.slice/user@{uid}.service/
# Docker: cgroup v2 + systemd 드라이버 설정
# /etc/docker/daemon.json
{
    "exec-opts": ["native.cgroupdriver=systemd"],
    "default-ulimits": {
        "nofile": {
            "Name": "nofile",
            "Hard": 65536,
            "Soft": 65536
        }
    }
}

# 컨테이너 리소스 제한 설정
docker run --memory=2g --memory-swap=3g \
           --cpus=2.0 --cpu-shares=512 \
           --pids-limit=1024 \
           --blkio-weight=500 \
           nginx

# 컨테이너의 cgroup 확인
docker inspect --format '{{.HostConfig.CgroupParent}}' 

# Kubernetes: cgroup v2 설정 (kubelet)
# /var/lib/kubelet/config.yaml
# cgroupDriver: systemd
# cgroupsPerQOS: true
# enforceNodeAllocatable: ["pods"]
Kubernetes와 cgroup v2: Kubernetes 1.25+에서 cgroup v2가 GA(정식 지원)됩니다. Pod 수준 리소스 제한이 cgroup v2의 memory.max/cpu.max에 직접 매핑되며, MemoryQoS(memory.high 기반)와 PSI 기반 eviction이 추가로 지원됩니다.

Kubernetes cgroup v2 계층 상세

Kubernetes는 cgroup v2에서 Pod 수준 QoS 클래스를 cgroup 계층으로 매핑합니다. kubelet의 cgroupDriver: systemd 설정이 권장되며, QoS 클래스별 cgroup 경로가 자동 생성됩니다.

Kubernetes cgroup v2 QoS 계층 kubepods.slice Guaranteed requests == limits (CPU, Memory) Burstable requests < limits BestEffort requests/limits 미설정 pod-abc.slice pod-def.slice container-1 container-2 Kubernetes → cgroup v2 매핑 Guaranteed Pod cpu.max = "200000 100000" cpu.weight = 204 (2 CPUs) memory.max = 2147483648 memory.min = 2147483648 → OOM 우선순위 최저 (보호) Burstable Pod cpu.max = "max 100000" cpu.weight = 102 (1 CPU req) memory.max = 4294967296 memory.low = 1073741824 → requests 보장, limits까지 버스트 BestEffort Pod cpu.max = "max 100000" cpu.weight = 2 (최소) memory.max = max memory.min/low = 0 → OOM 우선순위 최고 (첫 번째 kill)
# Kubernetes Pod cgroup 경로 (systemd 드라이버)
# /sys/fs/cgroup/kubepods.slice/
# ├── kubepods-guaranteed.slice/
# │   └── kubepods-guaranteed-pod{uid}.slice/
# │       ├── cri-containerd-{container-id}.scope
# │       └── cri-containerd-{pause-id}.scope
# ├── kubepods-burstable.slice/
# │   └── kubepods-burstable-pod{uid}.slice/
# │       └── ...
# └── kubepods-besteffort.slice/
#     └── kubepods-besteffort-pod{uid}.slice/
#         └── ...

# Pod의 실제 cgroup 설정 확인
POD_CGROUP=kubepods.slice/kubepods-burstable.slice/kubepods-burstable-podABC.slice
cat /sys/fs/cgroup/$POD_CGROUP/memory.max
cat /sys/fs/cgroup/$POD_CGROUP/cpu.max
cat /sys/fs/cgroup/$POD_CGROUP/memory.pressure

# kubelet MemoryQoS (alpha → beta, cgroup v2 전용)
# Burstable Pod: memory.high = requests × 1.5 (자동 설정)
# → memory.high 초과 시 스로틀링 (OOM 방지)
# kubelet feature gate: MemoryQoS=true

실전 운영 레시피

프로덕션 환경에서 자주 사용되는 cgroup 설정 패턴과 문제 해결 레시피를 정리합니다.

레시피 1: 웹 서버 최적 cgroup 설정

# 웹 서버: 응답 시간 보장 + 메모리 보호
# systemd unit: /etc/systemd/system/nginx.service.d/cgroup.conf
[Service]
# CPU: 4코어 분량, 버스트 허용
CPUQuota=400%
CPUWeight=300

# 메모리: 하드 4G, 스로틀 3G, 보호 1G
MemoryMax=4G
MemoryHigh=3G
MemoryLow=1G

# I/O: 높은 가중치 (DB보다 낮지만 배치보다 높게)
IOWeight=500

# PID: 웹 서버 워커 + 커넥션 핸들러
TasksMax=4096

# UCLAMP: 최소 성능 보장 (모바일/ARM)
# cpu.uclamp.min=20.00 (systemd 252+)

레시피 2: 데이터베이스 cgroup 설정

# 데이터베이스: 메모리 보호 최우선 + OOM 그룹 kill
[Service]
# CPU: 전용 코어 할당
AllowedCPUs=4-15
CPUWeight=500

# 메모리: 최대한 보호
MemoryMax=32G
MemoryHigh=28G
MemoryLow=16G
MemoryMin=8G
MemorySwapMax=0

# OOM 시 전체 종료 (일부만 죽으면 데이터 손상)
OOMPolicy=stop
# memory.oom.group=1 (수동 설정 필요)

# I/O: 최고 가중치
IOWeight=900
IODeviceWeight=/dev/nvme0n1 1000

# 위임: 내부 워커 관리
Delegate=cpu memory io pids

레시피 3: 배치 작업 격리

# 배치 작업: 낮은 우선순위, 남은 자원 활용
systemd-run --scope --slice=batch.slice \
  -p CPUWeight=10 \
  -p MemoryMax=8G \
  -p MemoryHigh=6G \
  -p IOWeight=10 \
  -p TasksMax=256 \
  # cpu.idle=1 효과 (systemd 252+)
  -- /usr/bin/batch-job

# 배치 슬라이스 전체 설정
# /etc/systemd/system/batch.slice
[Slice]
CPUWeight=10
MemoryMax=16G
IOWeight=10

레시피 4: 포크 폭탄 방어

# 시스템 전체 PID 제한
echo 32768 > /sys/fs/cgroup/pids.max

# 사용자별 PID 제한 (systemd)
# /etc/systemd/system/user-.slice.d/pids.conf
[Slice]
TasksMax=4096

# 컨테이너별 PID 제한
docker run --pids-limit=512 nginx

# 포크 폭탄 감지 스크립트
while true; do
    for cg in /sys/fs/cgroup/user.slice/user-*/; do
        current=$(cat "$cg/pids.current" 2>/dev/null)
        max=$(cat "$cg/pids.max" 2>/dev/null)
        if [ "$current" -gt $((max * 90 / 100)) ]; then
            echo "경고: $cg PID 90% 초과 ($current/$max)"
        fi
    done
    sleep 5
done

레시피 5: 폭주 워크로드 긴급 격리

# 긴급 상황: 특정 cgroup이 시스템 자원을 독점

# 1단계: 즉시 CPU 제한 (10% CPU만 허용)
echo "10000 100000" > /sys/fs/cgroup/runaway/cpu.max

# 2단계: 메모리 제한 강화
echo 1G > /sys/fs/cgroup/runaway/memory.max

# 3단계: I/O 제한
echo "8:0 rbps=1048576 wbps=1048576" > /sys/fs/cgroup/runaway/io.max

# 4단계: 프로세스 동결 (분석을 위해)
echo 1 > /sys/fs/cgroup/runaway/cgroup.freeze

# 5단계: 상태 분석
cat /sys/fs/cgroup/runaway/memory.stat
cat /sys/fs/cgroup/runaway/cpu.stat
cat /sys/fs/cgroup/runaway/pids.current

# 6단계: 필요 시 전체 종료
echo 1 > /sys/fs/cgroup/runaway/cgroup.kill

# 대안: 동결 해제 후 정상 제한으로 복구
echo 0 > /sys/fs/cgroup/runaway/cgroup.freeze
echo "200000 100000" > /sys/fs/cgroup/runaway/cpu.max
긴급 대응 시 주의: cgroup.freeze로 프로세스를 동결하면 데이터베이스 연결 타임아웃, 세션 만료 등 부작용이 발생할 수 있습니다. 동결은 분석 목적으로만 사용하고, 빠르게 해제하거나 cgroup.kill로 전환하세요.

mem_cgroup 커널 내부 구조

메모리 컨트롤러의 핵심 자료구조인 struct mem_cgroup을 분석합니다. 이 구조체는 cgroup당 메모리 회계, 제한, 통계를 관리합니다.

/* include/linux/memcontrol.h — mem_cgroup 핵심 필드 */
struct mem_cgroup {
    struct cgroup_subsys_state css;  /* cgroup 서브시스템 상태 (공통) */

    /* 페이지 카운터 (계층적 누적) */
    struct page_counter memory;     /* 전체 메모리 카운터 */
    struct page_counter swap;       /* 스왑 카운터 */
    struct page_counter kmem;       /* 커널 메모리 (v2에서는 memory에 통합) */
    struct page_counter tcpmem;     /* TCP 소켓 메모리 */

    /* 워터마크 */
    unsigned long high;             /* memory.high (스로틀링 임계값) */

    /* VMPRESSURE: 메모리 압력 모니터 */
    struct vmpressure vmpressure;

    /* OOM 관련 */
    bool oom_group;                 /* memory.oom.group */
    int under_oom;                  /* OOM 진행 중 */
    int oom_kill_disable;           /* OOM kill 비활성화 */

    /* 통계: memory.stat에 표시 */
    struct memcg_vmstats *vmstats;
    struct memcg_vmstats_percpu __percpu *vmstats_percpu;

    /* LRU: cgroup-aware 페이지 회수 */
    struct mem_cgroup_per_node *nodeinfo[];
    /* → 노드별 LRU 리스트 (anon_active, anon_inactive, */
    /*    file_active, file_inactive) */

    /* 이벤트 카운터: memory.events */
    struct cgroup_file events_file;
    struct cgroup_file events_local_file;

    /* 워크큐: memory.high 초과 시 비동기 회수 */
    struct work_struct high_work;

    /* 소프트 제한 (v1 호환) */
    unsigned long soft_limit;

    /* Swap: 스왑 관련 설정 */
    int swappiness;                 /* cgroup별 swappiness */

    /* 차징 관련 */
    struct task_struct *move_charge_at_immigrate;
};

/* mem_cgroup_per_node: NUMA 노드별 LRU */
struct mem_cgroup_per_node {
    struct lruvec lruvec;          /* cgroup-aware LRU 벡터 */
    unsigned long lru_zone_size[MAX_NR_ZONES][NR_LRU_LISTS];
    struct mem_cgroup_reclaim_iter iter;
    struct rb_node tree_node;     /* soft limit 트리 */
    unsigned long usage_in_excess; /* soft limit 초과량 */
};
Q: mem_cgroup의 핵심 설계 원리는?
  • page_counter모든 메모리 유형(anon, file, kernel, swap, TCP)을 page_counter로 추적합니다. 각 카운터는 부모를 가리켜 계층적으로 누적됩니다.
  • mem_cgroup_per_nodeNUMA 노드별로 독립적인 LRU 리스트를 유지하여 cgroup-aware 페이지 회수를 수행합니다. 글로벌 reclaim과 달리 특정 cgroup의 페이지만 회수합니다.
  • vmstats_percpuPer-CPU 통계를 유지하여 빈번한 charge/uncharge 경로에서 lock contention을 최소화합니다. 주기적으로 글로벌 통계에 합산됩니다.
  • high_workmemory.high 초과 시 직접 호출이 아닌 work queue로 비동기 회수를 수행하여 할당 경로의 지연을 줄입니다.

cgroup 커널 구현 아키텍처

cgroup 코어의 핵심 자료구조와 관계를 분석합니다.

cgroup 커널 자료구조 관계도 task_struct → cgroups (css_set *) → cg_list (list_head) → in_iowait (PSI 추적) → memcg_data (folio 과금) css_set → subsys[] (CSS 배열) → refcount (참조 카운트) → cgrp_links (cgrp 연결) → tasks (태스크 리스트) cgroup_subsys_state → cgroup (소속 cgroup) → ss (cgroup_subsys) → parent (부모 CSS) → refcnt (참조 카운트) 1:1 subsys[] cgroup → self (css) → subtree_control (비트맵) → children (자식 리스트) → kn (kernfs_node) →cgroup cgroup_subsys → css_alloc / css_free → attach / can_attach → fork / exit → name ("cpu"/"memory"...) mem_cgroup CSS 상속 + page_counter + LRU + 통계 + OOM task_group CSS 상속 + cfs_bandwidth + 스케줄 엔티티 blkcg CSS 상속 + io.max/weight + 디바이스별 정책 pids_cgroup CSS 상속 + counter + events 구체적 컨트롤러 상태 (CSS 상속)
/* cgroup 프로세스 이동 핵심 경로 */
/* cgroup.procs에 PID를 쓰면 호출되는 경로 */

/* 1. 유효성 검사 */
cgroup_procs_write(...)
  → cgroup_attach_task(dst_cgrp, task, threadgroup)
    → cgroup_migrate_add_src(...)   /* 이동 대상 수집 */cgroup_migrate_prepare_dst(...) /* 목적지 css_set 할당 */

/* 2. 각 컨트롤러의 can_attach() 호출 */
for_each_subsys(ss, i) {
    if (ss->can_attach)
        ret = ss->can_attach(css, &mgctx->tset);
    /* 메모리 컨트롤러: charge 이동 가능 여부 확인 */
    /* CPU 컨트롤러: 대역폭 재계산 */
}

/* 3. 실제 이동 실행 */
cgroup_migrate_execute(...)
  → css_set_move_task(task, from_cset, to_cset)
  → for_each_subsys(ss, i)
        ss->attach(css, &mgctx->tset);

/* 4. RCU 기반 안전한 교체 */
rcu_assign_pointer(task->cgroups, new_cset);
/* → 다음 RCU grace period 이후 old_cset 해제 */

cgroup 파일시스템 구조

cgroup v2는 cgroup2 가상 파일시스템을 통해 사용자 공간과 통신합니다. 이 파일시스템은 kernfs 기반으로 구현되어 있으며, 각 cgroup 디렉토리가 하나의 cgroup 노드에 대응합니다.

# cgroup2 파일시스템 정보
mount | grep cgroup2
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot)

# 마운트 옵션 상세
# nsdelegate: cgroup NS 경계에서 자동 delegation
# memory_recursiveprot: memory.low/min 재귀적 보호 활성화
# memory_hugetlb_accounting: HugeTLB 페이지 통합 회계

# 전체 cgroup 트리 구조 출력
find /sys/fs/cgroup -maxdepth 3 -type d | head -30
# /sys/fs/cgroup
# /sys/fs/cgroup/init.scope
# /sys/fs/cgroup/system.slice
# /sys/fs/cgroup/system.slice/nginx.service
# /sys/fs/cgroup/user.slice
# /sys/fs/cgroup/user.slice/user-1000.slice
# ...

# 각 cgroup 디렉토리의 파일 목록
ls /sys/fs/cgroup/system.slice/nginx.service/
# cgroup.controllers    cgroup.events        cgroup.freeze
# cgroup.kill           cgroup.max.depth     cgroup.max.descendants
# cgroup.pressure       cgroup.procs         cgroup.stat
# cgroup.subtree_control cgroup.threads       cgroup.type
# cpu.max               cpu.max.burst        cpu.pressure
# cpu.stat              cpu.uclamp.max       cpu.uclamp.min
# cpu.weight            cpu.weight.nice      io.max
# io.pressure           io.stat              io.weight
# memory.current        memory.events        memory.events.local
# memory.high           memory.low           memory.max
# memory.min            memory.numa_stat     memory.oom.group
# memory.peak           memory.pressure      memory.reclaim
# memory.stat           memory.swap.current  memory.swap.events
# memory.swap.high      memory.swap.max      memory.swap.peak
# memory.zswap.current  memory.zswap.max     pids.current
# pids.events           pids.max             pids.peak

cgroup.events를 이용한 이벤트 모니터링

cgroup.events 파일은 inotifypoll()로 모니터링할 수 있어, cgroup 상태 변화를 효율적으로 감지할 수 있습니다:

/* cgroup.events 이벤트 모니터링 예시 */
#include <sys/inotify.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    int ifd = inotify_init1(IN_NONBLOCK);
    int wd = inotify_add_watch(ifd,
        "/sys/fs/cgroup/myapp/cgroup.events",
        IN_MODIFY);

    struct pollfd fds = { .fd = ifd, .events = POLLIN };

    while (1) {
        int ret = poll(&fds, 1, -1);
        if (ret > 0) {
            /* cgroup 상태 변경 감지 */
            char buf[256];
            read(ifd, buf, sizeof(buf));
            /* cgroup.events 다시 읽기 */
            int fd = open("/sys/fs/cgroup/myapp/cgroup.events", O_RDONLY);
            ssize_t n = read(fd, buf, sizeof(buf) - 1);
            buf[n] = '\0';
            printf("Events: %s\n", buf);
            /* "populated 0" → cgroup 내 프로세스 없음 */
            /* "frozen 1"    → 프로세스 동결됨 */
            close(fd);
        }
    }
    return 0;
}

HugeTLB 컨트롤러

HugeTLB 컨트롤러는 cgroup별 대형 페이지(HugeTLB) 사용량을 제한합니다. 데이터베이스나 과학 연산 등 대형 메모리를 사용하는 워크로드에서 공정한 HugeTLB 분배를 보장합니다.

# HugeTLB 컨트롤러 파일 (페이지 크기별)
cat /sys/fs/cgroup/myapp/hugetlb.2MB.current
cat /sys/fs/cgroup/myapp/hugetlb.2MB.max
cat /sys/fs/cgroup/myapp/hugetlb.2MB.events
cat /sys/fs/cgroup/myapp/hugetlb.1GB.current
cat /sys/fs/cgroup/myapp/hugetlb.1GB.max

# 2MB HugeTLB 제한: 최대 512MB (256개 × 2MB)
echo 536870912 > /sys/fs/cgroup/myapp/hugetlb.2MB.max

# 1GB HugeTLB 제한: 최대 4GB
echo 4294967296 > /sys/fs/cgroup/myapp/hugetlb.1GB.max

# HugeTLB 통합 회계 (커널 6.1+, memory_hugetlb_accounting 마운트 옵션)
# HugeTLB 사용량이 memory.current에도 반영됨

Misc 컨트롤러

Misc 컨트롤러는 다른 컨트롤러에 속하지 않는 스칼라 리소스를 제한합니다. 대표적으로 SEV(AMD Secure Encrypted Virtualization) ASIDs와 Intel SGX EPC 페이지가 있습니다.

# Misc 컨트롤러 가용 리소스 확인
cat /sys/fs/cgroup/misc.capacity
# sev 509
# sev_es 509
# sgx_epc 65536

# cgroup별 Misc 제한
echo "sev 100" > /sys/fs/cgroup/vm-pool/misc.max
echo "sgx_epc 8192" > /sys/fs/cgroup/sgx-app/misc.max

# 현재 사용량
cat /sys/fs/cgroup/vm-pool/misc.current
# sev 42

cgroup 보안 강화

컨테이너 환경에서 cgroup 보안을 강화하기 위한 모범 사례를 정리합니다.

보안 조치설명설정 방법
v1 완전 비활성화v1의 release_agent 등 보안 취약점 제거cgroup_no_v1=all 부트 옵션
nsdelegatecgroup NS 경계에서 자동 delegationmount -o nsdelegate
memory_recursiveprotmemory.low/min 재귀적 보호mount -o memory_recursiveprot
PID 제한포크 폭탄 방지pids.max=4096
장치 BPF 필터컨테이너 장치 접근 제어BPF_CGROUP_DEVICE 프로그램
계층 깊이 제한무한 중첩 방지cgroup.max.depth=4
자손 수 제한cgroup 폭발 방지cgroup.max.descendants=256
메모리 하드 제한OOM 보호memory.max 필수 설정
네트워크 BPFcgroup별 네트워크 필터링BPF_CGROUP_INET_INGRESS/EGRESS
sysctl BPFcgroup별 sysctl 필터링BPF_CGROUP_SYSCTL
# 컨테이너 보안 강화: 권장 부트 옵션
GRUB_CMDLINE_LINUX="systemd.unified_cgroup_hierarchy=1 \
  cgroup_no_v1=all \
  systemd.legacy_systemd_cgroup_controller=0"

# cgroup 파일시스템 보안 마운트
mount -t cgroup2 none /sys/fs/cgroup \
  -o rw,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot

# 커널 설정 확인
grep CGROUP /boot/config-$(uname -r)
# CONFIG_CGROUPS=y
# CONFIG_CGROUP_V1=n          ← v1 비활성화 (최신 커널)
# CONFIG_BLK_CGROUP=y
# CONFIG_CGROUP_WRITEBACK=y   ← buffered I/O 제어
# CONFIG_CGROUP_SCHED=y
# CONFIG_CGROUP_PIDS=y
# CONFIG_CGROUP_BPF=y
# CONFIG_MEMCG=y
# CONFIG_PSI=y
보안 체크리스트:
  • 프로덕션 환경에서 cgroup v1은 반드시 비활성화하세요 (release_agent 취약점)
  • 모든 컨테이너에 memory.maxpids.max를 설정하세요
  • nsdelegate 마운트 옵션을 사용하여 delegation 경계를 강제하세요
  • root가 아닌 사용자에게 cgroup을 위임할 때는 subtree_controlcgroup.procs의 쓰기 권한만 부여하세요

프로덕션 설정 모범 사례

대규모 프로덕션 환경에서 cgroup을 효과적으로 운용하기 위한 모범 사례를 정리합니다.

계층 설계 원칙

# 권장 cgroup 계층 설계 (3단계)
/sys/fs/cgroup/
├── system.slice/              # 시스템 서비스
│   ├── nginx.service/         # 웹 서버
│   ├── postgresql.service/    # 데이터베이스
│   └── redis.service/         # 캐시
├── user.slice/                # 사용자 세션
│   ├── user-1000.slice/       # 사용자별 격리
│   └── user-1001.slice/
├── machine.slice/             # VM/컨테이너
│   ├── docker-abc.scope/      # 컨테이너별 격리
│   └── docker-def.scope/
└── batch.slice/               # 배치 작업 (낮은 우선순위)
    ├── etl.service/
    └── backup.service/

# 슬라이스별 리소스 배분 (systemd)
# /etc/systemd/system/system.slice
[Slice]
CPUWeight=500       # 시스템 서비스: 높은 가중치
MemoryHigh=60%      # 전체 메모리의 60% (비율 지원, systemd 254+)

# /etc/systemd/system/batch.slice
[Slice]
CPUWeight=10        # 배치: 낮은 가중치
MemoryHigh=20%
IOWeight=10

모니터링 구성

# Prometheus node_exporter cgroup 메트릭 활성화
# --collector.cgroup (기본 활성화)
# 수집 메트릭:
# node_cgroup_memory_usage_bytes
# node_cgroup_cpu_usage_seconds_total
# node_cgroup_memory_pressure_some
# node_cgroup_memory_pressure_full

# Grafana 대시보드 PromQL 예시
# CPU 스로틀링 비율:
# rate(container_cpu_cfs_throttled_periods_total[5m])
#   / rate(container_cpu_cfs_periods_total[5m]) * 100

# 메모리 사용률:
# container_memory_usage_bytes / container_spec_memory_limit_bytes * 100

# PSI 메모리 압력 (cAdvisor):
# container_memory_pressure_some_total

# 알림 규칙 예시 (Prometheus AlertManager)
# - alert: HighMemoryPressure
#   expr: container_memory_pressure_some > 25
#   for: 5m
#   labels: { severity: warning }
#
# - alert: CpuThrottlingHigh
#   expr: rate(container_cpu_cfs_throttled_periods_total[5m])
#         / rate(container_cpu_cfs_periods_total[5m]) > 0.25
#   for: 5m
#   labels: { severity: warning }

용량 계획 지침

워크로드 유형CPU 설정메모리 설정I/O 설정비고
웹 서버 (Nginx, Apache)CPUWeight=300, CPUQuota=400%MemoryMax=4G, MemoryHigh=3G, MemoryLow=1GIOWeight=500응답 시간 보장 우선
API 서버 (Spring, Django)CPUWeight=300, CPUQuota=800%MemoryMax=8G, MemoryHigh=6G, MemoryMin=2GIOWeight=300GC 고려 메모리 여유
데이터베이스 (PostgreSQL)CPUWeight=500, AllowedCPUs=4-15MemoryMax=32G, MemoryLow=16G, MemoryMin=8GIOWeight=900메모리 보호 최우선
캐시 (Redis)CPUWeight=200MemoryMax=16G, MemorySwapMax=0IOWeight=200스왑 금지, 메모리만
메시지 큐 (Kafka)CPUWeight=300MemoryMax=12G, MemoryHigh=10GIOWeight=800I/O 집약적
배치/ETLCPUWeight=10, cpu.idle=1MemoryMax=8GIOWeight=10최저 우선순위
CI/CD 빌드CPUQuota=400%, burst=200msMemoryMax=16G, MemoryHigh=12GIOWeight=200스파이크 허용
ML 학습CPUWeight=200, AllowedCPUs=16-31MemoryMax=64G, MemoryMin=32GIOWeight=500GPU cgroup 별도
💡

메모리 설정 경험 법칙:

  • memory.max: 애플리케이션 정상 사용량의 1.5~2배
  • memory.high: memory.max75~80% (스로틀링 시작점)
  • memory.low: 워킹 셋 크기 (글로벌 압력에서도 보호할 양)
  • memory.min: 절대 회수 불가 최소량 (핵심 서비스에만)

오버커밋과 QoS 전략

물리 리소스보다 더 많은 리소스를 할당(오버커밋)하되, 우선순위를 통해 경쟁 상황을 관리하는 전략입니다:

# 예: 8 CPU 코어, 32GB RAM 서버

# Tier 1: 보장 (Guaranteed) — 리소스 합: 6 CPU, 24GB
# → memory.min으로 하드 보호
echo 8G > /sys/fs/cgroup/tier1-db/memory.min
echo 16G > /sys/fs/cgroup/tier1-db/memory.max
echo 500 > /sys/fs/cgroup/tier1-db/cpu.weight

echo 4G > /sys/fs/cgroup/tier1-web/memory.min
echo 8G > /sys/fs/cgroup/tier1-web/memory.max
echo 300 > /sys/fs/cgroup/tier1-web/cpu.weight

# Tier 2: 탄력적 (Burstable) — 리소스 합: 4 CPU, 16GB
# → memory.low로 소프트 보호, 여유 시 burst
echo 4G > /sys/fs/cgroup/tier2-api/memory.low
echo 12G > /sys/fs/cgroup/tier2-api/memory.max
echo 200 > /sys/fs/cgroup/tier2-api/cpu.weight

# Tier 3: 최선 (BestEffort) — 보호 없음
# → 남는 리소스만 사용
echo 8G > /sys/fs/cgroup/tier3-batch/memory.max
echo 10 > /sys/fs/cgroup/tier3-batch/cpu.weight
echo 1 > /sys/fs/cgroup/tier3-batch/cpu.idle

# 전체 합: CPU weight 총합 = 1010, Memory min 총합 = 12GB < 32GB ✓
# 오버커밋 비율: Memory max 총합 = 44GB / 32GB = 1.375x
# → memory.min/low 보호로 tier1이 먼저 보장됨

cgroup 관련 커널 파라미터

파라미터경로기본값설명
vm.dirty_ratio/proc/sys/vm/dirty_ratio20시스템 전체 dirty 비율 (cgroup io와 별도)
vm.dirty_background_ratio/proc/sys/vm/dirty_background_ratio10백그라운드 writeback 시작 비율
vm.swappiness/proc/sys/vm/swappiness60글로벌 swappiness (cgroup별 오버라이드 가능)
vm.min_free_kbytes/proc/sys/vm/min_free_kbytes자동최소 여유 메모리 (cgroup reclaim에 영향)
kernel.pid_max/proc/sys/kernel/pid_max4194304시스템 전체 PID 최대값
kernel.threads-max/proc/sys/kernel/threads-max자동시스템 전체 스레드 최대 수

참고 자료 및 출처

자료설명링크
커널 cgroup v2 문서공식 커널 문서 (admin-guide)docs.kernel.org
cgroup v1 문서v1 레거시 문서docs.kernel.org/cgroup-v1
PSI 문서Pressure Stall Information 상세docs.kernel.org/psi
systemd Resource Controlsystemd 리소스 관리 가이드freedesktop.org
커널 소스: mm/memcontrol.c메모리 컨트롤러 구현mm/memcontrol.c
커널 소스: kernel/sched/fair.cCFS 대역폭 제어 구현kernel/sched/fair.c
커널 소스: kernel/cgroup/cgroup.ccgroup 코어 구현kernel/cgroup/cgroup.c
커널 소스: kernel/sched/psi.cPSI 구현kernel/sched/psi.c
LWN: Control groups v2v2 설계 해설 기사LWN.net
LWN: PSIPSI 도입 배경 해설LWN.net
Facebook oomduserspace OOM daemonGitHub

cgroup 발전 로드맵

cgroup 서브시스템은 리눅스 커널에서 가장 활발하게 발전하는 영역 중 하나입니다. 최신 커널에서 추가되었거나 진행 중인 주요 변화를 정리합니다.

커널 버전기능영향
5.14cgroup.killcgroup 내 전체 프로세스에 SIGKILL 일괄 전송
5.15memcg LRU reparenting 개선dying cgroup 해제 속도 향상
5.18io.stat에 dbytes/dios 추가discard I/O 통계 추적
5.19memory.reclaim능동적 메모리 회수 인터페이스
6.0cgroup v1 비활성화 옵션 개선CONFIG_CGROUP_V1=n 빌드 옵션
6.1memory.reclaim swappiness 파라미터회수 시 anon/file 비율 제어
6.1PSI cgroup 트리거 개선per-cgroup PSI 정밀도 향상
6.2cpuset 파티션 isolated 모드커널 스레드도 제외하는 완전 격리
6.3memory.peak 리셋 지원peak 사용량 초기화 가능
6.5memory.zswap.maxcgroup별 zswap 풀 크기 제한
6.5memory_hugetlb_accountingHugeTLB을 memory 카운터에 통합
6.6pids.peak최고 PID 사용량 기록
6.7memcg 통계 정밀도 개선per-CPU 카운터 집계 최적화
6.8memory.swap.peak스왑 최고 사용량 기록
6.9cpuset.cpus.exclusive전용 CPU 지정 인터페이스 안정화
6.10memory.reclaim recursive하위 cgroup 포함 재귀적 회수
6.12sched_ext (BPF 스케줄러)cgroup 단위 BPF 스케줄링 정책
미래 전망: cgroup v2는 지속적으로 발전하고 있으며, 특히 다음 영역에서 활발한 개발이 진행 중입니다:
  • sched_ext: BPF 기반 커스텀 스케줄러로 cgroup 단위 스케줄링 정책을 사용자가 정의
  • 메모리 티어링: CXL 메모리와 연동한 cgroup별 메모리 티어 배치 정책
  • GPU cgroup: GPU/가속기 리소스를 cgroup으로 제어 (DRM 서브시스템 연동)
  • 네트워크 QoS: BPF + EDT(Earliest Departure Time) 기반 cgroup별 네트워크 대역폭 보장

cgroup v1 폐기 일정

cgroup v1은 점진적으로 폐기(deprecation) 경로에 있습니다:

시점상태영향
커널 6.0CONFIG_CGROUP_V1=n 빌드 옵션 추가배포판이 v1을 선택적으로 제거 가능
커널 6.x+v1 컨트롤러에 새 기능 추가 중단모든 새 기능은 v2에만 구현
Android 12+cgroup v2 필수v1 지원 장치는 인증 불가
Fedora 31+기본 v2v1 하이브리드 모드 가능하지만 비권장
RHEL 10 (예정)v1 제거 예정cgroup v2만 지원
마이그레이션 시급성: cgroup v1에 의존하는 도구나 스크립트가 있다면 가능한 빨리 v2로 마이그레이션하세요. v1은 보안 패치만 적용되며 새로운 기능(PSI, memory.reclaim, cpu.max.burst 등)은 v2에서만 사용할 수 있습니다. 특히 release_agent 취약점(CVE-2022-0492)은 v1에서만 발생하므로 보안 측면에서도 v2 전환이 중요합니다.

cgroup 용어 사전

용어설명
cgroupControl Group의 약어. 프로세스 그룹의 리소스 사용을 제한·계량·격리하는 커널 메커니즘
css (cgroup_subsys_state)각 컨트롤러의 cgroup당 상태를 나타내는 구조체. mem_cgroup, task_group 등이 이를 상속
css_set프로세스가 속한 모든 cgroup 컨트롤러 상태의 집합. task_struct→cgroups가 이를 가리킴
subsyscgroup 서브시스템(=컨트롤러). cpu, memory, io, pids 등
slicesystemd의 cgroup 계층 단위. 여러 service/scope를 그룹화
scopesystemd가 외부 프로세스를 위해 런타임에 생성하는 cgroup 단위
delegation비특권 사용자에게 cgroup 하위 트리의 관리 권한을 안전하게 위임하는 메커니즘
charge/uncharge메모리 페이지를 cgroup에 과금/환급하는 메커니즘
PSIPressure Stall Information. 리소스 대기로 인한 stall 시간을 측정하는 지표
throttlingcpu.max 제한 초과 시 CPU 사용을 일시 중단하는 것
direct reclaim메모리 할당 경로에서 동기적으로 페이지를 회수하는 것. memory.high 초과 시 발생
dying cgroup삭제되었지만 page cache 참조로 인해 완전히 해제되지 않은 cgroup
No Internal Process Constraint자식 cgroup이 존재하는 cgroup에 프로세스를 직접 배치할 수 없는 v2 규칙
UCLAMPUtilization Clamping. 스케줄러에게 CPU 활용도 상/하한 힌트를 제공하여 주파수 결정에 영향
io.cost장치의 실제 성능 특성을 모델링하여 정밀한 가중치 기반 I/O 분배를 제공하는 cgroup v2 IO 제어 모델
nsdelegatecgroup NS 경계에서 자동으로 delegation을 적용하는 마운트 옵션
memory_recursiveprotmemory.low/min 보호를 재귀적으로 적용하는 마운트 옵션

cgroup 테스트 프레임워크

커널 cgroup 기능을 테스트하기 위한 도구와 프레임워크를 정리합니다.

# 1. 커널 셀프 테스트: tools/testing/selftests/cgroup/
cd /usr/src/linux/tools/testing/selftests/cgroup
make
sudo ./test_memcontrol     # 메모리 컨트롤러 테스트
sudo ./test_cpu            # CPU 컨트롤러 테스트
sudo ./test_pids           # PID 컨트롤러 테스트
sudo ./test_freezer        # Freezer 테스트
sudo ./test_kill           # cgroup.kill 테스트

# 2. LTP (Linux Test Project) cgroup 테스트
# https://github.com/linux-test-project/ltp
cd ltp
./configure && make
sudo ./runltp -f controllers

# 3. stress-ng로 cgroup 제한 검증
# CPU 제한 검증
systemd-run --scope -p CPUQuota=100% -- \
  stress-ng --cpu 4 --timeout 30s --metrics-brief
# → cpu-used 합계가 ~100%인지 확인

# 메모리 제한 검증
systemd-run --scope -p MemoryMax=512M -- \
  stress-ng --vm 2 --vm-bytes 256M --timeout 30s
# → memory.current가 512M 이하인지 확인

# 4. cgroup-v2-test (커뮤니티 도구)
# PSI 트리거 정확도 테스트
# 메모리 회수 경로 테스트
# OOM 동작 테스트
# delegation 권한 테스트

# 5. bpftrace 기반 cgroup 이벤트 검증
sudo bpftrace -e '
tracepoint:cgroup:cgroup_mkdir { printf("mkdir: %s\n", args->path); }
tracepoint:cgroup:cgroup_rmdir { printf("rmdir: %s\n", args->path); }
tracepoint:cgroup:cgroup_attach_task { printf("attach PID %d → %s\n", args->pid, args->dst_path); }
tracepoint:oom:oom_score_adj_update { printf("oom_adj PID %d → %d\n", args->pid, args->oom_score_adj); }
'
테스트 영역도구테스트 내용
메모리 제한stress-ng --vmmemory.max, memory.high 동작 확인
CPU 제한stress-ng --cpucpu.max 스로틀링, cpu.weight 비례 배분
I/O 제한fio, ddio.max 대역폭 제한, io.latency 동작
PID 제한포크 테스트pids.max 초과 시 EAGAIN 확인
Freezersignal 테스트cgroup.freeze 동결/해제 확인
OOMmemory exhaustionoom_kill, oom.group 동작 확인
PSIpoll triggerPSI 트리거 정확도, 지연 시간
Delegation권한 테스트비특권 사용자 cgroup 관리 확인
보안탈출 테스트delegation 경계 침범 불가 확인

자주 묻는 질문 (FAQ)

Q: memory.max와 memory.high 중 어떤 것을 먼저 설정해야 하나요?

memory.high을 먼저 설정하는 것이 권장됩니다. memory.high는 초과 시 스로틀링으로 점진적으로 메모리를 회수하므로 OOM을 예방합니다. memory.max는 최후의 안전장치로 설정합니다. 권장 비율: memory.high = memory.max × 0.8

Q: 컨테이너에서 free 명령어의 출력이 호스트 전체 메모리를 보여줍니다. 왜 그런가요?

free 명령은 /proc/meminfo를 읽지만, 이 파일은 cgroup을 인식하지 않습니다. 컨테이너 내에서 정확한 메모리 정보를 얻으려면 /sys/fs/cgroup/memory.currentmemory.max를 읽어야 합니다. LXCFS나 cgroup-aware /proc 에뮬레이션을 사용하면 free 등 기존 도구에서도 cgroup 제한을 반영합니다.

Q: cpu.max를 설정했는데 프로세스가 설정보다 더 많은 CPU를 사용합니다. 원인은?

몇 가지 원인이 있습니다:

  • cpu.max.burst가 설정되어 있으면 미사용 quota를 누적하여 순간적으로 초과 가능
  • 측정 도구가 정확하지 않을 수 있음 (cpu.statusage_usec으로 정확히 확인)
  • 커널 스레드 시간은 일부 CPU 계정에 포함되지 않을 수 있음
  • period가 너무 길면(예: 1초) period 내에서 순간적으로 높은 사용률이 관찰될 수 있음
Q: dying cgroup이 계속 증가합니다. 어떻게 해결하나요?

dying cgroup은 cgroup이 삭제되었지만 page cache나 slab 참조로 인해 완전히 해제되지 않은 상태입니다. 해결 방법:

  • echo 3 > /proc/sys/vm/drop_caches로 page cache 드롭
  • 해당 cgroup의 memory.reclaim에 큰 값을 쓰기
  • 컨테이너 런타임의 cgroup 정리 주기 확인 (containerd, Docker)
  • 커널 6.1+에서는 자동 정리가 개선됨
Q: PSI avg10이 50%입니다. 심각한 상황인가요?

some avg10=50%는 지난 10초 동안 50%의 시간에서 최소 1개 이상의 태스크가 리소스를 기다렸다는 의미입니다. 해석:

  • some 10% 이상: 리소스 병목이 감지되고 있음, 주의 필요
  • some 25% 이상: 심각한 리소스 부족, 성능 저하 가시적
  • full 10% 이상: 워크로드가 완전히 정체, 즉각 대응 필요
  • 참고: CPU에는 full 압력이 없으므로 some만 확인
Q: cgroup v1에서 v2로 어떻게 안전하게 마이그레이션하나요?

단계별 접근을 권장합니다:

  1. 하이브리드 모드 테스트: systemd.unified_cgroup_hierarchy=1만 설정하여 systemd는 v2, 일부 컨트롤러는 v1으로 운영
  2. 컨테이너 런타임 업그레이드: Docker 20.10+, containerd 1.4+, Podman 3.0+ 확인
  3. 인터페이스 매핑: v1 설정을 v2 equivalent로 변환 (cpu.shares→cpu.weight 등)
  4. 테스트 환경 검증: 워크로드별 성능/격리 테스트
  5. v1 비활성화: cgroup_no_v1=all로 완전 전환
Q: Kubernetes에서 cgroup v2를 사용하면 어떤 이점이 있나요?

Kubernetes 1.25+에서 cgroup v2 사용 시 이점:

  • MemoryQoS: memory.high 기반 메모리 스로틀링으로 OOM 감소
  • PSI 기반 eviction: 메모리 압력 기반 정밀한 Pod eviction
  • Swap 지원: cgroup v2의 memory.swap으로 Pod별 스왑 관리
  • 통합 I/O 제어: buffered I/O 포함 정확한 I/O 제한
  • 보안 강화: BPF 기반 디바이스 제어, nsdelegate
Q: cgroup의 memory.high를 설정했는데도 OOM이 발생합니다. 왜 그런가요?

memory.high는 best-effort 메커니즘이므로 완전한 보호를 제공하지 않습니다. 다음 상황에서 OOM이 발생할 수 있습니다:

  • 빠른 메모리 할당: 프로세스가 한 번에 대량의 메모리를 할당하면 reclaim이 따라가지 못함
  • 회수 불가능한 메모리: mlock()된 페이지나 kernel slab은 high 스로틀링의 reclaim 대상이 아님
  • memory.max 미설정: memory.max가 "max" (무제한)이면 시스템 전체 OOM이 발생
  • swap 미구성: swap이 없으면 anonymous 메모리를 회수할 수 없어 OOM 가능성 증가

권장: memory.highmemory.max를 함께 설정하고, memory.high = memory.max × 0.8 비율을 유지합니다.

Q: io.latency와 io.cost 중 어떤 것을 사용해야 하나요?

두 메커니즘은 서로 다른 목적에 최적화되어 있습니다:

  • io.latency: 지연 시간 보장이 중요한 워크로드에 적합 (데이터베이스, 실시간 서비스). 대상 지연 시간을 설정하면 커널이 자동으로 다른 cgroup의 I/O를 조절합니다.
  • io.cost: 비례적 대역폭 분배가 필요한 경우에 적합 (다중 테넌트 환경). 가중치 기반으로 I/O 자원을 분배합니다.

일반 규칙: 단일 중요 서비스를 보호하려면 io.latency, 여러 서비스 간 공정한 분배가 목적이면 io.cost를 사용합니다. 두 메커니즘은 동시에 사용할 수 없습니다.

cgroups와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.