cgroup v2 memory.high와 memory.max: 스로틀링과 OOM의 경계

cgroup v2에서 memory.high는 회수 압박과 스로틀링, memory.max는 OOM으로 이어지는 하드 리밋입니다. memory.events와 memory.oom.group까지 공식 문서 기준으로 정리합니다.

Linuxcgroup v2메모리OOM운영

컨테이너나 서비스에 메모리 상한을 걸 때 cgroup v2는 파일 두 개를 내놓습니다. memory.high와 memory.max입니다. 이름만 보면 둘 다 “이 이상 쓰지 마라”는 뜻 같지만, 넘었을 때 커널이 하는 일이 다릅니다. 하나는 프로세스를 느리게 만들고, 다른 하나는 프로세스를 죽일 수 있습니다.

이 글은 Linux 커널 공식 문서 “Control Group v2”(docs.kernel.org의 Linux 7.3.0 문서, 2026-10-20 확인)만을 근거로 두 경계의 차이, 그리고 그 결과를 확인하는 memory.events, memory.events.local, memory.oom.group을 정리합니다. 시스템 전체의 메모리 오버커밋은 Linux 메모리 오버커밋 글에서 따로 다뤘습니다.

먼저 memory.current

메모리 인터페이스 파일 중 가장 기본은 memory.current입니다. 루트가 아닌 cgroup에 있는 읽기 전용 파일이고, 문서는 이를 “cgroup과 그 하위 cgroup들이 현재 사용 중인 메모리 총량”이라고 정의합니다. 자식 cgroup의 사용량이 부모의 값에 포함된다는 뜻입니다.

몇 가지 기본 규칙도 함께 알아 두면 좋습니다.

  • 메모리 양은 모두 바이트 단위입니다. PAGE_SIZE에 맞지 않는 값을 쓰면 다시 읽을 때 가장 가까운 PAGE_SIZE 배수로 올림될 수 있습니다.
  • memory.high와 memory.max의 기본값은 max이고, 이는 상한이 없다는 의미입니다.
  • 이 파일들은 루트 cgroup에는 없습니다. 문서의 규약상 루트 cgroup은 자원 제어에서 제외됩니다.
  • 추적 대상은 사용자 공간 메모리(페이지 캐시와 익명 메모리), dentry와 inode 같은 커널 자료구조, TCP 소켓 버퍼입니다. 문서는 이 추적이 “완벽하게 빈틈없지는 않다”고 직접 밝힙니다.

memory.high: 넘으면 느려진다

문서는 memory.high를 “메모리 사용량 스로틀 리밋”이라고 부릅니다. 사용량이 이 경계를 넘으면 해당 cgroup의 프로세스는 스로틀링되고 강한 회수 압박을 받습니다. 컨트롤러 이슈와 해법 절은 이를 좀 더 구체적으로, 할당을 직접 회수(direct reclaim)로 보내 초과분을 해소하게 만드는 방식이라고 설명합니다.

핵심은 두 문장입니다.

  1. memory.high를 넘어도 OOM killer는 절대 호출되지 않습니다.
  2. 극단적인 조건에서는 이 경계를 넘어설 수 있습니다.

두 번째 문장의 “극단적인 조건”을 문서는 동시 할당이 많고 그룹 안의 회수 진행이 완전히 무너진 경우로 설명합니다. 그런 때에도 그룹을 죽이기보다 다른 그룹이나 시스템의 여유분으로 할당을 채우는 편이 대체로 낫다는 것이 설계 의도이고, 그런 넘침을 막는 역할은 memory.max에 맡깁니다.

그래서 memory.high를 너무 낮게 잡으면 프로세스가 죽는 대신 성능이 점점 떨어집니다. 문서는 사용자가 이를 관찰하면서 수용 가능한 성능을 내는 최소 메모리 크기를 찾을 때까지 조정할 수 있다고 씁니다.

memory.max: 줄일 수 없으면 OOM

memory.max는 “메모리 사용량 하드 리밋”이고, 문서 표현으로는 cgroup의 메모리 사용을 제한하는 주된 메커니즘입니다. 사용량이 이 값에 도달했는데 줄일 수 없으면 해당 cgroup 안에서 OOM killer가 호출됩니다.

