LabHub

컨테이너 내부 원리 · cgroup — 얼마나 쓸 수 있는가 · 이론

cgroup v2 와 제한이 지켜지지 않는 것처럼 보일 때

LabHub 에서 이어서 보기

한 줄 요약

네임스페이스는 무엇을 볼 수 있는가, cgroup 은 얼마나 쓸 수 있는가
담당합니다. 그리고 이 코스에서 제일 자주 사람을 속이는 사실은, cgroup 이 제한을
걸긴 하지만 /proc 을 가리지는 않는다는 것입니다.

왜 이게 필요했나

cgroup 은 2006년 구글에서 시작해 2007년 커널 2.6.24 에 병합됐습니다. 원래 이름은
"process container" 였는데 당시 이미 널리 쓰이던 container 라는 말과 헷갈린다는
이유로 control group, 줄여서 cgroup 으로 개명됐습니다. 이름의 역사가 곧 역할의
설명입니다 — 격리가 아니라 자원 통제입니다.

한 호스트에 컨테이너 서른 개를 올렸는데 그중 하나가 메모리를 다 먹으면 나머지
스물아홉 개가 함께 죽습니다. 네임스페이스는 이 문제를 전혀 해결하지 못합니다.
서로를 못 볼 뿐 같은 물리 자원을 공유하기 때문입니다.

어떻게 동작하나

먼저 버전을 확인합니다. stat -fc %T /sys/fs/cgroupcgroup2fs 면 v2,
tmpfs 면 v1 입니다. v2 의 주요 파일은 다음과 같습니다.

| 파일 | 뜻 |
| --- | --- |
| cpu.max | quota period 두 값. 50000 100000 이면 0.5 CPU |
| cpu.weight | 1~10000 사이의 상대 가중치, 기본 100 |
| cpu.stat | nr_periods, nr_throttled, throttled_usec |
| memory.min/low/high/max | 보호선과 상한 |
| memory.current | 현재 사용량 |
| memory.events | 한도에 걸린 횟수 |
| pids.max | 프로세스 개수 상한 |

v1 에서는 각각 cpu.cfs_quota_us, memory.limit_in_bytes 였습니다.

memory.highmemory.max 를 구분하는 것이 중요합니다. **high 를 넘으면
회수 압박이 걸리고 할당이 느려지지만 죽지는 않습니다. max 를 넘으면 OOM Kill
입니다.** 그래서 high 는 "여기서부터 아프게 하라", max 는 "여기가 벽이다"입니다.

CPU 는 죽이지 않고 재웁니다. cpu.stat 에서 nr_throttled / nr_periods
5%를 넘으면 문제로 봅니다. 1000 주기 중 350 번 스로틀됐다면 35%이고, 이때
CPU 사용률 그래프는 한가해 보이는데 응답시간만 튀는 전형적인 그림이 나옵니다.

자주 쓰는 바이트 값은 외워 두면 편합니다. 64MiB = 67108864, 192MiB = 201326592,
256MiB = 268435456, 512MiB = 536870912.

종료코드 137 = 128 + 9 = SIGKILL 입니다. OOM 킬은 앱에게 어떤 예외도 주지
않습니다. try/except 로 잡을 수 없고 로그도 남지 않습니다. 커널 로그의
Memory cgroup out of memory: Killed process ... 를 봐야 확정할 수 있고,
런타임 쪽에서는 State.OOMKilled 로 갈라야 합니다.

현장에서 만나는 모습

가장 중요한 함정입니다. --memory=256m --cpus=0.5 로 띄운 컨테이너에서
free -m 을 치면 64228MB 가, nproc 은 32 가 나옵니다. cgroup 은 제한을 걸지만
/proc 을 가리지 않기 때문입니다. JVM 은 UseContainerSupport 로 cgroup 파일을
직접 읽어 이 문제를 해결했지만, Node 의 os.cpus() 와 Go 의 runtime.NumCPU()
는 여전히 호스트 값을 봅니다. 그래서 Go 서비스에는 GOMAXPROCS 를 명시해야
합니다. 32 스레드로 워커를 띄워 놓고 0.5 코어를 배정하면 스로틀만 쌓입니다.

쿠버네티스의 QoS 클래스도 여기서 나옵니다. Guaranteed 는 OOM score -997,
Burstable 은 2~999, BestEffort 는 1000 입니다. **BestEffort 가 먼저 죽는 것은
정책 문장이 아니라 cgroup 과 OOM 점수의 직접적인 결과**입니다.

한 가지 더. rootless 환경에서는 제한을 걸어도 실제로 강제되지 않을 수
있습니다. systemd delegation(Delegate=cpu memory pids io)이 없으면 요청은
기록되지만 커널에 반영되지 않습니다. 그래서 다음 실습은 docker inspect
HostConfig 요청값과 컨테이너가 실제로 보는 값을 둘 다 확인합니다. 둘이
다르면 그것 자체가 배울 거리입니다.

다음 실습에서 할 것

cgroup 버전과 컨트롤러 목록을 확인하고, 메모리·CPU·PID 한도를 각각 걸어
본 뒤 요청값과 관측값을 대조합니다. SIGKILL 로 137 을 직접 만들어 보고
OOM 킬과 어떻게 구분하는지 정리합니다.