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 v2 — 단일 통합 계층으로 정책 일관성 강화
- cpu.max — quota/period 기반 CPU 상한
- memory.max — 하드 메모리 제한
- PSI — 자원 압력 기반 관측 지표
- Delegation — 하위 트리에 안전하게 권한 위임
단계별 이해
- 트리 구조 이해
부모-자식 누적 제한 원리를 먼저 이해합니다. - CPU/Memory 제한 적용
cpu.max,memory.max를 순서대로 적용합니다. - 관측 지표 확인
memory.events,cpu.stat, PSI를 점검합니다. - 운영 정책 고도화
위임 모델과 systemd 연동으로 정책 자동화를 구성합니다.
빠른 시작: 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 라이브러리 | 컨테이너 인식 | 비고 |
|---|---|---|---|
| C | libcgroup, 직접 파일 I/O | 수동 | 커널 API 직접 사용 |
| Go | containerd/cgroups/v3 | 자동 (1.19+) | GOMAXPROCS 자동 조정 |
| Rust | cgroups-rs | 수동 | 타입 안전 cgroup 관리 |
| Python | cgroupspy | 수동 | os.cpu_count() 미인식 |
| Java | JVM 내장 | 자동 (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 등에서 리소스 관리의 기반으로 사용됩니다.
리소스 제어 원리
cgroups의 핵심 설계 원리는 "커널의 리소스 할당 경로에 cgroup 검사를 삽입한다"는 것입니다. 프로세스가 리소스(메모리, CPU 시간, I/O 대역폭)를 요청할 때마다 커널은 해당 프로세스가 속한 cgroup의 제한을 확인합니다:
CPU 제어 원리
CPU 컨트롤러는 CFS(Completely Fair Scheduler) 스케줄러에 통합되어 동작합니다. 두 가지 메커니즘이 있습니다:
- 가중치(weight): 경쟁 상황에서 CPU 시간의 비례 배분.
cpu.weight=200인 그룹은cpu.weight=100인 그룹보다 2배의 CPU 시간을 받습니다. CPU가 여유로우면 제한 없이 사용 가능합니다. - 대역폭 제한(bandwidth throttling):
cpu.max의 quota/period 방식. period(예: 100ms) 동안 quota(예: 50ms)만큼만 실행을 허용하고, 초과하면 해당 period가 끝날 때까지 스로틀링합니다. 내부적으로 hrtimer가 period 경계를 관리합니다.
메모리 제어 원리
메모리 컨트롤러는 charge/uncharge 메커니즘으로 동작합니다:
- 프로세스가 페이지를 할당하면, 해당 페이지의 비용을 프로세스의 cgroup에 charge(부과)합니다.
- 페이지가 해제되면 uncharge(환급)합니다.
- cgroup의 누적 사용량이
memory.max에 도달하면, 커널은 해당 cgroup 내에서 페이지 회수를 시도합니다. - 회수로도 부족하면 OOM killer가 해당 cgroup 내의 프로세스를 종료합니다.
계층 구조와 리소스 누적 원리
cgroup v2의 계층 구조에서 리소스 제한은 부모가 자식을 포함합니다:
- 자식 cgroup의 리소스 사용량은 항상 부모에 누적됩니다. 부모의
memory.max가 1GB이면, 모든 자식의 합산 메모리가 1GB를 초과할 수 없습니다. - 자식은 부모보다 더 높은 제한을 설정할 수 있지만, 실질적으로는 부모의 제한이 먼저 적용됩니다.
memory.low는 리소스 보호를 위한 최소 보장으로, 글로벌 메모리 회수 시 이 값 이하의 메모리는 보호됩니다.
cgroup v2 설계 원칙: v1에서는 컨트롤러별로 독립된 계층이 존재하여 CPU cgroup과 메모리 cgroup의 트리 구조가 달라 정책 불일치가 발생했습니다. v2는 단일 통합 계층으로 모든 컨트롤러가 같은 트리를 공유하여 일관된 리소스 정책을 보장합니다.
cgroup v1 vs v2
cgroup v1과 v2는 설계 철학, 보안 모델, 기능 범위에서 근본적으로 다릅니다. v2는 v1의 설계 결함을 해결하기 위해 완전히 새로 설계되었으며, 커널 4.5에서 정식 릴리스되었습니다.
| 특성 | cgroup v1 | cgroup 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 모델 | 단순 throttle | io.latency + io.cost 모델 |
cgroup_no_v1=all 부트 옵션을 사용하세요.
cgroup v2 채택 현황
| 배포판/플랫폼 | cgroup v2 기본 버전 | 비고 |
|---|---|---|
| Fedora | 31+ (2019) | 최초 기본 v2 배포판 |
| Ubuntu | 21.10+ | 22.04 LTS에서 안정화 |
| Debian | 11 (bullseye) | 하이브리드 모드 가능 |
| RHEL/CentOS | 9+ | 8.x는 수동 활성화 |
| Arch Linux | 2021+ | 기본 v2 |
| Android | 12+ | cgroup v2 필수 |
| Docker | 20.10+ | systemd 드라이버 권장 |
| Kubernetes | 1.25+ | GA 지원 |
| containerd | 1.4+ | v2 지원 |
| Podman | 3.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
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는 메모리 컨트롤러의 핵심 관측 포인트입니다. 각 이벤트 카운터의 의미를 정확히 이해해야 효과적인 모니터링이 가능합니다:
| 카운터 | 트리거 조건 | 의미 | 대응 방법 |
|---|---|---|---|
low | memory.low 보호 활성화 | 글로벌 회수에서 이 cgroup이 보호됨 | 정상 동작, 보호가 작동 중 |
high | memory.high 초과 | 직접 회수(direct reclaim) 스로틀링 시작 | memory.high 상향 검토 |
max | memory.max 도달 | 할당 실패 직전, 강제 회수 시도 | 메모리 부족 경고 |
oom | OOM 상황 진입 | 회수 실패, OOM 핸들링 시작 | 즉시 대응 필요 |
oom_kill | OOM kill 실행 | 실제 프로세스 종료 발생 | 워크로드 분석, 제한 조정 |
oom_group_kill | oom.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 분석 |
slab | slab 할당자 메모리 | dentry/inode 캐시가 주요 사용처 |
sock | TCP/UDP 소켓 버퍼 | 네트워크 워크로드에서 중요 |
shmem | 공유 메모리 + tmpfs | IPC 사용 워크로드에서 확인 |
zswap | zswap 압축 메모리 | 스왑 대역폭 절약 효과 확인 |
zswapped | zswap으로 압축 전 원본 크기 | 압축률 = zswapped/zswap |
file_mapped | mmap으로 매핑된 파일 메모리 | 공유 라이브러리, 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_alloc | THP(Transparent Huge Page) 할당 성공 | THP 활용도 |
thp_collapse_alloc | THP 병합 성공 | 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의 핵심 원리는 "일할 수 있는데 리소스를 기다리느라 못 하는 시간의 비율"을 측정하는 것입니다:
- some: 최소 하나 이상의 태스크가 해당 리소스를 기다리는 시간 비율 (부분적 지연)
- full: 모든 태스크가 해당 리소스를 기다리는 시간 비율 (완전 정체). CPU에는 full이 없습니다.
커널은 각 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();
}
}
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 미인식 (수동 계산 필요)
-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.24 | cgroup v1 도입 |
| 3.16 | cgroup v2 통합 계층 초기 구현 |
| 4.5 | cgroup v2 정식 릴리스 |
| 4.20 | PSI (Pressure Stall Information) 도입 |
| 5.0 | cgroup v2 cpuset 컨트롤러 |
| 5.2 | cgroup v2 freezer 통합 |
| 5.7 | cgroup v1의 cpuacct를 cpu에 통합 (v2) |
| 6.1 | PSI per-cgroup 트리거 개선 |
참고 자료: 커널 cgroup v2 문서, Facebook PSI 문서
cgroup 관련 주요 취약점
cgroup은 컨테이너 리소스 격리의 핵심 메커니즘이지만, 특히 v1의 설계에서 보안 취약점이 반복적으로 발견되었습니다. cgroup v2로의 전환이 이러한 문제의 근본적 해결을 위해 가속화되고 있습니다.
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) 검사 추가 */
cgroup v1의 파일 디스크립터 처리에서 cgroup_get_from_fd()가 cgroup에 대한 참조를 적절히 관리하지 않아, 해제된 cgroup 구조체에 접근하는 Use-After-Free가 발생합니다. 이를 통해 로컬 권한 상승이 가능합니다.
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는 이를 하나의 트리로 통합하여 정책 일관성을 보장합니다.
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.type | R/W | cgroup 유형: domain, domain threaded, domain invalid, threaded |
cgroup.procs | R/W | 소속 프로세스 PID 목록 / PID 쓰기로 프로세스 이동 |
cgroup.threads | R/W | 소속 스레드 TID 목록 (threaded cgroup에서 사용) |
cgroup.controllers | R | 이 cgroup에서 사용 가능한 컨트롤러 목록 |
cgroup.subtree_control | R/W | 자식 cgroup에서 활성화할 컨트롤러 (+cpu -memory 형식) |
cgroup.events | R | populated/frozen 상태 모니터링 (inotify/poll 가능) |
cgroup.max.descendants | R/W | 허용되는 최대 자손 cgroup 수 |
cgroup.max.depth | R/W | 허용되는 최대 계층 깊이 |
cgroup.stat | R | nr_descendants, nr_dying_descendants 통계 |
cgroup.freeze | R/W | 1 쓰기로 모든 자손 프로세스 동결 |
cgroup.kill | W | 1 쓰기로 모든 자손 프로세스에 SIGKILL |
cgroup.pressure | R/W | PSI 모니터링 활성화/비활성화 (0/1) |
Memory 컨트롤러 심화
cgroup v2 메모리 컨트롤러는 사용자 메모리(anon, file cache, tmpfs), 커널 메모리(slab, page tables, sock buffers), 스왑을 통합 관리합니다. v1과 달리 커널 메모리가 별도가 아닌 통합 계정으로 처리되어 제한 우회가 불가능합니다.
Memory 인터페이스 파일 상세
| 파일 | 유형 | 설명 | 기본값 |
|---|---|---|---|
memory.current | R | 현재 메모리 사용량 (바이트) | - |
memory.min | R/W | 하드 메모리 보호 하한 | 0 |
memory.low | R/W | 소프트 메모리 보호 하한 (best-effort) | 0 |
memory.high | R/W | 메모리 스로틀링 임계값 | max |
memory.max | R/W | 메모리 하드 제한 (초과 시 OOM) | max |
memory.reclaim | W | 능동적 메모리 회수 트리거 (바이트 지정) | - |
memory.peak | R | 최고 메모리 사용량 기록 | - |
memory.oom.group | R/W | OOM 시 그룹 전체 종료 여부 (0/1) | 0 |
memory.swap.current | R | 현재 스왑 사용량 | - |
memory.swap.max | R/W | 스왑 하드 제한 | max |
memory.swap.high | R/W | 스왑 스로틀링 임계값 | max |
memory.zswap.current | R | zswap 사용량 | - |
memory.zswap.max | R/W | zswap 제한 | max |
memory.pressure | R | PSI 메모리 압력 (some/full) | - |
memory.numa_stat | R | NUMA 노드별 메모리 통계 | - |
memory.stat | R | 상세 메모리 통계 (anon, file, slab 등) | - |
memory.events | R | 이벤트 카운터 (low, high, max, oom, oom_kill, oom_group_kill) | - |
memory.events.local | R | 해당 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 ← 메이저 페이지 폴트
- 사용자 공간 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-12
charge_memcg()는 folio(페이지 묶음)를 cgroup에 과금합니다.try_charge()는 해당 memcg뿐만 아니라 모든 조상 cgroup의 카운터도 함께 증가시킵니다 (page_counter의 parent 체인 순회). - L14-15
memory.high초과 시 OOM 대신 비동기 회수 작업을 스케줄합니다. 프로세스는schedule_work()를 통해 점진적으로 감속됩니다. - L18
folio->memcg_data에 cgroup 포인터를 저장하여 나중에 uncharge 시 어떤 cgroup에서 차감할지 추적합니다. - L22-27페이지 해제 시
uncharge_folio()가 카운터를 감소시킵니다. 이 역시 부모 체인을 따라 올라가며 모든 조상의 사용량을 함께 감소시킵니다.
CPU 컨트롤러 심화
cgroup v2 CPU 컨트롤러는 CFS 스케줄러와 긴밀히 통합되어 가중치 기반 비례 배분과 대역폭 제한을 동시에 제공합니다. 여기서는 내부 메커니즘을 깊이 분석합니다.
대역폭 스로틀링 내부 구조
CPU 대역폭 스로틀링(cpu.max)은 커널 내부에서 CFS bandwidth control로 구현됩니다. 핵심 구조체는 struct cfs_bandwidth이며, period 타이머와 quota 관리를 담당합니다:
/* 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.min | 0.00 ~ 100.00 | 최소 활용도 보장 → 주파수 하한 결정 (부스트 효과) |
cpu.uclamp.max | 0.00 ~ 100.00 | 최대 활용도 제한 → 주파수 상한 결정 (전력 절약) |
cpu.idle | 0 / 1 | 1 설정 시 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)까지 포함하여 제한합니다.
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: 최대 가중치 비율 (%)
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으로 실패
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가 어떻게 계산되는지 상세히 분석합니다.
/* 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의 워크로드에 영향을 주지 않습니다.
# 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 리소스 제어 지시어
| systemd 지시어 | cgroup v2 파일 | 설명 | 예시 |
|---|---|---|---|
MemoryMax | memory.max | 메모리 하드 제한 | MemoryMax=2G |
MemoryHigh | memory.high | 메모리 스로틀링 임계값 | MemoryHigh=1G |
MemoryLow | memory.low | 메모리 소프트 보호 | MemoryLow=512M |
MemoryMin | memory.min | 메모리 하드 보호 | MemoryMin=256M |
MemorySwapMax | memory.swap.max | 스왑 제한 | MemorySwapMax=1G |
CPUQuota | cpu.max | CPU 대역폭 제한 | CPUQuota=150% |
CPUWeight | cpu.weight | CPU 가중치 | CPUWeight=200 |
IOWeight | io.weight | I/O 가중치 | IOWeight=500 |
IODeviceWeight | io.weight | 디바이스별 I/O 가중치 | IODeviceWeight=/dev/sda 200 |
IOReadBandwidthMax | io.max (rbps) | 읽기 대역폭 제한 | IOReadBandwidthMax=/dev/sda 100M |
IOWriteBandwidthMax | io.max (wbps) | 쓰기 대역폭 제한 | IOWriteBandwidthMax=/dev/sda 50M |
TasksMax | pids.max | 최대 프로세스/스레드 수 | TasksMax=512 |
Delegate | 위임 활성화 | 하위 트리 위임 | Delegate=cpu memory io |
AllowedCPUs | cpuset.cpus | 허용 CPU 목록 | AllowedCPUs=0-3 |
AllowedMemoryNodes | cpuset.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의 모든 프로세스에 자동으로 적용됩니다.
# 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로 전환하는 것이 권장됩니다.
v1 → v2 인터페이스 매핑
| cgroup v1 인터페이스 | cgroup v2 인터페이스 | 변경 사항 |
|---|---|---|
memory.limit_in_bytes | memory.max | 동일 기능 |
memory.soft_limit_in_bytes | memory.high | v2는 스로틀링 추가 |
| (없음) | memory.low, memory.min | v2에서 새로 추가된 보호 인터페이스 |
memory.memsw.limit_in_bytes | memory.swap.max | v2는 스왑만 별도 제한 |
memory.kmem.limit_in_bytes | (통합) | v2는 커널 메모리 통합 계정 |
cpu.cfs_quota_us / cpu.cfs_period_us | cpu.max | "quota period" 형식으로 통합 |
cpu.shares | cpu.weight | 범위 변경: 2-262144 → 1-10000 |
blkio.throttle.read_bps_device | io.max | rbps/wbps/riops/wiops 통합 |
blkio.weight | io.weight | 범위 변경: 10-1000 → 1-10000 |
devices.allow / devices.deny | BPF_CGROUP_DEVICE | BPF 프로그램으로 대체 |
net_cls.classid | BPF_CGROUP_INET_* | BPF 기반 네트워크 제어로 대체 |
| (없음) | cpu.pressure, memory.pressure, io.pressure | v2에서 PSI 추가 |
release_agent | cgroup.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 cgroup | threaded cgroup |
|---|---|---|
| 이동 단위 | 프로세스 (PID) | 스레드 (TID) |
| 프로세스 파일 | cgroup.procs | cgroup.threads |
| 지원 컨트롤러 | 전체 | cpu, cpuset, perf_event, pids |
| 리소스 도메인 | 자체 | 부모 domain의 리소스 도메인 공유 |
| 사용 사례 | 프로세스 단위 격리 | 스레드풀 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 마운트 옵션을 사용하면 cgroup 네임스페이스 경계를 자동으로 delegation 경계로 인식합니다. 이는 컨테이너 내부에서 cgroup 설정을 변경하더라도 네임스페이스 외부의 cgroup에 영향을 줄 수 없도록 보장합니다. systemd 247+에서 기본 활성화됩니다.
cgroup 모니터링 및 디버깅
cgroup 관련 문제를 진단하고 리소스 사용을 모니터링하기 위한 도구와 기법을 정리합니다.
# === 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 burst | burst=0이면 짧은 스파이크도 스로틀 | cpu.max.burst를 적절히 설정 (quota의 20-50%) |
| 메모리 회수 빈도 | memory.high가 너무 낮으면 잦은 direct reclaim | memory.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 관리를 가능하게 합니다.
# === 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 드라이버 | 계층 구조 |
|---|---|---|---|
| Docker | 20.10+ | systemd (권장) / cgroupfs | system.slice/docker-{id}.scope |
| containerd | 1.4+ | systemd (권장) / cgroupfs | system.slice/containerd-{id}.scope |
| CRI-O | 1.20+ | systemd | system.slice/crio-{id}.scope |
| Podman | 3.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"]
memory.max/cpu.max에 직접 매핑되며, MemoryQoS(memory.high 기반)와 PSI 기반 eviction이 추가로 지원됩니다.
Kubernetes cgroup v2 계층 상세
Kubernetes는 cgroup v2에서 Pod 수준 QoS 클래스를 cgroup 계층으로 매핑합니다. kubelet의 cgroupDriver: systemd 설정이 권장되며, QoS 클래스별 cgroup 경로가 자동 생성됩니다.
# 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 프로세스 이동 핵심 경로 */
/* 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 파일은 inotify와 poll()로 모니터링할 수 있어, 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 부트 옵션 |
| nsdelegate | cgroup NS 경계에서 자동 delegation | mount -o nsdelegate |
| memory_recursiveprot | memory.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 필수 설정 |
| 네트워크 BPF | cgroup별 네트워크 필터링 | BPF_CGROUP_INET_INGRESS/EGRESS |
| sysctl BPF | cgroup별 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.max와 pids.max를 설정하세요
- nsdelegate 마운트 옵션을 사용하여 delegation 경계를 강제하세요
- root가 아닌 사용자에게 cgroup을 위임할 때는 subtree_control과 cgroup.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=1G | IOWeight=500 | 응답 시간 보장 우선 |
| API 서버 (Spring, Django) | CPUWeight=300, CPUQuota=800% | MemoryMax=8G, MemoryHigh=6G, MemoryMin=2G | IOWeight=300 | GC 고려 메모리 여유 |
| 데이터베이스 (PostgreSQL) | CPUWeight=500, AllowedCPUs=4-15 | MemoryMax=32G, MemoryLow=16G, MemoryMin=8G | IOWeight=900 | 메모리 보호 최우선 |
| 캐시 (Redis) | CPUWeight=200 | MemoryMax=16G, MemorySwapMax=0 | IOWeight=200 | 스왑 금지, 메모리만 |
| 메시지 큐 (Kafka) | CPUWeight=300 | MemoryMax=12G, MemoryHigh=10G | IOWeight=800 | I/O 집약적 |
| 배치/ETL | CPUWeight=10, cpu.idle=1 | MemoryMax=8G | IOWeight=10 | 최저 우선순위 |
| CI/CD 빌드 | CPUQuota=400%, burst=200ms | MemoryMax=16G, MemoryHigh=12G | IOWeight=200 | 스파이크 허용 |
| ML 학습 | CPUWeight=200, AllowedCPUs=16-31 | MemoryMax=64G, MemoryMin=32G | IOWeight=500 | GPU cgroup 별도 |
메모리 설정 경험 법칙:
memory.max: 애플리케이션 정상 사용량의 1.5~2배memory.high:memory.max의 75~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_ratio | 20 | 시스템 전체 dirty 비율 (cgroup io와 별도) |
| vm.dirty_background_ratio | /proc/sys/vm/dirty_background_ratio | 10 | 백그라운드 writeback 시작 비율 |
| vm.swappiness | /proc/sys/vm/swappiness | 60 | 글로벌 swappiness (cgroup별 오버라이드 가능) |
| vm.min_free_kbytes | /proc/sys/vm/min_free_kbytes | 자동 | 최소 여유 메모리 (cgroup reclaim에 영향) |
| kernel.pid_max | /proc/sys/kernel/pid_max | 4194304 | 시스템 전체 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 Control | systemd 리소스 관리 가이드 | freedesktop.org |
| 커널 소스: mm/memcontrol.c | 메모리 컨트롤러 구현 | mm/memcontrol.c |
| 커널 소스: kernel/sched/fair.c | CFS 대역폭 제어 구현 | kernel/sched/fair.c |
| 커널 소스: kernel/cgroup/cgroup.c | cgroup 코어 구현 | kernel/cgroup/cgroup.c |
| 커널 소스: kernel/sched/psi.c | PSI 구현 | kernel/sched/psi.c |
| LWN: Control groups v2 | v2 설계 해설 기사 | LWN.net |
| LWN: PSI | PSI 도입 배경 해설 | LWN.net |
| Facebook oomd | userspace OOM daemon | GitHub |
cgroup 발전 로드맵
cgroup 서브시스템은 리눅스 커널에서 가장 활발하게 발전하는 영역 중 하나입니다. 최신 커널에서 추가되었거나 진행 중인 주요 변화를 정리합니다.
| 커널 버전 | 기능 | 영향 |
|---|---|---|
| 5.14 | cgroup.kill | cgroup 내 전체 프로세스에 SIGKILL 일괄 전송 |
| 5.15 | memcg LRU reparenting 개선 | dying cgroup 해제 속도 향상 |
| 5.18 | io.stat에 dbytes/dios 추가 | discard I/O 통계 추적 |
| 5.19 | memory.reclaim | 능동적 메모리 회수 인터페이스 |
| 6.0 | cgroup v1 비활성화 옵션 개선 | CONFIG_CGROUP_V1=n 빌드 옵션 |
| 6.1 | memory.reclaim swappiness 파라미터 | 회수 시 anon/file 비율 제어 |
| 6.1 | PSI cgroup 트리거 개선 | per-cgroup PSI 정밀도 향상 |
| 6.2 | cpuset 파티션 isolated 모드 | 커널 스레드도 제외하는 완전 격리 |
| 6.3 | memory.peak 리셋 지원 | peak 사용량 초기화 가능 |
| 6.5 | memory.zswap.max | cgroup별 zswap 풀 크기 제한 |
| 6.5 | memory_hugetlb_accounting | HugeTLB을 memory 카운터에 통합 |
| 6.6 | pids.peak | 최고 PID 사용량 기록 |
| 6.7 | memcg 통계 정밀도 개선 | per-CPU 카운터 집계 최적화 |
| 6.8 | memory.swap.peak | 스왑 최고 사용량 기록 |
| 6.9 | cpuset.cpus.exclusive | 전용 CPU 지정 인터페이스 안정화 |
| 6.10 | memory.reclaim recursive | 하위 cgroup 포함 재귀적 회수 |
| 6.12 | sched_ext (BPF 스케줄러) | cgroup 단위 BPF 스케줄링 정책 |
- sched_ext: BPF 기반 커스텀 스케줄러로 cgroup 단위 스케줄링 정책을 사용자가 정의
- 메모리 티어링: CXL 메모리와 연동한 cgroup별 메모리 티어 배치 정책
- GPU cgroup: GPU/가속기 리소스를 cgroup으로 제어 (DRM 서브시스템 연동)
- 네트워크 QoS: BPF + EDT(Earliest Departure Time) 기반 cgroup별 네트워크 대역폭 보장
cgroup v1 폐기 일정
cgroup v1은 점진적으로 폐기(deprecation) 경로에 있습니다:
| 시점 | 상태 | 영향 |
|---|---|---|
| 커널 6.0 | CONFIG_CGROUP_V1=n 빌드 옵션 추가 | 배포판이 v1을 선택적으로 제거 가능 |
| 커널 6.x+ | v1 컨트롤러에 새 기능 추가 중단 | 모든 새 기능은 v2에만 구현 |
| Android 12+ | cgroup v2 필수 | v1 지원 장치는 인증 불가 |
| Fedora 31+ | 기본 v2 | v1 하이브리드 모드 가능하지만 비권장 |
| RHEL 10 (예정) | v1 제거 예정 | cgroup v2만 지원 |
release_agent 취약점(CVE-2022-0492)은 v1에서만 발생하므로 보안 측면에서도 v2 전환이 중요합니다.
cgroup 용어 사전
| 용어 | 설명 |
|---|---|
| cgroup | Control Group의 약어. 프로세스 그룹의 리소스 사용을 제한·계량·격리하는 커널 메커니즘 |
| css (cgroup_subsys_state) | 각 컨트롤러의 cgroup당 상태를 나타내는 구조체. mem_cgroup, task_group 등이 이를 상속 |
| css_set | 프로세스가 속한 모든 cgroup 컨트롤러 상태의 집합. task_struct→cgroups가 이를 가리킴 |
| subsys | cgroup 서브시스템(=컨트롤러). cpu, memory, io, pids 등 |
| slice | systemd의 cgroup 계층 단위. 여러 service/scope를 그룹화 |
| scope | systemd가 외부 프로세스를 위해 런타임에 생성하는 cgroup 단위 |
| delegation | 비특권 사용자에게 cgroup 하위 트리의 관리 권한을 안전하게 위임하는 메커니즘 |
| charge/uncharge | 메모리 페이지를 cgroup에 과금/환급하는 메커니즘 |
| PSI | Pressure Stall Information. 리소스 대기로 인한 stall 시간을 측정하는 지표 |
| throttling | cpu.max 제한 초과 시 CPU 사용을 일시 중단하는 것 |
| direct reclaim | 메모리 할당 경로에서 동기적으로 페이지를 회수하는 것. memory.high 초과 시 발생 |
| dying cgroup | 삭제되었지만 page cache 참조로 인해 완전히 해제되지 않은 cgroup |
| No Internal Process Constraint | 자식 cgroup이 존재하는 cgroup에 프로세스를 직접 배치할 수 없는 v2 규칙 |
| UCLAMP | Utilization Clamping. 스케줄러에게 CPU 활용도 상/하한 힌트를 제공하여 주파수 결정에 영향 |
| io.cost | 장치의 실제 성능 특성을 모델링하여 정밀한 가중치 기반 I/O 분배를 제공하는 cgroup v2 IO 제어 모델 |
| nsdelegate | cgroup NS 경계에서 자동으로 delegation을 적용하는 마운트 옵션 |
| memory_recursiveprot | memory.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 --vm | memory.max, memory.high 동작 확인 |
| CPU 제한 | stress-ng --cpu | cpu.max 스로틀링, cpu.weight 비례 배분 |
| I/O 제한 | fio, dd | io.max 대역폭 제한, io.latency 동작 |
| PID 제한 | 포크 테스트 | pids.max 초과 시 EAGAIN 확인 |
| Freezer | signal 테스트 | cgroup.freeze 동결/해제 확인 |
| OOM | memory exhaustion | oom_kill, oom.group 동작 확인 |
| PSI | poll trigger | PSI 트리거 정확도, 지연 시간 |
| 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.current와 memory.max를 읽어야 합니다. LXCFS나 cgroup-aware /proc 에뮬레이션을 사용하면 free 등 기존 도구에서도 cgroup 제한을 반영합니다.
Q: cpu.max를 설정했는데 프로세스가 설정보다 더 많은 CPU를 사용합니다. 원인은?
몇 가지 원인이 있습니다:
- cpu.max.burst가 설정되어 있으면 미사용 quota를 누적하여 순간적으로 초과 가능
- 측정 도구가 정확하지 않을 수 있음 (
cpu.stat의usage_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로 어떻게 안전하게 마이그레이션하나요?
단계별 접근을 권장합니다:
- 하이브리드 모드 테스트:
systemd.unified_cgroup_hierarchy=1만 설정하여 systemd는 v2, 일부 컨트롤러는 v1으로 운영 - 컨테이너 런타임 업그레이드: Docker 20.10+, containerd 1.4+, Podman 3.0+ 확인
- 인터페이스 매핑: v1 설정을 v2 equivalent로 변환 (cpu.shares→cpu.weight 등)
- 테스트 환경 검증: 워크로드별 성능/격리 테스트
- 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.high와 memory.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와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.