하드 리밋이라고 해도 문서가 붙인 단서가 있습니다.

  • 특정 상황에서는 사용량이 일시적으로 리밋을 넘을 수 있습니다.
  • 기본 설정에서 일반적인 0차(order-0) 할당은 OOM killer가 현재 태스크를 희생자로 고르지 않는 한 항상 성공합니다.
  • 어떤 종류의 할당은 OOM killer를 호출하지 않습니다. 호출자는 다른 방식으로 재시도하거나, 사용자 공간에 -ENOMEM을 돌려주거나, 디스크 readahead처럼 조용히 무시할 수 있습니다.

운영 중인 cgroup의 memory.max를 바꿀 때 특히 중요한 문장도 있습니다. memory.max에 값을 쓰면 커널은 먼저 새 리밋을 적용해 새 charge를 막고, 그다음 새 리밋을 만족할 때까지 회수와 OOM kill을 진행합니다. 또는 memory.max에 값을 쓴 태스크 자신이 죽을 수도 있습니다. 현재 사용량보다 낮은 값을 쓰는 순간이 곧 OOM kill의 방아쇠가 될 수 있다는 뜻입니다.

두 파일 비교

항목 memory.high memory.max
문서상 성격 스로틀 리밋 하드 리밋
넘었을 때 스로틀링과 강한 회수 압박 줄일 수 없으면 cgroup 안에서 OOM killer 호출
OOM killer 절대 호출하지 않음 호출함 (단, 호출하지 않는 할당 종류가 있음)
초과 가능성 극단적 조건에서 넘을 수 있음 특정 상황에서 일시적으로 넘을 수 있음
기본값 max max
관련 이벤트 memory.events의 high memory.events의 max, oom, oom_kill

memory.high는 kill switch가 아니다

memory.high만 설정해 두고 “이 정도면 넘지 않겠지”라고 기대하면 문서의 의도와 어긋납니다. 문서는 high 리밋을 “외부 프로세스가 제한된 cgroup을 모니터링하면서 강한 회수 압박을 완화하는 시나리오”에서 쓰라고 명시합니다.

사용 지침 절도 같은 방향입니다. high 리밋을 넘으면 OOM killer 대신 스로틀링이 일어나므로, 관리 에이전트가 이를 관찰하고 메모리를 더 주거나 워크로드를 종료하는 등의 조치를 할 여유가 충분하다고 설명합니다. 같은 절은 memory.high를 메모리 사용을 제어하는 주된 메커니즘으로 소개하고, high 리밋의 합이 가용 메모리보다 큰 오버커밋도 유효한 전략이라고 씁니다. 하드 리밋으로서의 주역은 memory.max, 일상적인 사용량 조절의 주역은 memory.high로 역할이 나뉘는 셈입니다.

다만 사용량 숫자만으로는 판단이 어렵다는 점도 문서가 짚습니다. 네트워크로 받은 데이터를 파일에 쓰는 워크로드는 가용 메모리를 전부 쓸 수도 있지만 적은 메모리로도 비슷한 성능을 낼 수 있습니다. 메모리가 더 필요한지는 메모리 부족으로 워크로드가 얼마나 영향을 받는지, 즉 압박의 정도로 봐야 합니다. 문서의 메모리 인터페이스 목록에는 메모리에 대한 pressure stall information을 보여 주는 memory.pressure 파일이 있습니다. PSI 자체는 Linux Pressure Stall Information 글에서 다뤘습니다.

memory.events와 memory.events.local

경계를 넘었는지는 memory.events로 확인합니다. 루트가 아닌 cgroup에 있는 읽기 전용 flat-keyed 파일이고, 값이 바뀌면 file modified 이벤트가 발생합니다. 이 저장본(7.3.0)에 정의된 필드는 다음과 같습니다.

필드 문서상 의미
low 사용량이 low 경계 아래인데도 높은 메모리 압박 때문에 회수된 횟수. 보통 low 경계가 오버커밋되었다는 신호
high high 경계를 넘어 프로세스가 스로틀링되고 직접 회수로 보내진 횟수. global 압박이 아니라 high 리밋으로 사용량이 묶인 cgroup이라면 발생하는 것이 정상
max 사용량이 max 경계를 넘으려던 횟수. 직접 회수로도 줄이지 못하면 OOM 상태로 감
oom 사용량이 리밋에 도달해 할당이 실패하려던 횟수. 실패한 high-order 할당이나 호출자가 재시도를 원하지 않은 경우처럼 OOM killer가 선택지가 아니면 올라가지 않음
oom_kill 이 cgroup에 속한 프로세스가 어떤 종류의 OOM killer에 의해서든 죽은 수
oom_group_kill group OOM이 발생한 횟수
sock_throttled 이 cgroup에 연결된 네트워크 소켓이 스로틀링된 횟수

