Docker/Kubernetes 환경에서 컨테이너는 호스트 커널의 동일한 메모리맵을 공유하면서 cgroup v2 메모리 컨트롤러를 통해 격리(Isolation)됩니다. 각 컨테이너는 독립적인 메모리 제한(memory.max), 소프트 제한(memory.high), 보호 수준(memory.min/low)을 가지며, 커널은 page_counter를 통해 페이지 할당 시 계정(charge)을 수행합니다. 컨테이너의 메모리 한계 초과 시 직접 회수(direct reclaim) 또는 스로틀링(throttling)이 발생하며, 최종적으로 OOM Killer에 의해 프로세스가 종료될 수 있습니다.
cgroup v2는 컨테이너 오케스트레이션 도구(Docker, containerd, Kubernetes)와 통합되어 컨테이너별 메모리 사용량을 실시간으로 모니터링하고 제어할 수 있게 합니다. 컨테이너 메모리 관리의 핵심은 메모리 한계 설정, OOM 처리, 스왑 동작, 통계 모니터링이며, 이를 통해 시스템 안정성과 자원 활용도를 최적화할 수 있습니다.
일상 비유로 보면 컨테이너 메모리 관리는 아파트 단지의 전력 관리와 같습니다. 각 세대(컨테이너)는 별도의 전력 계량기(memory.current)와 차단기(memory.max)를 가지며, 관리사무소(커널)는 각 세대의 사용량을 모니터링하고 과도한 사용 시 경고(memory.high) 또는 차단(memory.max 초과 시 OOM)을 수행합니다.
// 소스 파일 경로
mm/memcontrol.c // 메모리 컨트롤러 핵심 구현 (5679줄)
mm/memcontrol-v1.c // cgroup v1 레거시 인터페이스
include/linux/memcontrol.h // 핵심 구조체 정의 (1939줄)
include/linux/page_counter.h // 페이지 카운터 기반 계정 (112줄)
mm/oom_kill.c // OOM Killer 통합
# 1. 메모리 cgroup 계층 구조 확인 (cgroup v2)
ls /sys/fs/cgroup/
cat /sys/fs/cgroup/cgroup.controllers # 사용 가능한 컨트롤러
cat /sys/fs/cgroup/cgroup.subtree_control # 하위에 활성화된 컨트롤러
# 2. 루트 메모리 현황
cat /sys/fs/cgroup/memory.current # 현재 사용량 (바이트)
cat /sys/fs/cgroup/memory.max # 하드 제한 (bytes 또는 max)
cat /sys/fs/cgroup/memory.high # 소프트 제한 (throttle 기준)
cat /sys/fs/cgroup/memory.low # 하위 보호량
cat /sys/fs/cgroup/memory.min # 절대 보호량
# 3. Docker 컨테이너별 메모리 사용량
for id in $(docker ps -q); do
echo "=== Container: $id ==="
docker inspect --format '{{.Name}}' $id
cat /sys/fs/cgroup/docker/$id/memory.current 2>/dev/null || \
cat /sys/fs/cgroup/system.slice/docker-$(docker inspect --format '{{.Id}}' $id).scope/memory.current 2>/dev/null
cat /sys/fs/cgroup/docker/$id/memory.max 2>/dev/null || \
cat /sys/fs/cgroup/system.slice/docker-$(docker inspect --format '{{.Id}}' $id).scope/memory.max 2>/dev/null
done
# 4. Kubernetes Pod별 메모리 사용량
kubectl top pods --namespace=default --sort-by=memory
# 5. 메모리 이벤트 확인 (OOM, high 등)
cat /sys/fs/cgroup/memory.events
# oom: memcg OOM 발생 횟수
# oom_kill: OOM killer에 의해 종료된 프로세스 수
# high: memory.high 초과 회수 시도 횟수
# 6. 프로세스의 memcg 소속 확인
cat /proc/$$/cgroup
# 0::/system.slice/docker-<ID>.scope (컨테이너 내부)
# 7. 컨테이너 메모리 통계
cat /sys/fs/cgroup/memory.stat | head -20
# 8. 스왑 사용량 확인
cat /sys/fs/cgroup/memory.swap.current
cat /sys/fs/cgroup/memory.swap.max
# 9. memcg OOM 그룹 킬 설정
cat /sys/fs/cgroup/memory.oom.group # 0 또는 1
# 10. 컨테이너 메모리 제한 테스트
# 메모리 256MB 제한으로 컨테이너 실행
docker run --memory=256m --memory-swap=512m alpine sh -c '
echo "Memory limit: $(cat /sys/fs/cgroup/memory.max)"
echo "Current usage: $(cat /sys/fs/cgroup/memory.current)"
'
# 11. cgroup v2 컨트롤러 활성화 확인
cat /sys/fs/cgroup/cgroup.controllers
# memory cpu io pids 등이 나와야 함
# 12. 컨테이너 메모리 압력 확인 (PSI)
cat /proc/pressure/memory
메모리 컨트롤러의 핵심 구조체. 각 컨테이너 cgroup에 하나씩 생성되며, 페이지 계정, 한계 설정, 통계, 회수 이터레이터 등을 포함합니다.
// include/linux/memcontrol.h:190-325
struct mem_cgroup {
struct cgroup_subsys_state css; // cgroup 상태 (부모-자식 관계)
/* memcg 고유 ID. cgroup보다 오래 사는 오브젝트 식별에 사용 */
struct mem_cgroup_private_id id;
/* 계정 대상 자원 */
struct page_counter memory; // 메모리 사용량/한계 (v1 & v2)
union {
struct page_counter swap; // 스왑 사용량/한계 (v2 only)
struct page_counter memsw; // 메모리+스왑 (v1 only)
};
/* 높은 사용량 감시 등록자 */
struct list_head memory_peaks;
struct list_head swap_peaks;
spinlock_t peaks_lock;
/* 인터럽트 상황에서의 한계 강제 실행 */
struct work_struct high_work;
/* vmpressure 알림 */
struct vmpressure vmpressure;
/* OOM 시 그룹 전체 종료 여부 */
bool oom_group;
/* 스왑 사용 비율 (0-200, v2에서 확장) */
int swappiness;
/* memory.events 통계 */
struct cgroup_file events_file;
atomic_long_t memory_events[MEMCG_NR_MEMORY_EVENTS];
/* 메모리 통계 */
struct memcg_vmstats *vmstats;
struct memcg_vmstats_percpu __percpu *vmstats_percpu;
/* per-node 정보 (NUMA) */
struct mem_cgroup_per_node *nodeinfo[];
};
// include/linux/memcontrol.h:324
페이지 할당 시 계정을 수행하는 카운터. 부모-자식 계층 구조를 가지며, 한계 초과 시 회수/스로틀링이 발생합니다.
// include/linux/page_counter.h:10-44
struct page_counter {
/* v2에서 메모리 사용량 핫 멤버 (캐시라인 분리) */
atomic_long_t usage; // 현재 사용량 (페이지 수)
unsigned long failcnt; // v1 전용 실패 횟수
CACHELINE_PADDING(_pad1_); // 캐시라인 분리
/* memory.min/low 사용량 추적 */
unsigned long emin; // 유효 memory.min
atomic_long_t min_usage; // memory.min 사용량
atomic_long_t children_min_usage; // 자식들의 min 사용량 합
unsigned long elow; // 유효 memory.low
atomic_long_t low_usage; // memory.low 사용량
atomic_long_t children_low_usage; // 자식들의 low 사용량 합
unsigned long watermark; // 피크 사용량
unsigned long local_watermark; // 로컬 피크 (cg2 리셋용)
CACHELINE_PADDING(_pad2_); // 캐시라인 분리
bool protection_support; // 보호 기능 지원 여부
bool track_failcnt; // 실패 횟수 추적 여부
unsigned long min; // 절대 보호량 (최소 보장)
unsigned long low; // 소프트 보호량 (가능하면 보장)
unsigned long high; // 소프트 제한 (초과 시 스로틀)
unsigned long max; // 하드 제한 (초과 시 OOM)
struct page_counter *parent; // 부모 카운터 (계층 구조)
} ____cacheline_internodealigned_in_smp;
// include/linux/page_counter.h:44
커널 메모리(slab 등)를 계정하는 오브젝트 컨트롤러. 메모리 컨트롤러와 별도로 관리되며, 컨테이너별 커널 메모리 사용량을 추적합니다.
// include/linux/memcontrol.h:174-182
struct obj_cgroup {
struct percpu_ref refcnt; // 참조 카운트
struct mem_cgroup *memcg; // 연결된 메모리 컨트롤러
atomic_t nr_charged_bytes; // 계정된 바이트 수
union {
struct list_head list; // objcg_lock로 보호
struct rcu_head rcu; // RCU 해제용
};
};
// include/linux/memcontrol.h:182
NUMA 노드별 memcg 정보. 각 노드의 LRU 리스트, 회수 이터레이터, shrinker 정보를 포함합니다.
// include/linux/memcontrol.h:88-123
struct mem_cgroup_per_node {
struct mem_cgroup *memcg; // 역방향 포인터
struct lruvec_stats_percpu __percpu *lruvec_stats_percpu;
struct lruvec_stats *lruvec_stats;
struct shrinker_info __rcu *shrinker_info;
/* v1 전용 필드 (RB 트리, 소프트 리밋) */
struct rb_node tree_node;
unsigned long usage_in_excess;
bool on_tree;
/* 자주 업데이트되는 필드 */
struct lruvec lruvec; // LRU 리스트 (활성/비활성/비_evict)
unsigned long lru_zone_size[MAX_NR_ZONES][NR_LRU_LISTS];
struct mem_cgroup_reclaim_iter iter; // 회수 이터레이터
};
// include/linux/memcontrol.h:123
메모리 할당 시 memcg에 페이지를 계정하는 핵심 함수. 한계 초과 시 회수, 스로틀링, OOM 처리를 수행합니다.
// mm/memcontrol.c:2355-2548
static int try_charge_memcg(struct mem_cgroup *memcg, gfp_t gfp_mask,
unsigned int nr_pages)
{
unsigned int batch = max(MEMCG_CHARGE_BATCH, nr_pages);
int nr_retries = MAX_RECLAIM_RETRIES;
struct mem_cgroup *mem_over_limit;
struct page_counter *counter;
retry:
/* 1. per-CPU 캐시에서 먼저 차감 시도 */
if (consume_stock(memcg, nr_pages))
return 0;
/* 2. page_counter에 계정 시도 (한계 초과 시 실패) */
if (!do_memsw_account() ||
page_counter_try_charge(&memcg->memsw, batch, &counter)) {
if (page_counter_try_charge(&memcg->memory, batch, &counter))
goto done_restock; // 성공
/* memory 한계 초과 */
mem_over_limit = mem_cgroup_from_counter(counter, memory);
} else {
/* memsw 한계 초과 */
mem_over_limit = mem_cgroup_from_counter(counter, memsw);
reclaim_options &= ~MEMCG_RECLAIM_MAY_SWAP;
}
/* 3. 직접 회수 시도 */
nr_reclaimed = try_to_free_mem_cgroup_pages(mem_over_limit, nr_pages,
gfp_mask, reclaim_options, NULL);
/* 4. 회수 후 재시도 */
if (mem_cgroup_margin(mem_over_limit) >= nr_pages)
goto retry;
/* 5. 모든 재시도 실패 시 memcg OOM killer 호출 */
if (mem_cgroup_oom(mem_over_limit, gfp_mask,
get_order(nr_pages * PAGE_SIZE))) {
passed_oom = true;
nr_retries = MAX_RECLAIM_RETRIES;
goto retry;
}
nomem:
/* 6. __GFP_NOFAIL 또는 __GFP_HIGH가 아니면 실패 반환 */
if (!(gfp_mask & (__GFP_NOFAIL | __GFP_HIGH)))
return -ENOMEM;
force:
/* 7. 강제 계정 (임시 한계 초과 허용) */
page_counter_charge(&memcg->memory, nr_pages);
return 0;
}
// mm/memcontrol.c:2355-2548
memory.high 초과 시 사용자를 유저 모드로 복귀하면서 스로틀링을 수행하는 함수. 회수와 지연(jiffies)을 계산하여 프로세스를 늦춥니다.
// mm/memcontrol.c:2265-2353
void __mem_cgroup_handle_over_high(gfp_t gfp_mask)
{
unsigned long penalty_jiffies;
unsigned long nr_reclaimed;
unsigned int nr_pages = current->memcg_nr_pages_over_high;
struct mem_cgroup *memcg;
memcg = get_mem_cgroup_from_mm(current->mm);
current->memcg_nr_pages_over_high = 0;
retry_reclaim:
/* 이미 종료 중인 프로세스는 회수하지 않음 */
if (task_is_dying())
goto out;
/* memory.high 초과분만큼 회수 시도 */
nr_reclaimed = reclaim_high(memcg,
in_retry ? SWAP_CLUSTER_MAX : nr_pages, gfp_mask);
/* 스로틀 지연 계산 (오버레이 비율 기반) */
penalty_jiffies = calculate_high_delay(memcg, nr_pages,
mem_find_max_overage(memcg));
penalty_jiffies = min(penalty_jiffies, MEMCG_MAX_HIGH_DELAY_JIFFIES);
/* 10ms 미만이면 스로틀하지 않음 (부드러운 전환) */
if (penalty_jiffies <= HZ / 100)
goto out;
/* 회수 진행 중이면 재시도 */
if (nr_reclaimed || nr_retries--) {
in_retry = true;
goto retry_reclaim;
}
/* 유저 모드 복귀 시 스로틀 (killable) */
psi_memstall_enter(&pflags);
schedule_timeout_killable(penalty_jiffies);
psi_memstall_leave(&pflags);
out:
css_put(&memcg->css);
}
// mm/memcontrol.c:2265-2353
새로운 memcg CSS를 할당하고 초기화하는 함수. 컨테이너 생성 시 호출됩니다.
// mm/memcontrol.c:3822-3873
static struct cgroup_subsys_state * __ref
mem_cgroup_css_alloc(struct cgroup_subsys_state *parent_css)
{
struct mem_cgroup *parent = mem_cgroup_from_css(parent_css);
struct mem_cgroup *memcg;
memcg = mem_cgroup_alloc(parent); // 메모리 할당 및 초기화
if (IS_ERR(memcg))
return ERR_CAST(memcg);
/* 기본 high 제한은 최대값 (제한 없음) */
page_counter_set_high(&memcg->memory, PAGE_COUNTER_MAX);
page_counter_set_high(&memcg->swap, PAGE_COUNTER_MAX);
if (parent) {
/* 부모 memcg가 있으면 계층 구조 초기화 */
page_counter_init(&memcg->memory, &parent->memory, memcg_on_dfl);
page_counter_init(&memcg->swap, &parent->swap, false);
/* v1 레거시: swappiness, oom_kill_disable 상속 */
WRITE_ONCE(memcg->swappiness, mem_cgroup_swappiness(parent));
} else {
/* 루트 memcg 초기화 */
page_counter_init(&memcg->memory, NULL, true);
page_counter_init(&memcg->swap, NULL, false);
root_mem_cgroup = memcg;
return &memcg->css;
}
return &memcg->css;
}
// mm/memcontrol.c:3822-3873
OOM 발생 시 그룹 전체 종료(oom.group)를 위한 memcg를 찾는 함수. 컨테이너 전체를 종료해야 할 때 사용됩니다.
// mm/memcontrol.c:1735-1780
struct mem_cgroup *mem_cgroup_get_oom_group(struct task_struct *victim,
struct mem_cgroup *oom_domain)
{
struct mem_cgroup *oom_group = NULL;
struct mem_cgroup *memcg;
/* cgroup v2에서만 지원 */
if (!cgroup_subsys_on_dfl(memory_cgrp_subsys))
return NULL;
if (!oom_domain)
oom_domain = root_mem_cgroup;
rcu_read_lock();
memcg = mem_cgroup_from_task(victim);
/* OOM 도메인 내의 하위 memcg인지 확인 */
if (unlikely(!mem_cgroup_is_descendant(memcg, oom_domain)))
goto out;
/* 피해자 memcg에서 루트까지 oom_group 플래그 확인 */
for (; memcg; memcg = parent_mem_cgroup(memcg)) {
if (READ_ONCE(memcg->oom_group))
oom_group = memcg; // 가장 높은 oom_group memcg 선택
if (memcg == oom_domain)
break;
}
if (oom_group)
css_get(&oom_group->css);
out:
rcu_read_unlock();
return oom_group;
}
// mm/memcontrol.c:1735-1780
컨테이너에서 메모리 할당 시:
__alloc_pages
→ __alloc_pages_slowpath
→ try_to_free_mem_cgroup_pages (회수 필요 시)
→ __mem_cgroup_charge
→ charge_memcg
→ try_charge_memcg
→ consume_stock (per-CPU 캐시 확인)
→ page_counter_try_charge (한계 검사)
→ try_to_free_mem_cgroup_pages (직접 회수)
→ mem_cgroup_oom (OOM 처리)
→ commit_charge ( folio에 memcg 연결)
memory.high 초과 시:
try_charge_memcg → page_counter 성공
→ high_work scheduled
→ __mem_cgroup_handle_over_high
→ reclaim_high (회수)
→ calculate_high_delay (지연 계산)
→ schedule_timeout_killable (스로틀)
컨테이너 OOM 시:
mem_cgroup_oom
→ mem_cgroup_out_of_memory
→ mem_cgroup_get_oom_group (oom.group 확인)
→ oom_kill_process (프로세스 종료)
→ mem_cgroup_scan_tasks (그룹 전체 종료)
| 수준 | cgroup v2 파일 | 동작 | 용도 |
|---|---|---|---|
| memory.min | 절대 보호 | 이 양만큼은 어떤 경우에도 회수하지 않음 | 핵심 서비스 보장 |
| memory.low | 소프트 보호 | 가능하면 보장, 부모 memcg 잔여량 있으면 회수 | 일반 서비스 보호 |
| memory.high | 소프트 제한 | 초과 시 회수 + 스로틀링 (지연) | 과사용 억제 |
| memory.max | 하드 제한 | 초과 시 memcg OOM killer 호출 | 절대 제한 |
| 조건 | 동작 | 결과 |
|---|---|---|
| memory.max 초과 + 회수 실패 | mem_cgroup_oom → mem_cgroup_out_of_memory | memcg 내 프로세스 종료 |
| memory.oom.group = 1 | mem_cgroup_get_oom_group → 그룹 전체 종료 | 컨테이너 전체 종료 |
| memory.oom.group = 0 | oom_badness → 가장 높은 oom_score 프로세스만 종료 | 단일 프로세스 종료 |
| __GFP_NOFAIL 할당 | force_charge (임시 한계 초과 허용) | 할당 성공 |
| 항목 | Docker | Kubernetes |
|---|---|---|
| 제한 설정 | `--memory=256m` | `resources.limits.memory: 256Mi` |
| 예약 설정 | `--memory-reservation=128m` | `resources.requests.memory: 128Mi` |
| 스왑 설정 | `--memory-swap=512m` | (스왑 비활성화 권장) |
| OOM 처리 | 컨테이너 재시작 (restart policy) | Pod 재스케줄링 (OOMKilled 상태) |
| 모니터링 | `docker stats` | `kubectl top pods` |
| 메모리 이벤트 | `/sys/fs/cgroup/memory.events` | `kubectl describe pod` |
| 상황 | memory.high 초과 | memory.max 초과 |
|---|---|---|
| 즉시 동작 | 회수 시도 + 스로틀링 | 회수 시도 |
| 프로세스 영향 | 유저 모드 복귀 시 지연 (killable) | 프로세스 종료 |
| 복구 방법 | 메모리 사용량 감소 시 자동 복구 | 컨테이너 재시작 필요 |
| 시스템 영향 | 해당 컨테이너만 영향 | 해당 컨테이너 + 시스템 안정성 |
# 메모리 제한 + 예약 설정
docker run \
--memory=512m \ # 하드 제한 (memory.max)
--memory-reservation=256m \ # 소프트 보호 (memory.low)
--memory-swap=1g \ # 메모리+스왑 합계 제한
--oom-kill-disable \ # OOM killer 비활성화 (위험)
--cpus=2 \
myapp
# 메모리 제한 확인
docker inspect --format '{{.HostConfig.Memory}}' <container_id>
docker stats <container_id>
apiVersion: v1
kind: Pod
metadata:
name: memory-demo
spec:
containers:
- name: memory-demo-ctr
image: polinux/stress
resources:
requests:
memory: "64Mi" # 예약량 (调度 시 참고)
limits:
memory: "128Mi" # 하드 제한 (memory.max)
# 스왑 비활성화 (권장)
# terminationGracePeriodSeconds: 30
# 메모리 100MB 제한으로 stress 테스트
docker run --rm -m 100m progrium/stress \
--vm 1 --vm-bytes 120M --timeout 10s
# 결과 확인
# stress: '--vm 1 --vm-bytes 120M' exceeds memory limit
# 또는 OOM killer에 의해 프로세스 종료
# memcg 이벤트 확인
cat /sys/fs/cgroup/memory.events
# oom_kill 1 ← OOM 발생 횟수
#!/bin/bash
# 컨테이너 메모리 사용량 모니터링
while true; do
echo "=== $(date) ==="
for id in $(docker ps -q); do
NAME=$(docker inspect --format '{{.Name}}' $id)
CGROUP_PATH=$(docker inspect --format '{{.HostConfig.CgroupParent}}' $id 2>/dev/null)
if [ -f "/sys/fs/cgroup${CGROUP_PATH}/memory.current" ]; then
USAGE=$(cat /sys/fs/cgroup${CGROUP_PATH}/memory.current)
MAX=$(cat /sys/fs/cgroup${CGROUP_PATH}/memory.max)
EVENTS=$(cat /sys/fs/cgroup${CGROUP_PATH}/memory.events)
echo "$NAME: usage=$(($USAGE/1024/1024))MB max=$MAX events=$EVENTS"
fi
done
sleep 5
done