왜 리소스 격리가 문제가 되는가
한 호스트에서 여러 컨테이너를 돌리면 특정 프로세스가 CPU와 메모리를 독점하는 "시끄러운 이웃(noisy neighbor)" 문제가 생긴다. 격리 없이 배포하면 배치 작업 하나가 메모리를 몰아 쓰다가 OOM Killer가 엉뚱한 API 서버를 죽이고, CPU 스파이크가 지연 시간을 흔든다. cgroup(control group)은 커널이 프로세스 그룹 단위로 CPU·메모리·I/O를 제한하고 계량하는 기능이며, Docker·Kubernetes·systemd가 모두 이 위에서 동작한다.
cgroup v1과 v2의 차이
v1은 컨트롤러(cpu, memory, io)마다 별도 계층 구조를 가져 설정이 파편화됐다. v2는 단일 통합 계층(unified hierarchy)으로 모든 컨트롤러가 하나의 트리를 공유한다. 최근 배포판과 Kubernetes 1.25+는 v2를 기본으로 삼는다.
| 항목 | v1 | v2 |
|---|---|---|
| 계층 구조 | 컨트롤러별 분리 | 단일 통합 트리 |
| 메모리 제한 | memory.limit_in_bytes | memory.max / memory.high |
| CPU 제한 | cfs_quota_us / period_us | cpu.max (통합) |
| 압박 지표 | 없음 | PSI(memory.pressure) |
v2 활성화 확인
먼저 시스템이 v2로 마운트됐는지 확인한다. cgroup2fs가 보이면 v2다.
mount | grep cgroup2
# cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,...)
# 사용 가능한 컨트롤러 확인
cat /sys/fs/cgroup/cgroup.controllers
# cpuset cpu io memory pids
직접 cgroup 만들어 제한 걸기
개념을 이해하려면 손으로 만들어 보는 게 빠르다. 하위 그룹에 컨트롤러를 위임하려면 부모의 cgroup.subtree_control에 먼저 등록해야 한다.
cd /sys/fs/cgroup
# 부모에서 자식에게 컨트롤러 위임
echo "+cpu +memory" > cgroup.subtree_control
mkdir demo
# CPU: 100000us 주기 중 50000us만 = 0.5 코어
echo "50000 100000" > demo/cpu.max
# 메모리 하드 리밋 256MB
echo 268435456 > demo/memory.max
# 소프트 리밋(초과 시 회수 압박) 200MB
echo 209715200 > demo/memory.high
# 현재 셸을 이 그룹에 넣기
echo $$ > demo/cgroup.procs
이제 이 셸에서 뜬 프로세스는 CPU 0.5코어, 메모리 256MB로 묶인다. memory.high는 초과 시 즉시 죽이지 않고 회수 압박을 주는 반면, memory.max를 넘기면 OOM으로 종료된다.
계량과 압박 지표 읽기
격리만큼 중요한 게 관측이다. v2는 실제 사용량과 압박(PSI)을 노출한다. PSI는 리소스 부족으로 프로세스가 얼마나 지연됐는지를 비율로 보여줘, 리밋 튜닝의 근거가 된다.
cat /sys/fs/cgroup/demo/memory.current # 현재 메모리 사용 바이트
cat /sys/fs/cgroup/demo/cpu.stat # usage_usec, throttled_usec 등
cat /sys/fs/cgroup/demo/memory.pressure # some avg10=... full avg10=...
cpu.stat의 throttled_usec가 계속 증가하면 CPU 쿼터가 너무 빡빡하다는 신호다. memory.pressure의 full 값이 높으면 메모리 리밋을 올릴지 검토한다.
컨테이너 런타임에서의 매핑
Docker/Kubernetes 옵션은 결국 위 파일에 그대로 반영된다. 아래 요청/제한 설정이 어떤 cgroup 값으로 번역되는지 알면 디버깅이 쉬워진다.
resources:
limits:
cpu: "500m" # cpu.max → 50000 100000
memory: "256Mi" # memory.max → 268435456
requests:
cpu: "250m" # cpu.weight(상대 가중치)에 반영
memory: "128Mi"
여기서 CPU limit은 절대 쿼터(cpu.max)로, CPU request는 경쟁 시 상대 가중치(cpu.weight)로 매핑된다는 점이 핵심이다. request만 설정하면 상한 없이 가중치만 부여된다.
실무 주의점
- CPU limit을 과하게 낮추면 스로틀링으로 오히려 지연이 튄다. 지연에 민감한 서비스는 limit보다 request 기반 가중치로 관리하는 편이 안전할 때가 많다.
- JVM·Go 등 일부 런타임은 cgroup 리밋을 자동 감지하지만, 구버전은 호스트 코어/메모리를 보고 스레드 풀을 잡아 OOM을 유발한다. 런타임의 cgroup 인식 여부를 반드시 확인한다.
memory.max만 걸고memory.high를 두지 않으면 완충 없이 바로 OOM으로 떨어진다. high로 회수 여지를 두는 편이 안정적이다.- swap도 v2에서는
memory.swap.max로 별도 통제된다. 이를 무제한으로 두면 메모리 리밋이 사실상 무력화될 수 있다.
cgroup v2 파일 인터페이스를 한 번 손으로 다뤄 두면, 상위 오케스트레이터가 왜 그렇게 동작하는지 커널 레벨에서 설명할 수 있게 된다.