high와 max, oom, oom_kill은 서로 다른 단계를 셉니다. high가 꾸준히 오르는 것은 memory.high가 제 역할을 하고 있다는 뜻일 수 있습니다. max는 하드 리밋에 부딪힌 시도이고, oom은 할당이 실패 직전까지 간 경우이며, oom_kill은 실제로 프로세스가 죽은 수입니다. oom_kill의 정의가 “어떤 종류의 OOM killer든”이라는 점도 기억해 둘 만합니다. 이 cgroup의 memory.max가 아닌 다른 원인의 OOM으로 죽은 프로세스도 여기에 셀 수 있습니다.

memory.events의 모든 필드는 계층적입니다. 하위 cgroup에서 일어난 이벤트도 상위 cgroup의 값에 반영되고, 하위의 이벤트 때문에 상위 파일의 file modified 이벤트가 생길 수도 있습니다. 해당 cgroup 자체에서 일어난 이벤트만 보고 싶다면 memory.events.local을 읽습니다. 형식은 같지만 필드가 계층적이지 않고, 이 파일의 file modified 이벤트도 로컬 이벤트만 반영합니다.

예를 들어 부모 P 아래에 자식 A와 B가 있고 A에서만 OOM kill이 일어났다면, P/memory.events의 oom_kill은 올라가지만 P/memory.events.local은 그대로입니다. 어느 자식에서 문제가 생겼는지 찾을 때는 부모의 memory.events로 범위를 좁히고 자식들의 memory.events.local로 위치를 확인하는 식으로 쓸 수 있습니다.

한 가지 예외가 있습니다. cgroup2를 memory_localevents 마운트 옵션으로 마운트하면 memory.events도 하위 트리를 빼고 현재 cgroup의 데이터만 채웁니다. 문서는 이를 레거시 동작이라고 부르며, 옵션이 없는 기본 동작은 하위 트리 카운트를 포함하는 것입니다. 이 옵션은 시스템 전체에 적용되고 init 네임스페이스에서 마운트하거나 리마운트할 때만 바꿀 수 있습니다.

memory.oom.group: 전부 아니면 전무, 단 예외 포함

memory.oom.group은 루트가 아닌 cgroup에 있는 읽기/쓰기 파일이고 기본값은 0입니다. 이 값이 설정되면 OOM killer는 그 cgroup을 나눌 수 없는 하나의 워크로드로 다룹니다. cgroup에 속한 태스크(리프가 아니면 하위 cgroup의 태스크까지)를 함께 죽이거나 아무것도 죽이지 않습니다. 일부 프로세스만 죽어서 워크로드가 어중간한 상태로 남는 것을 피하려는 기능입니다.

문서가 명시한 한계도 두 가지 있습니다.

  • oom_score_adj가 -1000인, OOM 보호가 걸린 태스크는 예외이며 절대 죽지 않습니다. “전부”에서 이런 태스크는 빠집니다.
  • 어떤 cgroup에서 OOM killer가 호출되면, 상위 cgroup의 memory.oom.group 값과 관계없이 그 cgroup 밖의 태스크는 죽이지 않습니다.

memory.oom.group은 OOM이 일어났을 때 무엇을 함께 죽일지를 정할 뿐, OOM 자체를 막지는 않습니다. 실제로 group OOM이 일어났는지는 memory.events의 oom_group_kill로 확인합니다.

읽기 전용 점검 순서

아래 명령은 파일을 읽기만 합니다. <pid>, <cgroup2-mount>, <cgroup-path>는 자리표시자이므로 자신의 환경에 맞게 바꿔야 합니다.

# 1. 프로세스가 속한 cgroup 경로 확인 (v2 항목은 "0::<cgroup-path>" 형식)
cat /proc/<pid>/cgroup

# 2. cgroup2 마운트 지점과 경로를 합쳐 디렉터리 지정
CG="<cgroup2-mount><cgroup-path>"

