Linux cgroup v2 메모리 제한, memory.high와 memory.max를 나눠 운영하는 법
AI를 활용하여 생성한 이미지입니다.
Linux 서비스가 갑자기 종료되거나 느려질 때 “메모리를 많이 썼다”는 말만으로는 원인을 가르기 어려워요. 프로세스가 실제로 차지한 메모리, 상위 slice가 허용한 한도, 커널이 압박을 걸어 reclaim한 상황, OOM으로 프로세스를 죽인 상황이 서로 다르기 때문이에요. cgroup v2에서는 계층별 제한이 아래 그룹으로 그대로 내려가므로, 서비스 하나의 설정만 봐서는 부족할 때가 많습니다.[1]
memory.high와 memory.max는 역할이 달라요
memory.high는 주로 압박을 시작하는 경계예요. 이 값을 넘으면 커널은 해당 cgroup의 메모리 사용을 강하게 회수하고 작업을 늦출 수 있지만, 값에 도달했다고 즉시 프로세스를 죽이지는 않아요. 반대로 memory.max는 절대 상한에 가깝고, 그 안에 사용량을 가둘 수 없으면 cgroup 내부에서 OOM 처리가 일어날 수 있습니다.[1]
| 설정 | 운영 의미 | 관찰할 현상 |
|---|---|---|
memory.high |
압박과 속도 저하를 먼저 유도해요. | 응답 지연, reclaim 증가, 처리량 저하 |
memory.max |
서비스가 넘지 못할 마지막 경계예요. | cgroup 내부 OOM, 프로세스 종료 |
memory.current |
현재 사용량을 보여줘요. | 상한과 실제 사용량의 간격 |
memory.events |
압박·상한·OOM 사건을 누적해요. | high, max, oom, oom_kill 변화 |
systemd를 쓰는 서비스라면 unit에 MemoryHigh=와 MemoryMax=를 선언할 수 있어요. systemd는 해당 설정에 맞는 cgroup 제어를 구성하고, 상위 slice의 제한과 합쳐진 유효값을 적용합니다.[2] 그래서 서비스 파일에 MemoryMax=4G라고 적었다고 실제로 4GiB를 온전히 쓸 수 있다고 단정하면 안 돼요.
먼저 상위 계층을 읽어요
다음 명령은 설정을 바꾸지 않고 현재 경계를 확인하는 데 사용해요.
systemctl show example.service -p ControlGroup -p MemoryCurrent -p MemoryHigh -p MemoryMax -p EffectiveMemoryHigh -p EffectiveMemoryMax
systemctl status example.service --no-pager
systemctl cat example.service
ControlGroup으로 실제 경로를 알아낸 다음 그 디렉터리에서 커널 파일을 직접 읽으면 systemd가 보여주는 값과 사건 카운터를 맞춰 볼 수 있어요.
cg=$(systemctl show -p ControlGroup --value example.service)
cd "/sys/fs/cgroup${cg}"
printf 'current='; cat memory.current
printf 'high='; cat memory.high
printf 'max='; cat memory.max
cat memory.events
cat memory.stat | sed -n '1,25p'
cgroup v2의 컨트롤러는 계층적이에요. 부모에서 더 작은 상한을 걸면 자식 unit이 더 큰 값을 적어도 실제 유효 상한은 부모와 자식 중 더 엄격한 쪽에 가까워집니다.[1] EffectiveMemoryMax=가 예상보다 작다면 unit 파일만 고치지 말고 slice와 상위 scope도 함께 확인하세요.[2]
지연과 OOM을 같은 문제로 보지 않아요
memory.events에서 high만 늘고 oom_kill이 그대로라면, 서비스가 죽지는 않았지만 메모리 압박으로 처리량이 떨어졌을 가능성이 있어요. 반대로 max, oom, oom_kill이 함께 증가하면 상한을 실제로 건드린 흐름에 가까워집니다. 이 파일의 숫자는 누적값이라 한 번 읽고 결론을 내리기보다 배포 전후 값을 저장하고 증가분을 비교해야 해요.
애플리케이션 로그의 “out of memory”만으로 호스트 전체 메모리가 부족했다고 말해서도 안 됩니다. cgroup 내부 OOM은 시스템 전체 여유 메모리가 남아 있어도 발생할 수 있어요. 반대로 호스트 OOM과 unit 내부 OOM은 journal에 남는 주체와 종료 과정이 다를 수 있으니 journalctl -k와 journalctl -u example.service를 시간순으로 겹쳐 보세요.
실제 적용은 작은 단계로 나눠요
- 현재
memory.current,memory.events, 요청량, 응답 지연을 함께 기록해요. - 서비스가 평소 사용하는 양보다 조금 높은 값에
MemoryHigh=를 먼저 둬요. - 피크 구간에서 지연과 사건 카운터가 어떻게 바뀌는지 관찰해요.
- 정말 필요한 경우에만
MemoryMax=를 마지막 방어선으로 추가해요. - 설정 변경 뒤에는
daemon-reload과 재시작을 분리하고, 실제 요청을 다시 보내요.[3]
sudo systemctl edit example.service
# [Service]
# MemoryHigh=3G
# MemoryMax=4G
sudo systemctl daemon-reload
sudo systemctl restart example.service
제한을 너무 낮추면 메모리 누수를 빨리 발견하는 대신 정상적인 캐시나 일시적인 작업까지 OOM으로 잘라낼 수 있어요. 반대로 상한을 무한대로 두면 장애가 늦게 드러날 수 있습니다. 서비스가 캐시를 많이 쓰는지, 요청 동시성이 변하는지, 스왑이 켜져 있는지까지 같이 기록해야 숫자를 조정할 수 있어요.
되돌릴 때도 사건 기록을 남겨요
변경을 되돌릴 때는 drop-in을 삭제하고 unit을 다시 읽는 것만으로 끝내지 마세요. 적용 전후의 systemctl show 출력과 memory.events를 보관하면 제한이 원인이었는지 애플리케이션 자체 문제였는지 비교할 수 있어요. 상위 slice에서 제한을 상속받는 환경이라면 자식 unit만 원복해도 문제가 계속됩니다.
이미 OOM이 발생한 서비스는 자동 재시작 횟수를 올리는 것보다 먼저 원인 데이터를 남기는 편이 나아요. 자동 재시작은 복구 장치이지 메모리 예산을 늘리는 기능이 아니에요. systemd의 재시작 정책과 로그 확인 순서는 journalctl 로그 관리 글의 흐름과 함께 보면 좋습니다.
점검 기준
MemoryHigh초과가 지연으로 이어졌는지 확인해요.MemoryMax와 부모 slice의 유효 상한을 함께 읽어요.memory.events증가분을 배포 전후로 비교해요.- unit 내부 OOM과 호스트 OOM의 journal 주체를 나눠요.
- 제한을 조정한 뒤 정상 요청과 피크 요청을 각각 다시 보내요.
자주 묻는 점
MemoryMax만 설정하면 충분할까요?
마지막 방어선으로는 쓸 수 있지만, 압박을 단계적으로 관찰하려면 MemoryHigh를 함께 두는 편이 낫습니다. 공식 문서도 MemoryHigh를 주된 제어 수단으로, MemoryMax를 마지막 제한으로 설명해요.[2]
메모리 사용량이 상한보다 낮은데 왜 느려질까요?
부모 slice의 압박, 스왑과 IO, 파일 캐시 회수, 다른 cgroup의 경쟁이 원인일 수 있어요. 서비스 unit의 숫자 하나만으로는 설명이 안 되므로 이벤트와 상위 계층을 같이 봐야 합니다.[1]