Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

컨테이너 메모리 관리 실전

개요 (Overview)

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

핵심 자료구조

struct mem_cgroup

메모리 컨트롤러의 핵심 구조체. 각 컨테이너 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

struct page_counter

페이지 할당 시 계정을 수행하는 카운터. 부모-자식 계층 구조를 가지며, 한계 초과 시 회수/스로틀링이 발생합니다.

// 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

struct obj_cgroup

커널 메모리(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

struct mem_cgroup_per_node

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

핵심 함수

try_charge_memcg

메모리 할당 시 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

__mem_cgroup_handle_over_high

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

mem_cgroup_css_alloc

새로운 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

mem_cgroup_get_oom_group

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 호출절대 제한

컨테이너 OOM 처리 경로 비교

조건동작결과
memory.max 초과 + 회수 실패mem_cgroup_oom → mem_cgroup_out_of_memorymemcg 내 프로세스 종료
memory.oom.group = 1mem_cgroup_get_oom_group → 그룹 전체 종료컨테이너 전체 종료
memory.oom.group = 0oom_badness → 가장 높은 oom_score 프로세스만 종료단일 프로세스 종료
__GFP_NOFAIL 할당force_charge (임시 한계 초과 허용)할당 성공

Docker vs Kubernetes 메모리 관리 비교

항목DockerKubernetes
제한 설정`--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`

스로틀 vs OOM 동작 비교

상황memory.high 초과memory.max 초과
즉시 동작회수 시도 + 스로틀링회수 시도
프로세스 영향유저 모드 복귀 시 지연 (killable)프로세스 종료
복구 방법메모리 사용량 감소 시 자동 복구컨테이너 재시작 필요
시스템 영향해당 컨테이너만 영향해당 컨테이너 + 시스템 안정성

관련 문서

  • Memory Cgroup - memcg 상세 구현
  • OOM Killer - OOM 처리 메커니즘
  • 페이지 회수 - kswapd/direct reclaim
  • Swap / zswap - 스왑 관리
  • Compaction - 메모리 단편화 해소
  • NUMA - NUMA 인식 메모리 관리
  • MGLRU - 차세대 LRU 개선사항

  • 실전 튜닝 가이드

    Docker 컨테이너 메모리 설정

    # 메모리 제한 + 예약 설정
    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>

    Kubernetes Pod 메모리 설정

    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