# 3. 현재 사용량과 두 경계
cat "$CG/memory.current"
cat "$CG/memory.high"
cat "$CG/memory.max"

# 4. 계층 이벤트와 로컬 이벤트 비교
cat "$CG/memory.events"
cat "$CG/memory.events.local"

# 5. group OOM 설정과 압박 정보
cat "$CG/memory.oom.group"
cat "$CG/memory.pressure"

/proc/<pid>/cgroup에 레거시 cgroup 줄이 여러 개 있을 수 있지만, cgroup v2 항목은 항상 0:: 뒤에 경로가 오는 형식입니다. 해당 cgroup 아래에 살아 있는 프로세스가 있는지는 cgroup.events의 populated 필드(있으면 1, 없으면 0)로 볼 수 있습니다.

memory.peak도 읽으면 cgroup과 하위 cgroup의 최대 사용량 기록을 보여 주지만, 이 파일에 비어 있지 않은 문자열을 쓰면 같은 파일 디스크립터로 읽는 값이 현재 사용량으로 리셋됩니다. 점검 중에는 읽기만 하는 편이 안전합니다.

쓰기는 실험 환경에서만

다음은 버려도 되는 테스트 VM 같은 실험 환경을 가정한 예시입니다. 운영 중인 cgroup에 그대로 적용하지 마세요. 앞에서 본 것처럼 현재 사용량보다 낮은 memory.max를 쓰면 즉시 OOM kill이 일어날 수 있습니다.

# 실험 전용: 메모리 컨트롤러가 활성화된 테스트용 부모 아래에서만 실행
cat "<lab-parent>/cgroup.subtree_control"   # 출력에 "memory"가 있는지 확인
mkdir "<lab-parent>/memtest"
echo 268435456 > "<lab-parent>/memtest/memory.high"   # 256 MiB, 바이트 단위
echo 536870912 > "<lab-parent>/memtest/memory.max"    # 512 MiB
echo <test-pid> > "<lab-parent>/memtest/cgroup.procs"
cat "<lab-parent>/memtest/memory.events"

권한과 위임에 관해 문서가 정한 규칙도 확인해 둘 필요가 있습니다.

  • 컨트롤러는 부모의 cgroup.subtree_control에 활성화되어 있어야 자식에서 쓸 수 있고, 루트가 아닌 cgroup은 자기 프로세스가 없을 때만 자식에게 도메인 자원을 나눠 줄 수 있습니다.
  • 어떤 디렉터리의 자원 제어 파일은 부모의 자원 분배를 제어합니다. 그래서 위임받은 쪽은 자기 위임 지점의 이런 파일에 쓰지 못하게 되어 있습니다. 비특권 사용자에게 위임할 때는 디렉터리와 cgroup.procs, cgroup.threads, cgroup.subtree_control에 대한 쓰기 권한만 주는 방식입니다.
  • 비루트 euid로 프로세스를 옮기려면 대상 cgroup.procs와, 출발지와 목적지의 공통 조상의 cgroup.procs 모두에 쓰기 권한이 있어야 합니다.
  • 메모리처럼 상태를 가진 자원은 프로세스를 옮겨도 함께 옮겨지지 않습니다. 문서는 자원 제한을 바꾸려고 프로세스를 자주 옮기는 방식을 권장하지 않습니다.

한계와 버전 메모

이 글은 2026-10-20에 docs.kernel.org의 Linux 7.3.0 “Control Group v2” 문서를 기준으로 확인했습니다. 실제 커널 버전, 빌드 옵션, 마운트 옵션에 따라 보이는 파일과 필드가 다를 수 있고, 서비스 매니저나 컨테이너 런타임이 이 인터페이스를 감싸 다른 방식으로 노출하거나 직접 관리할 수도 있습니다. 자신의 시스템에서는 해당 커널 버전의 문서와 사용하는 도구의 문서를 함께 확인하세요. 이 글은 특정 기능이 처음 들어온 커널 버전을 다루지 않습니다.

출처

  • Linux kernel documentation (7.3.0), “Control Group v2”, 2026-10-20 확인. 참고한 절: Mounting, Processes, Controlling Controllers, Delegation, Guidelines, Interface Files (Conventions), Core Interface Files, Memory Interface Files, Usage Guidelines, Controller Issues and Remedies.

블로그로