메모리 컴팩션 (Memory Compaction)

리눅스 커널의 Memory Compaction 메커니즘을 심층 분석합니다. Buddy Allocator에서 발생하는 외부 단편화(external fragmentation) 문제를 해결하기 위한 Two-Scanner 알고리즘(migrate scanner + free scanner), migrate type별 페이지(Page) 분류(UNMOVABLE/MOVABLE/RECLAIMABLE/CMA), kcompactd 데몬 동작, Direct Compaction과 Proactive Compaction 경로, fragmentation index 계산, CMA 및 THP와의 상호작용, ftrace 이벤트 기반 디버깅(Debugging), vmstat 카운터 분석, 운영 튜닝 플레이북까지 커널 소스(mm/compaction.c, mm/migrate.c) 기반으로 분석합니다.

전제 조건: 메모리 관리(Memory Management) 개요페이지 할당자(Page Allocator) 문서를 먼저 읽으세요. Memory Compaction은 Buddy Allocator의 외부 단편화를 해소하는 메커니즘이므로 페이지 할당 구조를 이해해야 합니다.
일상 비유: Memory Compaction은 주차장 재배치(Relocation)와 비슷합니다. 빈 공간이 여러 곳에 흩어져 있으면 큰 차(고차 할당)가 주차할 수 없습니다. 작은 차들을 한쪽으로 모아 연속 빈 공간을 만드는 과정이 compaction입니다.

핵심 요약

  • 외부 단편화 해소 -- 물리 메모리(Physical Memory)에 빈 페이지가 충분하지만 연속되지 않아 고차(order) 할당이 실패하는 문제를 해결합니다.
  • Two-Scanner -- migrate scanner(존 시작부터 이동 가능 페이지 탐색)와 free scanner(존 끝부터 빈 페이지 탐색)가 양쪽에서 만날 때까지 동작합니다.
  • 3가지 트리거 -- Direct Compaction(할당 실패 시 즉시), kcompactd(워터마크(Watermark) 기반 백그라운드), Proactive Compaction(단편화 점수 기반 사전 예방).
  • migrate type 구분 -- UNMOVABLE(커널 slab 등), MOVABLE(사용자 페이지), RECLAIMABLE(파일 캐시(Cache)), CMA(연속 메모리 예약)로 분류하여 compaction 대상을 결정합니다.
  • 성능 영향 -- Direct Compaction은 할당 지연(Latency)을 수백 ms까지 증가시킬 수 있으므로 Proactive Compaction으로 사전 방지가 중요합니다.

단계별 이해

  1. 외부 단편화 개념 파악
    Buddy Allocator에서 order-0 페이지는 충분하지만 order-3 이상 연속 블록을 만들 수 없는 상황을 이해합니다.
  2. migrate type 분류 이해
    각 pageblock(보통 2MB)이 어떤 migrate type으로 분류되는지, 왜 이 분류가 compaction에 중요한지 파악합니다.
  3. Two-Scanner 알고리즘 추적
    migrate scanner와 free scanner가 존의 양 끝에서 출발하여 이동 가능한 페이지를 빈 페이지 위치로 옮기는 과정을 따라갑니다.
  4. 트리거 경로 구분
    Direct, kcompactd, Proactive 각 트리거의 진입 조건과 동작 차이를 구분합니다.
  5. 모니터링과 튜닝
    vmstat 카운터, ftrace 이벤트, /proc 인터페이스로 compaction 상태를 진단하고 최적화합니다.
관련 커널 소스: mm/compaction.c (핵심 compaction 로직), mm/migrate.c (페이지 마이그레이션), mm/page_alloc.c (Buddy Allocator와 direct compaction 트리거), include/linux/compaction.h (API 선언). Mel Gorman의 커밋 시리즈(v2.6.35, "Memory compaction")가 최초 구현이며, Vlastimil Babka의 proactive compaction(v5.9)이 최근 주요 개선입니다.

왜 필요한가: 외부 단편화 문제

Buddy Allocator는 물리 메모리를 2의 거듭제곱 크기 블록(order 0 = 4KB, order 1 = 8KB, ..., order 10 = 4MB)으로 관리합니다. 시스템이 오래 동작하면 할당과 해제가 반복되면서 외부 단편화(external fragmentation)가 발생합니다. 전체 빈 페이지 수는 충분하지만, 연속된 물리 페이지가 부족하여 고차 할당이 실패하는 상황입니다.

외부 단편화의 실제 영향

할당 요구필요 order연속 페이지단편화 시 영향
일반 페이지 할당order-01 페이지 (4KB)영향 없음
네트워크 버퍼 (jumbo)order-1~32~8 페이지중간 영향
THP (Transparent Huge Page)order-9512 페이지 (2MB)심각한 영향
CMA 연속 할당가변수백~수천 페이지심각한 영향
hugetlbfs (1GB)order-18262144 페이지부팅 시 예약 필수
단편화 ≠ 메모리 부족: /proc/meminfoMemFree가 충분해도 /proc/buddyinfo에서 고차 order의 빈 블록이 0이면 고차 할당은 실패합니다. 이때 OOM Killer가 호출되지 않고 compaction이 먼저 시도됩니다.
외부 단편화 vs Compaction 후 메모리 상태 단편화 상태 (Before) 사용 중 페이지 빈 페이지 (흩어져 있음) order-3 (8 연속 페이지) 할당 불가! Compaction 수행 Compaction 후 (After) order-3 이상 연속 할당 가능! Before /proc/buddyinfo: Node 0, zone Normal: 6 0 0 0 0 0 0 0 0 0 0 After /proc/buddyinfo: Node 0, zone Normal: 1 1 1 1 0 0 0 0 0 0 0 * 실제 커널에서는 pageblock(2MB) 단위로 migrate type을 구분하여 compaction을 수행합니다
Compaction은 흩어진 빈 페이지를 한쪽으로 모아 연속된 고차 블록을 생성합니다

Buddy Allocator의 /proc/buddyinfo를 보면 단편화 상태를 직접 확인할 수 있습니다:

# 단편화 확인
cat /proc/buddyinfo
# Node 0, zone   Normal  312  156   78   39   12    3    1    0    0    0    0
#                         ^order-0              ^order-4         ^order-7 이상 없음

# 페이지 타입별 분포 확인
cat /proc/pagetypeinfo
# Free pages count per migrate type at order
# Node    Zone    Type       0     1     2     3     4     5
# Node 0  Normal  Unmovable  45    12     3     0     0     0
# Node 0  Normal  Movable   200    80    30    10     2     1
# Node 0  Normal  Reclaimable 67   20     5     1     0     0
설명 /proc/buddyinfo는 각 order별 빈 블록 수를 보여줍니다. 고차 order(7 이상)의 값이 0이면 2MB THP 할당이 불가능합니다. /proc/pagetypeinfo는 migrate type별로 분류하여 어떤 종류의 페이지가 단편화를 유발하는지 진단할 수 있습니다.

Two-Scanner 알고리즘

Memory Compaction의 핵심은 Two-Scanner 알고리즘입니다. 하나의 존(zone) 내에서 두 개의 스캐너가 양쪽 끝에서 출발하여 서로를 향해 진행합니다:

두 스캐너가 만나면 compaction 라운드가 종료됩니다. 격리된 이동 가능 페이지를 빈 페이지 위치로 마이그레이션하면, 존의 아래쪽에 사용 중인 페이지가 모이고 위쪽에 빈 페이지가 연속으로 생깁니다.

Two-Scanner 알고리즘: Migrate Scanner + Free Scanner Zone 시작 (낮은 PFN) Zone 끝 (높은 PFN) Phase 1: 스캐너 시작 Migrate Scanner --> <-- Free Scanner Phase 2: 페이지 격리 및 마이그레이션 빈 페이지 위치로 이동 두 스캐너 만남 -- compaction 완료 compact_zone() 주요 루프: isolate_migratepages() isolate_freepages() migrate_pages() migrate_pfn < free_pfn 동안 반복 * cc->migrate_pfn: migrate scanner 현재 위치 | cc->free_pfn: free scanner 현재 위치 | cc: struct compact_control
migrate scanner는 이동 가능한 페이지를, free scanner는 빈 페이지를 격리하여 페이지를 존의 한쪽으로 모읍니다

compact_zone() 핵심 구현

/* mm/compaction.c - compact_zone() 핵심 루프 (단순화) */
static enum compact_result compact_zone(struct compact_control *cc)
{
    /* migrate scanner: 존 시작부터 위쪽으로 */
    cc->migrate_pfn = cc->zone->compact_cached_migrate_pfn[...];
    /* free scanner: 존 끝부터 아래쪽으로 */
    cc->free_pfn = cc->zone->compact_cached_free_pfn;

    while ((ret = compact_finished(cc)) == COMPACT_CONTINUE) {
        /* 1. migrate scanner: 이동 가능 페이지 격리 */
        isolate_migratepages(cc);

        /* 2. free scanner: 빈 페이지 격리 */
        if (list_empty(&cc->migratepages))
            continue;

        /* 3. 실제 마이그레이션 수행 */
        err = migrate_pages(&cc->migratepages, compaction_alloc,
                           compaction_free, (unsigned long)cc,
                           cc->mode, MR_COMPACTION, NULL);
    }
    return ret;
}
설명 compact_zone()struct compact_control을 통해 스캐너 위치를 추적합니다. compact_finished()는 두 스캐너가 만났거나 요청된 order의 블록이 충분히 확보되었는지 확인합니다. compaction_alloc()은 free scanner가 격리한 빈 페이지를 마이그레이션 대상으로 제공하는 콜백(Callback)입니다.

스캐너 위치 캐싱

매번 존의 시작/끝부터 스캔하면 비효율적이므로, 각 존은 마지막 스캔 위치를 캐싱합니다:

struct zone {
    /* ... */
    /* Compaction scanner 캐시 */
    unsigned long compact_cached_migrate_pfn[ASYNC_AND_SYNC];
    unsigned long compact_cached_free_pfn;
    /* ... */
};

ASYNC_AND_SYNC 배열로 비동기/동기 모드별 별도 캐시를 유지합니다. Compaction이 완료되거나 존 전체를 스캔한 경우 캐시가 리셋됩니다.

Migrate Type: 페이지 이동(Page Migration) 가능성 분류

커널은 물리 메모리를 pageblock(일반적으로 2MB = pageblock_order 크기) 단위로 migrate type을 지정합니다. 이 분류는 compaction이 어떤 페이지를 이동시킬 수 있는지를 결정합니다.

Migrate Type설명Compaction 이동 가능예시
MIGRATE_UNMOVABLE이동 불가능한 페이지불가커널 slab, 페이지 테이블(Page Table), kmalloc
MIGRATE_MOVABLE이동 가능한 페이지가능사용자 프로세스(Process) anon/file 페이지
MIGRATE_RECLAIMABLE회수 가능한 페이지회수 후 가능페이지 캐시(Page Cache), dentry/inode 캐시
MIGRATE_PCPTYPESper-CPU 페이지 캐시 경계-(분류 기준값, 실제 타입 아님)
MIGRATE_HIGHATOMIC고우선순위 원자적(Atomic) 할당용불가GFP_ATOMIC 예약 블록
MIGRATE_CMACMA 예약 영역조건부 가능DMA 연속 할당 예약
MIGRATE_ISOLATE격리 중 (hot-remove 등)-메모리 핫플러그(Hotplug) 진행 중

pageblock_flags 관리

/* include/linux/pageblock-flags.h */
enum pageblock_bits {
    PB_migrate,
    PB_migrate_end = PB_migrate + 3 - 1,  /* 3비트: 8가지 타입 */
    PB_migrate_skip,                        /* compaction 스캐너 skip 비트 */
    NR_PAGEBLOCK_BITS
};

/* pageblock의 migrate type 설정 */
void set_pageblock_migratetype(struct page *page, int migratetype)
{
    /* pageblock의 첫 번째 page의 flags에 3비트로 인코딩 */
    set_pfnblock_flags_mask(page, (unsigned long)migratetype,
                           page_to_pfn(page), MIGRATETYPE_MASK);
}

/* pageblock의 migrate type 조회 */
int get_pageblock_migratetype(struct page *page)
{
    return get_pfnblock_flags_mask(page, page_to_pfn(page),
                                   MIGRATETYPE_MASK);
}
설명 각 pageblock(2MB)의 첫 번째 struct page에 3비트로 migrate type을 저장합니다. PB_migrate_skip 비트는 compaction 스캐너가 이미 방문한 pageblock을 건너뛰도록 합니다. 할당 시 fallback 순서(MOVABLE -> RECLAIMABLE -> UNMOVABLE)에 따라 다른 migrate type의 블록을 빌려올 수 있으며, 이때 pageblock의 type이 변경될 수 있습니다.
Fallback 메커니즘: Buddy Allocator에서 요청한 migrate type의 빈 블록이 없으면 다른 type에서 빌려옵니다. 예를 들어 MOVABLE 할당 요청에 MOVABLE 블록이 없으면 RECLAIMABLE이나 UNMOVABLE 블록에서 가져옵니다. 이때 빌려온 pageblock 전체의 migrate type이 변경되어 장기적으로 UNMOVABLE 영역이 확산될 수 있습니다.
존(Zone) 내 Pageblock 배치와 Migrate Type Fallback 오염 UNMOVABLE MOVABLE RECLAIMABLE CMA 오염된 블록 초기 상태 (이상적 배치) Zone UNMOVABLE UNMOVABLE MOVABLE MOVABLE MOVABLE RECLAIMABLE CMA CMA 2MB (pageblock) Fallback 발생: MOVABLE 블록 부족 → UNMOVABLE에서 차용 Fallback 후 상태 (오염 진행) Zone UNMOVABLE MOVABLE (was UNMOV) MOVABLE MOVABLE MOVABLE RECLAIMABLE CMA CMA fallback 차용 장기 운영 후: UNMOVABLE 산재 → 단편화 단편화된 상태 (UNMOVABLE 산재) Zone MOVABLE UNMOVABLE MOVABLE UNMOVABLE RECLAIMABLE UNMOVABLE MOVABLE CMA × × × × = UNMOVABLE 블록은 이동 불가 → 연속 물리 메모리 확보 차단 Fallback 순서: MOVABLE → RECLAIMABLE → UNMOVABLE → HIGHATOMIC pageblock 전체의 migrate type이 요청자의 type으로 변경(steal)됩니다. Compaction은 MOVABLE/RECLAIMABLE 페이지만 이동 가능 — UNMOVABLE 산재 시 효과 감소
그림: Pageblock Migrate Type 배치와 Fallback에 의한 오염·단편화 과정

페이지 격리와 마이그레이션 경로

Compaction은 두 단계로 진행됩니다: (1) 페이지 격리(isolation) -- LRU 리스트에서 분리, (2) 페이지 마이그레이션(migration) -- 새 위치로 복사 후 매핑(Mapping) 갱신.

페이지 격리 및 마이그레이션 경로 1. 이동 페이지 격리 isolate_migratepages_block() - PageLRU 확인 - MOVABLE type 확인 - LRU에서 분리 2. 빈 페이지 격리 isolate_freepages_block() - Buddy에서 빈 페이지 분리 - freelist에서 제거 - cc->freepages에 추가 3. 마이그레이션 migrate_pages() - 새 페이지에 복사 - PTE 갱신 (rmap) - 원본 페이지 해제 migrate_pages() 내부 단계 unmap_and_move() 페이지별 처리 trylock_page() 페이지 잠금 move_to_new_folio() 내용 복사 remove_migration _ptes() 원본 해제 put_page() PTE 갱신 과정 (rmap) migration entry 설치 페이지 내용 복사 새 PTE로 교체 (flush TLB) 핵심: Migration Entry 마이그레이션 중 원본 PTE를 migration entry(스왑 유사 엔트리)로 교체합니다. 다른 프로세스가 해당 페이지에 접근하면 page fault가 발생하고, 마이그레이션 완료를 대기합니다.
페이지 격리 -> 내용 복사 -> PTE 갱신 -> 원본 해제 순서로 마이그레이션이 진행됩니다

isolate_migratepages_block() 상세

/* mm/compaction.c - 이동 가능 페이지 격리 (단순화) */
static bool isolate_migratepages_block(struct compact_control *cc,
                                       unsigned long low_pfn,
                                       unsigned long end_pfn,
                                       isolate_mode_t mode)
{
    for (; low_pfn < end_pfn; low_pfn++) {
        struct page *page = pfn_to_page(low_pfn);

        /* skip 비트가 설정된 pageblock 건너뛰기 */
        if (get_pageblock_skip(page))
            continue;

        /* 격리 불가 조건 확인 */
        if (!PageLRU(page))       /* LRU에 없으면 skip */
            continue;
        if (PageWriteback(page))  /* 쓰기 중이면 skip */
            continue;

        /* pageblock의 migrate type 확인 */
        mt = get_pageblock_migratetype(page);
        if (mt == MIGRATE_UNMOVABLE)
            continue;             /* 이동 불가 */

        /* LRU에서 격리 */
        if (__isolate_lru_page_prepare(page, mode) == 0) {
            list_move(&page->lru, &cc->migratepages);
            cc->nr_migratepages++;
        }
    }
    return true;
}
설명 migrate scanner는 pageblock 단위로 순회하면서 PageLRU()인 이동 가능 페이지를 cc->migratepages 리스트에 모읍니다. MIGRATE_UNMOVABLE 타입의 pageblock은 건너뛰고, PB_migrate_skip 비트로 이미 처리된 블록도 건너뜁니다. PageWriteback(디스크 쓰기 중)인 페이지는 안전하게 이동할 수 없으므로 제외합니다.

kcompactd 데몬 동작

kcompactd는 NUMA 노드마다 하나씩 생성되는 커널 스레드(Kernel Thread)로, 백그라운드에서 compaction을 수행합니다. kswapd와 유사하게 워터마크 기반으로 깨어나며, 시스템 부하가 적을 때 단편화를 사전에 해소합니다.

깨어남 조건

조건트리거 위치설명
Watermark boostboost_watermark()페이지 해제 시 워터마크 부스트가 활성화되면 kcompactd를 깨움
Order-based 요청wakeup_kcompactd()특정 order 이상의 블록이 부족할 때 요청
Proactive 트리거kcompactd_do_work()단편화 점수가 임계치 초과 시 자체 루프
/* mm/compaction.c - kcompactd 메인 루프 (단순화) */
static int kcompactd(void *p)
{
    pg_data_t *pgdat = (pg_data_t *)p;

    while (!kthread_should_stop()) {
        /* 요청 대기 */
        wait_event_freezable(pgdat->kcompactd_wait,
            kcompactd_work_requested(pgdat));

        /* 깨어나면 compaction 수행 */
        kcompactd_do_work(pgdat);

        /* proactive compaction 조건 확인 */
        if (should_proactive_compact_node(pgdat))
            proactive_compact_node(pgdat);
    }
    return 0;
}

static void kcompactd_do_work(pg_data_t *pgdat)
{
    for (zoneid = 0; zoneid < MAX_NR_ZONES; zoneid++) {
        struct zone *zone = &pgdat->node_zones[zoneid];

        if (!populated_zone(zone))
            continue;

        /* compaction 필요 여부 확인 */
        if (compaction_suitable(zone, pgdat->kcompactd_max_order,
                               0, zoneid) != COMPACT_CONTINUE)
            continue;

        /* 실제 compaction 수행 */
        compact_zone(&cc);
    }
}
설명 kcompactdwait_event_freezable()로 대기하다가 wakeup_kcompactd()에 의해 깨어납니다. 깨어나면 노드의 모든 존을 순회하며 compaction_suitable()로 compaction이 필요한 존에서 compact_zone()을 호출합니다. should_proactive_compact_node()는 v5.9+에서 추가된 proactive compaction 조건을 확인합니다.
kcompactd vs kswapd: kswapd는 빈 페이지 수(워터마크)를 유지하고, kcompactd는 연속 빈 블록의 질(단편화 수준)을 유지합니다. kswapd가 페이지를 회수하면 kcompactd가 정리하는 상보적 관계입니다.
kcompactd 데몬 생명주기 및 깨움 흐름 SLEEP wait_event_freezable() 워터마크 부스트 boost_watermark() 명시적 깨움 wakeup_kcompactd() 사전 압축 트리거 proactive trigger 존(zone) 순회 compaction_suitable()? 깨어남 compact_zone() 이주 스캐너 + 빈 스캐너 적합 부적합 → 다음 존 사전 압축 확인 should_proactive_compact_node() 다시 수면 필요 시 재압축 kswapd와의 상보적 관계 kswapd: 빈 페이지 수(워터마크) 유지 → 페이지 회수 │ kcompactd: 연속 빈 블록의 질(단편화) 유지 → 압축 kswapd가 회수 완료 후 boost_watermark()로 kcompactd를 깨움
그림: kcompactd 데몬의 수면/깨움 주기와 세 가지 깨움 소스, 처리 흐름

Direct Compaction 경로

페이지 할당 요청이 고차 order이고, Buddy Allocator에서 즉시 충족되지 않으며, kswapd 깨우기(Wakeup)와 직접 회수(direct reclaim)로도 해결되지 않으면, 할당 요청 컨텍스트에서 직접(direct) compaction을 수행합니다.

/* mm/page_alloc.c - 할당 slow path에서 direct compaction (단순화) */
static struct page *__alloc_pages_direct_compact(gfp_t gfp_mask,
        unsigned int order, unsigned int alloc_flags,
        const struct alloc_context *ac,
        enum compact_priority prio,
        enum compact_result *compact_result)
{
    /* compaction 가능 여부 사전 확인 */
    if (!order)
        return NULL;  /* order-0은 compaction 불필요 */

    /* compaction 수행 */
    *compact_result = try_to_compact_pages(gfp_mask, order,
                                           alloc_flags, ac, prio, &page);

    if (page)
        return page;  /* compaction으로 확보 성공 */

    /* compact_result에 따라 재시도/포기 결정 */
    if (*compact_result == COMPACT_SKIPPED ||
        *compact_result == COMPACT_DEFERRED)
        return NULL;

    /* compaction 후 다시 할당 시도 */
    return get_page_from_freelist(gfp_mask, order, alloc_flags, ac);
}
페이지 할당 Slow Path & Direct Compaction 흐름 alloc_pages() Fast Path 성공? 페이지 반환 Yes No Slow Path (재시도 루프) wake_all_kswapds() 할당 재시도 페이지 반환 Direct Reclaim __alloc_pages_direct_compact() try_to_compact_pages → compact_zone Priority 에스컬레이션 ASYNC (최약) ↓ 실패 시 SYNC_LIGHT ↓ 실패 시 SYNC_FULL (최강) 할당 재시도 페이지 반환 재시도 루프 OOM Killer 호출
그림: 페이지 할당 slow path 흐름 — direct compaction은 direct reclaim 이후 시도되며, 실패 시 priority를 높여 재시도합니다.

Compaction Priority

Direct compaction은 단계적으로 강도를 높여가며 시도합니다:

Priority설명동작
COMPACT_PRIO_ASYNC비동기 (최약)일부 pageblock만 스캔, 잠금(Lock) 경쟁 시 즉시 포기
COMPACT_PRIO_SYNC_LIGHT가벼운 동기더 넓은 범위 스캔, 일부 대기 허용
COMPACT_PRIO_SYNC_FULL완전 동기 (최강)전체 존 스캔, 쓰기 중 페이지 대기, 최대 노력
Direct Compaction의 비용: COMPACT_PRIO_SYNC_FULL은 수백 ms의 지연을 유발할 수 있습니다. 실시간성이 중요한 워크로드에서는 /proc/sys/vm/compaction_proactiveness를 높여 사전 compaction을 강화하거나, THP를 비활성화하는 것을 고려하세요.

Compaction 결과 코드

결과의미후속 동작
COMPACT_SUCCESS충분한 연속 블록 확보할당 재시도
COMPACT_PARTIAL_SKIPPED일부 존만 처리다음 priority로 재시도
COMPACT_COMPLETE전체 스캔 완료, 불충분OOM 또는 포기
COMPACT_SKIPPEDcompaction 불필요/불가능다른 경로 시도
COMPACT_DEFERRED최근 실패로 연기deferred 카운터 감소 대기
COMPACT_CONTENDED잠금 경쟁으로 중단나중에 재시도
COMPACT_NOT_SUITABLE_ZONE존 조건 불충족다른 존에서 시도

Proactive Compaction (v5.9+)

커널 v5.9에서 도입된 Proactive Compaction은 할당 실패를 기다리지 않고, 단편화 점수가 특정 임계치를 넘으면 kcompactd가 사전에 compaction을 수행합니다.

Proactive Compaction 트리거 흐름 Direct Compaction 할당 실패 시 동기적 수행 kcompactd (기존) 워터마크 부스트 시 백그라운드 Proactive (v5.9+) 단편화 점수 기반 사전 예방 should_proactive_compact_node() fragmentation_score_node() > wmark_high fragmentation_score_zone(zone) extfrag_for_order() * zone 비율 proactive_compact_node(pgdat) 핵심 sysctl vm.compaction_proactiveness = 20 (기본값) 0: 비활성 | 100: 최대 적극성 wmark_low = 100 - proactiveness Proactive Watermark 계산 score > wmark_high -----> compaction 시작 score < wmark_low -----> compaction 중지 wmark_high = wmark_low + 10 proactiveness=20 => wmark_low=80, wmark_high=90 * fragmentation_score: 0(완벽) ~ 1000(극심한 단편화) | proactive compaction은 kcompactd 루프에서 주기적으로 확인
Proactive Compaction은 단편화 점수가 워터마크를 초과하면 사전에 compaction을 수행하여 direct compaction 발생을 최소화합니다
/* mm/compaction.c - proactive compaction 핵심 로직 (단순화) */
static bool should_proactive_compact_node(pg_data_t *pgdat)
{
    int wmark_high;

    if (!sysctl_compaction_proactiveness)
        return false;  /* 비활성화 */

    wmark_high = 100 - sysctl_compaction_proactiveness + 10;
    /* fragmentation_score가 wmark_high 이상이면 compaction 필요 */
    return fragmentation_score_node(pgdat) > wmark_high;
}

static unsigned int fragmentation_score_zone(struct zone *zone)
{
    unsigned int score = 0;
    /* 각 order에 대해 외부 단편화 점수 누적 */
    for (order = 1; order < MAX_ORDER; order++) {
        int index = __fragmentation_index(order, ...);
        score += (index * (1 << order)) / zone->managed_pages;
    }
    return score;
}
설명 sysctl_compaction_proactiveness가 20(기본값)이면 wmark_high = 90, wmark_low = 80으로 설정됩니다. 노드의 fragmentation score가 90을 초과하면 proactive compaction이 시작되고, 80 아래로 내려가면 중지됩니다. 값을 높일수록(예: 50) wmark가 낮아져 더 적극적으로 compaction하며, 0이면 완전 비활성화됩니다.

트리거 조건: 워터마크, Order, Fragmentation Index

Compaction이 시작되려면 여러 조건이 충족되어야 합니다. compaction_suitable() 함수가 이 판단을 담당합니다:

/* mm/compaction.c - compaction 적합성 판단 (단순화) */
enum compact_result compaction_suitable(struct zone *zone, int order,
                                        unsigned int alloc_flags,
                                        int highest_zoneidx)
{
    /* 1. order-0이면 compaction 불필요 */
    if (order == -1)
        return COMPACT_SKIPPED;

    /* 2. 빈 페이지가 워터마크 이하이면 회수가 먼저 필요 */
    watermark = low_wmark_pages(zone) + compact_gap(order);
    if (!__zone_watermark_ok(zone, 0, watermark, highest_zoneidx,
                             alloc_flags, zone->free_area))
        return COMPACT_SKIPPED;

    /* 3. fragmentation index로 단편화 문제인지 판단 */
    fragindex = fragmentation_index(zone, order);
    if (fragindex >= 0 && fragindex <= sysctl_extfrag_threshold)
        return COMPACT_NOT_SUITABLE_ZONE;
        /* 단편화가 아니라 실제 메모리 부족 */

    return COMPACT_CONTINUE;
}
설명 compaction은 다음 세 조건을 모두 만족해야 시작됩니다: (1) 요청 order가 1 이상, (2) 존의 빈 페이지 수가 최소 워터마크 이상 (회수가 먼저가 아니라는 의미), (3) fragmentation index가 extfrag_threshold(기본 500)보다 큼 (단편화가 실제 원인).

compact_gap() 함수

compact_gap()은 compaction이 의미 있으려면 존에 최소 몇 페이지의 빈 공간이 있어야 하는지를 계산합니다:

static inline unsigned long compact_gap(unsigned int order)
{
    /* 최소 2 * (1 << order) 페이지의 여유 필요 */
    return 2UL << order;
}

단편화 인덱스 계산

Fragmentation Index는 특정 order의 할당 실패가 실제 메모리 부족 때문인지, 외부 단편화 때문인지를 0~1000 범위로 정량화합니다.

Fragmentation Index 계산과 해석 fragmentation_index 공식 fragindex = 1000 - (1000+ (free_pages * 1000 / requested)) / (total_free * 1000 / requested) Fragmentation Index 스케일 (0 ~ 1000) 할당 문제 (메모리 부족) 경계 단편화 문제 (compaction 효과적) 0 500 1000 extfrag_threshold (기본 500) fragindex <= 500 빈 페이지 자체가 부족 -> reclaim 필요, compaction 비효과적 fragindex = -1 해당 order 블록이 이미 충분 -> compaction 불필요 fragindex > 500 빈 페이지 충분하나 흩어짐 -> compaction 효과적! 확인 경로 /sys/kernel/debug/extfrag/extfrag_index | /sys/kernel/debug/extfrag/unusable_index
fragindex가 높을수록 단편화가 원인이며 compaction이 효과적입니다. 낮으면 실제 메모리 부족이므로 reclaim이 필요합니다
/* mm/compaction.c - fragmentation_index 계산 (단순화) */
static int __fragmentation_index(unsigned int order, struct contig_page_info *info)
{
    unsigned long requested = 1UL << order;

    if (info->free_blocks_total == 0)
        return 0;  /* 빈 블록이 아예 없음 */

    if (info->free_blocks_suitable)
        return -1000;  /* 이미 충분한 블록 존재 */

    /*
     * fragindex = 1000 - (free_pages 대비 적합 블록 비율)
     * 0에 가까울수록: 빈 페이지 자체가 부족 (allocation problem)
     * 1000에 가까울수록: 빈 페이지는 충분하나 단편화 (fragmentation problem)
     */
    return 1000 - div_u64(
        (1000 + (div_u64(info->free_pages * 1000ULL, requested))),
        info->free_blocks_total);
}
설명 free_blocks_suitable: 요청 order 이상의 빈 블록 수. 이미 충분하면 -1000을 반환합니다. free_pages: 모든 order의 총 빈 페이지 수. free_blocks_total: 모든 order의 빈 블록 수. 빈 페이지가 많지만 작은 블록으로 쪼개져 있으면(free_blocks_total이 크면) fragindex가 1000에 가까워집니다.

debugfs로 단편화 인덱스 확인

# 각 존, 각 order별 단편화 인덱스 확인
cat /sys/kernel/debug/extfrag/extfrag_index
# Node 0, zone   Normal  -1.000 -1.000 -1.000 -1.000  0.861  0.912  0.953  0.976  0.988  0.994  0.997
#                          order-0            order-3    ^order-4: 높은 단편화

# unusable index: 특정 order를 할당할 수 없는 비율
cat /sys/kernel/debug/extfrag/unusable_index
# Node 0, zone   Normal  0.000  0.012  0.045  0.134  0.387  0.621  0.812  0.923  0.967  0.989  0.997

CMA와의 상호작용

CMA(Contiguous Memory Allocator)는 부팅 시 대규모 연속 메모리를 예약하고, 사용하지 않을 때는 MOVABLE 할당에 빌려줍니다. CMA 할당 요청이 오면 빌려간 페이지를 마이그레이션하여 연속 블록을 복구해야 하는데, 이때 compaction 인프라를 활용합니다.

속성MIGRATE_CMAMIGRATE_MOVABLE
평소 사용MOVABLE 할당에 빌려줌직접 할당
CMA 할당 시빌려간 페이지를 마이그레이션하여 반환영향 없음
UNMOVABLE 할당사용 불가 (빌려줄 수 없음)fallback으로 사용 가능
Compaction 대상CMA 영역 내 MOVABLE 페이지일반 MOVABLE 페이지
/* mm/cma.c - CMA 할당 시 compaction/migration (단순화) */
struct page *cma_alloc(struct cma *cma, unsigned long count,
                       unsigned int align, bool no_warn)
{
    for (;;) {
        /* CMA 예약 영역에서 연속 PFN 찾기 */
        pfn = cma_alloc_range(cma, count, align);
        if (!pfn)
            break;

        /* 해당 범위의 페이지를 격리 */
        ret = alloc_contig_range(pfn, pfn + count, MIGRATE_CMA,
                                GFP_KERNEL | __GFP_NORETRY);
        if (ret == 0)
            return pfn_to_page(pfn);  /* 성공 */

        /* 실패하면 다른 범위 시도 */
        cma_clear_bitmap(cma, pfn, count);
    }
    return NULL;
}

/* mm/page_alloc.c - alloc_contig_range는 내부적으로 __alloc_contig_migrate_range() 호출 */
/* __alloc_contig_migrate_range()는 migrate_pages()를 사용하여 범위 내 페이지를 이동 */
설명 CMA 할당은 alloc_contig_range()를 통해 지정 범위의 모든 사용 중 페이지를 마이그레이션합니다. 이 과정은 compaction과 동일한 migrate_pages() 인프라를 사용하지만, 특정 PFN 범위를 대상으로 한다는 점이 다릅니다. UNMOVABLE 페이지가 CMA 영역에 할당되면 마이그레이션이 불가능하므로, CMA 영역은 UNMOVABLE 할당 요청을 거부합니다.
CMA 영역의 UNMOVABLE 오염: 버그나 잘못된 드라이버가 CMA 영역에 UNMOVABLE 페이지를 할당하면, CMA 연속 할당이 영구적으로 실패할 수 있습니다. CONFIG_CMA_DEBUG를 활성화하면 이러한 오염을 감지할 수 있습니다.

THP 트리거 Compaction

Transparent Huge Pages(THP)는 order-9(2MB) 페이지를 투명하게 사용하므로, 할당 시 compaction이 빈번하게 트리거됩니다. THP의 defrag 모드에 따라 compaction 동작이 달라집니다:

defrag 모드설정 위치compaction 동작
always/sys/kernel/mm/transparent_hugepage/defrag모든 THP 할당에서 direct compaction 수행
defer동일kcompactd에게 위임, 직접 compaction 안 함
defer+madvise동일MADV_HUGEPAGE 영역만 direct, 나머지 defer
madvise동일MADV_HUGEPAGE 영역에서만 direct compaction
never동일compaction 트리거 안 함, fallback to order-0
# 현재 THP defrag 모드 확인
cat /sys/kernel/mm/transparent_hugepage/defrag
# always defer defer+madvise [madvise] never

# 서버 권장: defer+madvise (대부분 defer, 명시적 요청만 direct)
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag

# 데이터베이스/지연 민감 워크로드: never 또는 madvise
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag
THP와 compaction 성능: always 모드는 THP 할당마다 최대 수백 ms의 direct compaction 지연을 유발할 수 있습니다. Redis, MongoDB 등 지연 민감 워크로드에서는 madvise 또는 never가 권장됩니다. compact_stall vmstat 카운터가 급증한다면 THP defrag 모드를 낮추는 것을 고려하세요.

THP 할당 경로

/* mm/huge_memory.c - THP 할당 시 compaction 트리거 (단순화) */
static struct page *__alloc_hugepage(gfp_t gfp, int order, int node)
{
    /* GFP 플래그에 따라 direct compaction 허용 여부 결정 */
    gfp_t alloc_gfp = gfp | __GFP_COMP | __GFP_NOWARN;

    /* defrag 모드에 따라 __GFP_DIRECT_RECLAIM 설정 */
    if (defrag == ALWAYS || (defrag == MADVISE && vma_is_madvised))
        alloc_gfp |= __GFP_DIRECT_RECLAIM;
    else if (defrag == DEFER || defrag == DEFER_MADVISE)
        alloc_gfp &= ~__GFP_DIRECT_RECLAIM;
        /* kcompactd에게만 위임 */

    return alloc_pages_node(node, alloc_gfp, order);
    /* -> __alloc_pages() -> slow path -> __alloc_pages_direct_compact() */
}

ftrace 이벤트로 디버깅

커널은 compaction 동작을 추적할 수 있는 ftrace 이벤트를 제공합니다. 이를 통해 compaction이 언제, 왜, 얼마나 효과적으로 수행되는지 분석할 수 있습니다.

Compaction ftrace 이벤트 흐름 시간 mm_compaction _begin zone, migrate_pfn, free_pfn, order mm_compaction _isolate_migratepages nr_scanned, nr_taken mm_compaction _migratepages nr_migrated, nr_failed mm_compaction _end status, order, result 주요 ftrace 명령어 이벤트 활성화 cd /sys/kernel/debug/tracing echo 1 > events/compaction/enable # 또는 개별 이벤트: echo 1 > events/compaction/ mm_compaction_begin/enable echo 1 > events/compaction/ mm_compaction_end/enable cat trace_pipe # 실시간 모니터링 trace-cmd 사용 trace-cmd record \ -e compaction \ -e migrate \ sleep 30 trace-cmd report | grep compact # perf와 조합: perf trace -e 'compaction:*' \ -a sleep 10
ftrace의 compaction 이벤트로 begin/isolate/migrate/end 각 단계의 성능과 효과를 추적할 수 있습니다

주요 ftrace 이벤트 상세

이벤트필드의미
mm_compaction_beginzone_start, migrate_pfn, free_pfn, zone_end, synccompaction 시작, 스캐너 초기 위치
mm_compaction_endzone_start, migrate_pfn, free_pfn, zone_end, sync, statuscompaction 종료, 결과 상태
mm_compaction_isolate_migratepagesstart_pfn, end_pfn, nr_scanned, nr_takenmigrate scanner 격리 결과
mm_compaction_isolate_freepagesstart_pfn, end_pfn, nr_scanned, nr_takenfree scanner 격리 결과
mm_compaction_migratepagesnr_migrated, nr_failed마이그레이션 성공/실패 수
mm_compaction_try_to_compact_pagesorder, gfp_mask, modedirect compaction 시작
mm_compaction_finishedzone, order, result존별 compaction 완료 판정
mm_compaction_suitablezone, order, resultcompaction 적합성 판단
mm_compaction_deferredzone, ordercompaction 연기
mm_compaction_kcompactd_wakenid, order, highest_zoneidxkcompactd 깨어남
# ftrace 출력 예시
# kcompactd0-34  [002]  1234.567: mm_compaction_begin:
#   zone_start=0x100000 migrate_pfn=0x100000 free_pfn=0x1ff000 zone_end=0x200000 sync=0
# kcompactd0-34  [002]  1234.568: mm_compaction_isolate_migratepages:
#   range=(0x100000 ~ 0x100200) nr_scanned=512 nr_taken=45
# kcompactd0-34  [002]  1234.569: mm_compaction_migratepages:
#   nr_migrated=42 nr_failed=3
# kcompactd0-34  [002]  1234.570: mm_compaction_end:
#   zone_start=0x100000 migrate_pfn=0x100200 free_pfn=0x1fe800 zone_end=0x200000
#   sync=0 status=success
설명 이 예시에서 kcompactd가 비동기(sync=0) 모드로 동작하여, 512 페이지를 스캔해 45개를 격리하고 42개 마이그레이션에 성공했습니다(3개 실패). nr_failed가 높으면 이동 불가능한 페이지(UNMOVABLE이 아니지만 잠금 중이거나 writeback 중인 페이지)가 많다는 의미입니다.

vmstat compact_* 카운터 분석

/proc/vmstat에서 compaction 관련 카운터를 모니터링하면 시스템의 compaction 부하를 정량적으로 파악할 수 있습니다.

카운터의미높을 때 의미
compact_stallDirect compaction으로 인한 프로세스 정지 횟수할당 지연 증가, 단편화 심각
compact_failCompaction 시도 후 실패 횟수UNMOVABLE 오염 심각, 구조적 문제
compact_successCompaction 성공 횟수compaction이 효과적으로 동작 중
compact_migrate_scannedMigrate scanner가 스캔한 페이지 수스캔 범위 파악
compact_free_scannedFree scanner가 스캔한 페이지 수스캔 범위 파악
compact_isolated격리된 페이지 총 수실제 이동 대상 규모
compact_daemon_wakekcompactd 깨어난 횟수백그라운드 compaction 빈도
compact_daemon_migrate_scannedkcompactd migrate scanner 스캔 수데몬 작업량
compact_daemon_free_scannedkcompactd free scanner 스캔 수데몬 작업량
# compaction 카운터 모니터링
grep ^compact /proc/vmstat
# compact_migrate_scanned 12345678
# compact_free_scanned 23456789
# compact_isolated 345678
# compact_stall 123
# compact_fail 45
# compact_success 78
# compact_daemon_wake 234
# compact_daemon_migrate_scanned 5678901
# compact_daemon_free_scanned 8901234

# 주기적 모니터링 (10초 간격)
watch -n 10 'grep ^compact /proc/vmstat'

# compact_stall 증가 속도 확인 (성능 문제 진단)
awk '/compact_stall/ {print $2}' /proc/vmstat
sleep 60
awk '/compact_stall/ {print $2}' /proc/vmstat
# 차이가 크면 direct compaction이 빈번하게 발생 중

핵심 비율 분석

성공률 분석: compact_success / (compact_success + compact_fail)가 50% 이하이면 구조적 단편화 문제가 있습니다. compact_stall이 빠르게 증가하면 proactive compaction을 강화하거나 THP를 조정해야 합니다. compact_isolated / compact_migrate_scanned가 매우 낮으면(1% 이하) UNMOVABLE 페이지가 존을 지배하고 있다는 신호입니다.

Compaction Tunable 파라미터

커널은 여러 sysctl과 sysfs 파라미터를 통해 compaction 동작을 조정할 수 있습니다.

파라미터경로기본값설명
compaction_proactiveness/proc/sys/vm/200(비활성)~100(최대). proactive compaction 적극성
extfrag_threshold/proc/sys/vm/500fragindex 이 값 이하이면 compaction 건너뜀 (메모리 부족 판단)
compact_memory/proc/sys/vm/-1을 쓰면 수동으로 모든 존 compaction 트리거
defrag/sys/kernel/mm/transparent_hugepage/madviseTHP compaction 트리거 모드
compact_unevictable_allowed/proc/sys/vm/11이면 unevictable LRU의 페이지도 compaction 대상
min_free_kbytes/proc/sys/vm/가변간접 영향: 워터마크 높이를 결정하여 compaction 트리거에 영향

시나리오별 튜닝 가이드

# 1. THP를 많이 사용하는 HPC/VM 서버: proactive 강화
echo 50 > /proc/sys/vm/compaction_proactiveness
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag

# 2. 지연 민감 DB 서버: direct compaction 최소화
echo 70 > /proc/sys/vm/compaction_proactiveness  # 사전 예방 강화
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag
echo 300 > /proc/sys/vm/extfrag_threshold  # compaction 진입 기준 낮춤

# 3. 임베디드/IoT: 메모리 절약 우선
echo 0 > /proc/sys/vm/compaction_proactiveness  # proactive 비활성
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 4. 수동 compaction 트리거 (유지보수 시간)
echo 1 > /proc/sys/vm/compact_memory

# 5. 존별 수동 compaction (debugfs)
echo 1 > /sys/kernel/debug/compaction/compact_node_0
설명 compaction_proactiveness를 높이면 kcompactd가 더 자주 동작하여 direct compaction 발생을 줄입니다. 대신 CPU 사용량이 약간 증가합니다. extfrag_threshold를 낮추면 더 적은 단편화에서도 compaction을 시작하여 고차 할당 성공률이 높아집니다. compact_memory에 1을 쓰면 모든 노드, 모든 존에 대해 즉시 compaction을 수행합니다 (운영 시간에는 부하 유발 가능).

Compaction vs Page Migration 비교

Compaction과 Page Migration은 모두 migrate_pages()를 사용하지만, 목적과 대상이 다릅니다.

Compaction vs Page Migration 비교 Memory Compaction 목적: 외부 단편화 해소 대상: 동일 존 내 MOVABLE 페이지 방향: 존 내부 재배치 (한쪽으로 모음) 트리거: 고차 할당 실패, kcompactd 스캐너: Two-Scanner (migrate+free) 소스 코드: mm/compaction.c Page Migration 목적: NUMA 밸런싱, 핫플러그, CMA 대상: 노드 간/존 간 이동 가능 방향: 노드 간 이동 (로컬리티 최적화) 트리거: NUMA hint fault, mbind, migrate_pages(2) 스캐너: NUMA fault handler, 사용자 요청 소스 코드: mm/migrate.c 공유 인프라 migrate_pages() | unmap_and_move() | move_to_new_folio() migration entry | rmap | TLB flush MR_COMPACTION MR_NUMA_MISPLACED, MR_MEMORY_HOTPLUG, ... * migrate_reason: MR_COMPACTION, MR_MEMORY_HOTPLUG, MR_SYSCALL, MR_MEMPOLICY, MR_NUMA_MISPLACED, MR_CONTIG_RANGE
Compaction과 Migration은 동일한 migrate_pages() 인프라를 공유하지만, 목적과 대상 범위가 다릅니다
비교 항목CompactionPage Migration
migrate_reasonMR_COMPACTIONMR_NUMA_MISPLACED, MR_SYSCALL
대상 선택Two-Scanner 자동 선택NUMA fault, 사용자 mbind, cpuset
목표 위치free scanner가 찾은 빈 위치 (동일 존)다른 NUMA 노드의 빈 위치
syscallcompact_memory sysctlmigrate_pages(2), move_pages(2)
GFP 플래그GFP_KERNEL 기반호출자 지정
최대 재시도priority 3단계보통 1~10회

커널 CONFIG 옵션

CONFIG 옵션기본값설명
CONFIG_COMPACTIONyMemory Compaction 전체 활성화/비활성화
CONFIG_MIGRATIONyPage Migration 지원 (compaction의 전제 조건)
CONFIG_TRANSPARENT_HUGEPAGEyTHP 지원 (compaction의 주요 소비자)
CONFIG_CMAy (ARM 등)CMA 지원 (MIGRATE_CMA 타입 활성화)
CONFIG_CMA_DEBUGnCMA 디버깅 (UNMOVABLE 오염 감지)
CONFIG_MEMORY_HOTPLUG가변메모리 핫플러그 (compaction 인프라 활용)
CONFIG_NUMA가변NUMA 지원 (노드별 kcompactd)
CONFIG_PAGE_REPORTINGn빈 페이지 보고 (가상화(Virtualization) 환경에서 활용)

의존성 관계

# CONFIG_COMPACTION의 의존성
config COMPACTION
    bool "Allow for memory compaction"
    depends on MMU
    select MIGRATION
    help
      Allows the compaction of memory for the allocation of huge pages.

# CONFIG_MIGRATION은 CONFIG_COMPACTION에 의해 자동 선택됨
config MIGRATION
    bool
    depends on MMU
설명 CONFIG_COMPACTION을 활성화하면 CONFIG_MIGRATION이 자동 선택됩니다. CONFIG_COMPACTION=n으로 빌드하면 compact_zone(), kcompactd, direct compaction 경로가 모두 제거됩니다. 임베디드 시스템에서 THP가 불필요하면 compaction을 비활성화하여 커널 코드 크기와 런타임 오버헤드(Overhead)를 줄일 수 있습니다.
CONFIG_COMPACTION=n의 영향: compaction을 비활성화하면 고차 할당이 실패할 때 대안이 줄어듭니다. order-1 이상 할당이 필요한 기능(THP, 일부 네트워크 버퍼, jumbo frame 등)이 간헐적으로 실패할 수 있습니다. 비활성화 전 워크로드의 고차 할당 요구사항을 반드시 확인하세요.

운영 플레이북

실제 운영 환경에서 compaction 관련 문제를 진단하고 해결하는 단계별 절차입니다.

1단계: 문제 진단

#!/bin/bash
# compaction 건강 상태 종합 진단 스크립트

echo "=== 1. Buddy Allocator 상태 ==="
cat /proc/buddyinfo

echo ""
echo "=== 2. Pagetypeinfo (migrate type별 분포) ==="
cat /proc/pagetypeinfo | head -30

echo ""
echo "=== 3. Compaction 카운터 ==="
grep ^compact /proc/vmstat

echo ""
echo "=== 4. 단편화 인덱스 ==="
cat /sys/kernel/debug/extfrag/extfrag_index 2>/dev/null || echo "debugfs 미마운트"

echo ""
echo "=== 5. THP defrag 모드 ==="
cat /sys/kernel/mm/transparent_hugepage/defrag

echo ""
echo "=== 6. Proactive compaction 설정 ==="
cat /proc/sys/vm/compaction_proactiveness
cat /proc/sys/vm/extfrag_threshold

echo ""
echo "=== 7. 성공률 계산 ==="
SUCCESS=$(awk '/compact_success/ {print $2}' /proc/vmstat)
FAIL=$(awk '/compact_fail/ {print $2}' /proc/vmstat)
STALL=$(awk '/compact_stall/ {print $2}' /proc/vmstat)
TOTAL=$((SUCCESS + FAIL))
if [ $TOTAL -gt 0 ]; then
    RATE=$((SUCCESS * 100 / TOTAL))
    echo "compact_success=$SUCCESS compact_fail=$FAIL compact_stall=$STALL"
    echo "성공률: ${RATE}%"
else
    echo "compaction 이력 없음"
fi

시나리오 1: compact_stall 급증

증상: compact_stall이 초당 수십 건 증가, 애플리케이션 지연 증가
# 원인 분석
# 1. THP defrag 모드 확인 -- always이면 원인일 가능성 높음
cat /sys/kernel/mm/transparent_hugepage/defrag

# 2. 즉시 조치: THP defrag 낮추기
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag

# 3. proactive compaction 강화 (장기 대책)
echo 50 > /proc/sys/vm/compaction_proactiveness

# 4. 모니터링: stall 감소 확인
watch -n 5 'awk "/compact_stall/ {print \$2}" /proc/vmstat'

시나리오 2: compact_fail 비율 높음 (50% 이상 실패)

증상: compact_success 대비 compact_fail이 높음, 고차 할당 계속 실패
# 원인 분석: UNMOVABLE 오염 확인
cat /proc/pagetypeinfo | grep -E "Unmovable|Movable|Reclaimable"
# Unmovable 블록이 과도하게 많으면 문제

# 1. 어떤 slab이 UNMOVABLE을 많이 사용하는지 확인
cat /proc/slabinfo | sort -k3 -rn | head -20

# 2. extfrag_threshold 낮추기 (더 적극적으로 compaction 시도)
echo 300 > /proc/sys/vm/extfrag_threshold

# 3. 수동 compaction 시도
echo 1 > /proc/sys/vm/compact_memory

# 4. 장기 대책: min_free_kbytes 증가 (예비 메모리 확보)
echo 262144 > /proc/sys/vm/min_free_kbytes

시나리오 3: CMA 할당 실패

# CMA 상태 확인
cat /proc/meminfo | grep Cma
# CmaTotal:         262144 kB
# CmaFree:           12288 kB    <-- 매우 낮으면 문제

# CMA 디버깅 (CONFIG_CMA_DEBUG 필요)
dmesg | grep -i cma

# ftrace로 CMA 마이그레이션 추적
echo 1 > /sys/kernel/debug/tracing/events/migrate/enable
echo 1 > /sys/kernel/debug/tracing/events/compaction/enable
cat /sys/kernel/debug/tracing/trace_pipe &
# CMA 할당 트리거하여 로그 확인

지속 모니터링 설정

# Prometheus node_exporter 커스텀 텍스트 파일 생성
cat > /etc/node_exporter/compaction.prom << 'EOF'
#!/bin/bash
SUCCESS=$(awk '/compact_success/ {print $2}' /proc/vmstat)
FAIL=$(awk '/compact_fail/ {print $2}' /proc/vmstat)
STALL=$(awk '/compact_stall/ {print $2}' /proc/vmstat)
DAEMON=$(awk '/compact_daemon_wake/ {print $2}' /proc/vmstat)
echo "# HELP node_compaction_success_total Total compaction successes"
echo "# TYPE node_compaction_success_total counter"
echo "node_compaction_success_total $SUCCESS"
echo "# HELP node_compaction_fail_total Total compaction failures"
echo "# TYPE node_compaction_fail_total counter"
echo "node_compaction_fail_total $FAIL"
echo "# HELP node_compaction_stall_total Total direct compaction stalls"
echo "# TYPE node_compaction_stall_total counter"
echo "node_compaction_stall_total $STALL"
echo "# HELP node_compaction_daemon_wake_total kcompactd wakeups"
echo "# TYPE node_compaction_daemon_wake_total counter"
echo "node_compaction_daemon_wake_total $DAEMON"
EOF
chmod +x /etc/node_exporter/compaction.prom

고급: Compaction 내부 최적화

커널은 compaction 효율을 높이기 위해 여러 최적화를 적용합니다.

Pageblock Skip 비트

이미 스캔한 pageblock을 다시 방문하지 않도록 PB_migrate_skip 비트를 설정합니다:

/* mm/compaction.c - skip 비트 관리 (단순화) */
static void update_pageblock_skip(struct compact_control *cc,
                                   struct page *page,
                                   unsigned long pfn)
{
    if (cc->no_set_skip_hint)
        return;

    /* 이 pageblock에 이동 가능 페이지가 없으면 skip 표시 */
    set_pageblock_skip(page);

    /* 스캐너 캐시 위치 갱신 */
    if (pfn > cc->zone->compact_cached_migrate_pfn[cc->sync])
        cc->zone->compact_cached_migrate_pfn[cc->sync] = pfn;
}

Deferred Compaction

연속된 compaction 실패 시 지수적 백오프(exponential backoff)로 재시도 간격을 늘립니다:

/* mm/compaction.c - deferred compaction 로직 (단순화) */
static bool compaction_deferred(struct zone *zone, int order)
{
    unsigned int defer_shift = zone->compact_defer_shift;
    unsigned int max_retries = 1UL << defer_shift;

    if (zone->compact_considered < max_retries) {
        zone->compact_considered++;
        return true;  /* 아직 연기 중 */
    }
    return false;  /* 충분히 기다림, 재시도 허용 */
}

/* compaction 실패 시 defer_shift 증가 */
static void defer_compaction(struct zone *zone, int order)
{
    zone->compact_considered = 0;
    zone->compact_defer_shift++;

    if (zone->compact_defer_shift > COMPACT_MAX_DEFER_SHIFT)
        zone->compact_defer_shift = COMPACT_MAX_DEFER_SHIFT;
    /* COMPACT_MAX_DEFER_SHIFT = 6 => 최대 64회 건너뜀 */
}
설명 compact_defer_shift는 실패마다 1씩 증가하여, 재시도까지 건너뛰는 횟수가 2배로 늘어납니다(1, 2, 4, 8, ..., 최대 64). 성공하면 compact_defer_shift가 0으로 리셋됩니다. 이 메커니즘은 반복적으로 실패하는 존에서 CPU 낭비를 방지합니다.

비동기 vs 동기 모드

속성ASYNCSYNC_LIGHTSYNC_FULL
잠금 경쟁 시즉시 포기일부 대기대기 후 재시도
Writeback 페이지건너뜀건너뜀완료 대기
스캔 범위제한적중간전체 존
사용 컨텍스트kcompactddirect (1차)direct (마지막 시도)
need_resched() 확인확인 후 양보(Yield)확인 후 양보확인하지만 계속

Compaction 진화 역사

커널 버전주요 변경배경
v2.6.35Memory Compaction 최초 도입Mel Gorman, lumpy reclaim 대체
v3.3비동기 compaction 추가direct compaction 지연 감소
v3.5스캐너 위치 캐싱반복 스캔 비용 절감
v3.18kcompactd 도입백그라운드 compaction 데몬
v4.6skip 비트 개선불필요한 재스캔 방지
v4.8deferred compaction 개선지수적 백오프
v5.1COMPACT_PRIO_SYNC_LIGHT 추가3단계 priority 체계
v5.9Proactive Compaction 도입Vlastimil Babka, 사전 예방적 접근
v5.13Folio 기반 마이그레이션 시작compound page 지원 개선
v6.0+per-zone proactive scoring존별 단편화 점수 세분화

성능 특성과 오버헤드

Compaction 비용 분석

비용 요소크기영향
페이지 복사4KB memcpy per page대역폭(Bandwidth) 소비, 캐시 오염
PTE 갱신rmap walk per pagefork 많은 환경에서 비용 증가
TLB flushIPI per migration batchCPU 수 비례 오버헤드
LRU lockzone->lru_lock 경쟁다수 CPU 동시 접근 시 병목(Bottleneck)
존 스캔pageblock 순회존 크기에 비례
# compaction 비용 측정 (perf)
perf stat -e 'compaction:*' -a sleep 60
# mm_compaction_begin: 42
# mm_compaction_end: 42
# mm_compaction_migratepages: 42 (nr_migrated=12345, nr_failed=678)

# compaction 함수별 CPU 시간
perf top -g -p $(pgrep kcompactd)
# compact_zone         [kernel]   15.2%
# isolate_migratepages [kernel]    8.7%
# migrate_pages        [kernel]    6.3%
# unmap_and_move       [kernel]    4.1%
성능 벤치마크: 일반적인 서버에서 single compaction round(한 존)의 소요 시간:
  • ASYNC 모드: 1~50ms (스캔 범위에 따라)
  • SYNC_LIGHT: 10~200ms
  • SYNC_FULL: 50~500ms (worst case, 대용량 존)
마이그레이션되는 페이지당 약 5~20us의 비용이 발생합니다 (memcpy + rmap walk + TLB flush).

NUMA 환경에서의 Compaction

NUMA 시스템에서는 각 노드마다 독립적인 kcompactd 스레드(Thread)가 동작하며, 존별로 독립적인 compaction을 수행합니다.

# NUMA 노드별 kcompactd 확인
ps -eo pid,comm | grep kcompactd
# 34 kcompactd0    <-- Node 0
# 35 kcompactd1    <-- Node 1

# 노드별 buddyinfo 확인
cat /proc/buddyinfo
# Node 0, zone   Normal  312  156   78   39   12    3    1    0    0    0    0
# Node 1, zone   Normal  456  228  114   57   28   14    7    3    1    0    0
# Node 1의 단편화가 더 심함 -> kcompactd1이 더 활발히 동작
NUMA 불균형 compaction: 한 노드에서 집중적으로 고차 할당이 발생하면 해당 노드의 compact_stall만 증가합니다. /proc/vmstat은 전체 합산이므로, 노드별 분석이 필요하면 /sys/devices/system/node/node*/vmstat을 확인하세요.

Compaction 안티패턴

안티패턴 1: UNMOVABLE 페이지 확산

/* 잘못된 패턴: GFP_KERNEL로 대량 kmalloc */
/* GFP_KERNEL은 MIGRATE_UNMOVABLE로 할당됨 */
for (i = 0; i < 10000; i++)
    buffers[i] = kmalloc(PAGE_SIZE, GFP_KERNEL);
    /* -> 다수의 pageblock이 UNMOVABLE로 오염 */

/* 개선: slab 대신 vmalloc 또는 alloc_pages(GFP_HIGHUSER_MOVABLE) 활용 */
/* 또는 메모리 풀 사전 할당으로 UNMOVABLE 확산 제어 */

안티패턴 2: THP always + 지연 민감 워크로드

# 잘못된 설정: DB 서버에서 THP always
echo always > /sys/kernel/mm/transparent_hugepage/enabled
echo always > /sys/kernel/mm/transparent_hugepage/defrag
# -> 모든 mmap에서 direct compaction 발생 가능

# 올바른 설정: DB 서버
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag
# -> 명시적 MADV_HUGEPAGE 영역에서만 THP/compaction 시도

안티패턴 3: compact_memory 남용

# 잘못된 cron:
# */5 * * * * echo 1 > /proc/sys/vm/compact_memory
# -> 5분마다 전체 존 강제 compaction, 시스템 부하 급증

# 올바른 대안: proactive compaction 활용
echo 40 > /proc/sys/vm/compaction_proactiveness
# -> 커널이 필요할 때만 자동으로 적절한 수준의 compaction 수행

핵심 자료 구조

/* include/linux/compaction.h - struct compact_control */
struct compact_control {
    struct list_head freepages;      /* 격리된 빈 페이지 리스트 */
    struct list_head migratepages;   /* 격리된 이동 대상 페이지 리스트 */

    unsigned int nr_freepages;       /* freepages 리스트 크기 */
    unsigned int nr_migratepages;    /* migratepages 리스트 크기 */

    unsigned long free_pfn;          /* free scanner 현재 위치 */
    unsigned long migrate_pfn;       /* migrate scanner 현재 위치 */

    struct zone *zone;               /* 대상 존 */
    unsigned long total_migrate_scanned;  /* 총 migrate scanner 스캔 수 */
    unsigned long total_free_scanned;     /* 총 free scanner 스캔 수 */

    int order;                       /* 요청된 할당 order */
    gfp_t gfp_mask;                  /* 할당 GFP 플래그 */
    int highest_zoneidx;             /* 최고 존 인덱스 */

    enum migrate_mode mode;          /* MIGRATE_ASYNC/SYNC_LIGHT/SYNC */
    bool whole_zone;                 /* 전체 존 스캔 여부 */
    bool contended;                  /* 잠금 경쟁 발생 여부 */
    bool finish_pageblock;           /* 현재 pageblock 완료 여부 */
    bool alloc_contig;               /* alloc_contig_range 사용 여부 */

    int migratetype;                 /* 대상 migrate type */
    enum compact_result result;      /* compaction 결과 */
};
설명 struct compact_control은 하나의 compaction 세션 전체를 제어하는 핵심 구조체(Struct)입니다. freepages/migratepages 리스트는 격리 단계에서 수집된 페이지들을 담고, migrate_pages()에서 소비됩니다. free_pfn/migrate_pfn은 두 스캐너의 현재 위치를 추적합니다. mode는 비동기/동기 모드를 결정하며, contended는 잠금 경쟁 발생 시 조기 종료를 위해 사용됩니다.

Folio 기반 Compaction 전환

커널 v5.13부터 시작된 folio 전환은 compaction 경로에도 영향을 미칩니다. struct folio는 compound page를 직접 표현하여 THP compaction의 효율성을 높입니다.

/* mm/migrate.c - folio 기반 마이그레이션 (v6.0+, 단순화) */
static int migrate_folio_unmap(new_folio_t get_new_folio,
                               free_folio_t put_new_folio,
                               unsigned long private,
                               struct folio *src, struct folio *dst,
                               enum migrate_mode mode, enum migrate_reason reason,
                               struct list_head *ret)
{
    /* folio 잠금 */
    if (!folio_trylock(src))
        return -EAGAIN;

    /* rmap으로 모든 매핑 제거 */
    try_to_migrate(src, TTU_BATCH_FLUSH);

    /* migration entry 설치 완료 확인 */
    if (folio_mapped(src)) {
        /* 아직 매핑 남아있으면 복구 */
        remove_migration_ptes(src, src, false);
        folio_unlock(src);
        return -EAGAIN;
    }

    return MIGRATEPAGE_UNMAP;
}

static int migrate_folio_move(free_folio_t put_new_folio,
                              unsigned long private,
                              struct folio *src, struct folio *dst,
                              enum migrate_mode mode, enum migrate_reason reason,
                              struct list_head *ret)
{
    /* 실제 페이지 내용 복사 */
    folio_copy(dst, src);

    /* 메타데이터 이전 */
    migrate_folio_undo_src(src, ...);
    migrate_folio_done(src, ...);

    /* 새 folio로 PTE 교체 */
    remove_migration_ptes(src, dst, false);

    return MIGRATEPAGE_SUCCESS;
}
설명 folio 기반 마이그레이션은 두 단계(unmap + move)로 분리되어, batch processing이 가능합니다. 여러 folio의 unmap을 먼저 수행하고 TLB flush를 한 번에 처리한 후, 순차적으로 move를 수행하여 IPI 오버헤드를 줄입니다. THP(order-9 folio)의 경우 512 페이지를 한 번의 folio_copy()로 처리할 수 있어 compound page 분해/재조립 비용이 없습니다.

Batch TLB Flush 최적화

folio 기반 마이그레이션에서는 여러 페이지의 unmap을 모아서 TLB flush를 batch로 처리합니다:

/* unmap 단계에서 TTU_BATCH_FLUSH 사용 */
try_to_migrate(src, TTU_BATCH_FLUSH);
/* -> TLB flush를 즉시 수행하지 않고 지연 */

/* 모든 folio의 unmap 완료 후 한 번에 flush */
try_to_unmap_flush();
/* -> arch_tlbbatch_flush()로 IPI 최소화 */
성능 개선 효과: batch TLB flush는 특히 NUMA 시스템에서 효과적입니다. 100개 페이지를 마이그레이션할 때 개별 flush(100회 IPI)에서 batch flush(1~2회 IPI)로 줄어들어, 마이그레이션 처리량(Throughput)이 30~50% 향상될 수 있습니다.

compact_zone() 호출 체인 분석

페이지 할당 실패(또는 kcompactd 깨어남)가 발생하면 커널은 다단계 호출 체인을 통해 compact_zone()에 도달합니다. 이 체인을 추적하면 compaction이 언제, 왜, 어떤 경로로 진입하는지를 정확히 파악할 수 있습니다.

compact_zone() 호출 체인: 3가지 진입 경로 Direct Compaction __alloc_pages_direct_compact() kcompactd kcompactd_do_work() 수동 트리거 /proc/sys/vm/compact_memory try_to_compact_pages() zonelist 순회, 각 zone에 대해 호출 compact_zone_order() compact_control 초기화, 적합성 확인 compact_zone(cc) 핵심 compaction 루프 isolate_migratepages(cc) isolate_freepages(cc) migrate_pages(&cc->migratepages) compact_finished() == COMPACT_CONTINUE 동안 반복 핵심: compact_zone_order()에서 struct compact_control을 초기화하고 compact_zone()에 전달 cc->mode (ASYNC/SYNC_LIGHT/SYNC), cc->order, cc->zone, cc->migrate_pfn, cc->free_pfn 등 설정
3가지 진입 경로 모두 try_to_compact_pages() -> compact_zone_order() -> compact_zone()을 거칩니다
/* mm/compaction.c - try_to_compact_pages(): zonelist를 순회하며 compaction 수행 */
enum compact_result try_to_compact_pages(gfp_t gfp_mask,
        unsigned int order, unsigned int alloc_flags,
        const struct alloc_context *ac,
        enum compact_priority prio, struct page **page)
{
    int zoneid;
    struct zoneref *z;
    struct zone *zone;
    enum compact_result rc = COMPACT_SKIPPED;

    /* zonelist의 모든 zone을 순회 */
    for_each_zone_zonelist_nodemask(zone, z, ac->zonelist,
                                     ac->highest_zoneidx, ac->nodemask) {
        enum compact_result status;

        /* zone별 compaction 수행 */
        status = compact_zone_order(zone, order, gfp_mask,
                                    prio, alloc_flags,
                                    ac->highest_zoneidx, page);

        /* 성공하면 즉시 반환 */
        if (status == COMPACT_SUCCESS) {
            rc = COMPACT_SUCCESS;
            break;
        }
        rc = max(status, rc);  /* 가장 진행된 결과 보존 */
    }
    return rc;
}
코드 설명
  • 1~5행try_to_compact_pages()는 할당 컨텍스트(alloc_context)에서 전달받은 zonelist를 순회합니다. prio는 ASYNC/SYNC_LIGHT/SYNC_FULL 중 하나로, 시도 강도를 결정합니다.
  • 12~14행for_each_zone_zonelist_nodemask() 매크로로 NUMA 노드마스크(Nodemask)를 고려하여 적합한 zone을 순회합니다. 할당 요청의 highest_zoneidx보다 높은 zone은 건너뜁니다.
  • 18~20행compact_zone_order()를 호출하여 해당 zone에서 compaction을 수행합니다. 이 함수가 struct compact_control을 초기화하고 compact_zone()을 호출합니다.
  • 23~26행한 zone에서라도 COMPACT_SUCCESS가 반환되면 루프를 종료합니다. 실패 시 가장 진행된 결과(max)를 보존하여 호출자가 재시도 여부를 판단할 수 있게 합니다.
/* mm/compaction.c - compact_zone_order(): compact_control 초기화 */
static enum compact_result compact_zone_order(
        struct zone *zone, int order,
        gfp_t gfp_mask, enum compact_priority prio,
        unsigned int alloc_flags, int highest_zoneidx,
        struct page **capture)
{
    struct compact_control cc = {
        .order          = order,
        .gfp_mask       = gfp_mask,
        .zone           = zone,
        .mode           = (prio == COMPACT_PRIO_ASYNC) ?
                          MIGRATE_ASYNC : MIGRATE_SYNC_LIGHT,
        .alloc_flags    = alloc_flags,
        .highest_zoneidx = highest_zoneidx,
        .direct_compaction = true,
        .whole_zone     = (prio == COMPACT_PRIO_SYNC_FULL),
    };

    /* 적합성 사전 확인 */
    if (compaction_deferred(zone, order))
        return COMPACT_DEFERRED;

    /* 핵심 compaction 루프 실행 */
    return compact_zone(&cc);
}
코드 설명
  • 8~18행struct compact_control을 스택에 초기화합니다. order는 요청된 할당 크기, mode는 priority에 따라 ASYNC 또는 SYNC_LIGHT로 설정됩니다. whole_zone이 true이면 전체 zone을 처음부터 끝까지 스캔합니다.
  • 21~22행compaction_deferred()로 최근 연속 실패에 의한 지수적 백오프(Exponential Backoff)가 적용 중인지 확인합니다. 아직 대기 중이면 COMPACT_DEFERRED를 반환하여 불필요한 CPU 소모를 방지합니다.
  • 25행모든 사전 검사를 통과하면 compact_zone()을 호출하여 실제 Two-Scanner 알고리즘을 실행합니다. 반환된 compact_result가 그대로 호출자에게 전달됩니다.

compact_zone() 함수 구현 분석

compact_zone()은 Memory Compaction의 핵심 루프를 담당합니다. migrate scanner와 free scanner를 교대로 실행하면서, 이동 가능한 페이지를 빈 페이지 위치로 마이그레이션합니다. 이 함수의 상세 흐름을 분석합니다.

/* mm/compaction.c - compact_zone() 핵심 루프 (단순화된 ~35줄) */
static enum compact_result compact_zone(struct compact_control *cc)
{
    enum compact_result ret;
    unsigned long start_pfn = cc->zone->zone_start_pfn;
    unsigned long end_pfn = zone_end_pfn(cc->zone);
    int err;

    /* 스캐너 시작 위치 설정 (캐시 또는 zone 시작/끝) */
    cc->migrate_pfn = cc->zone->compact_cached_migrate_pfn[cc->sync];
    cc->free_pfn = cc->zone->compact_cached_free_pfn;

    /* whole_zone 모드이면 캐시 무시, zone 전체 스캔 */
    if (cc->whole_zone) {
        cc->migrate_pfn = start_pfn;
        cc->free_pfn = end_pfn;
    }

    migrate_prep();  /* LRU 드레인 등 사전 준비 */

    /* 메인 compaction 루프 */
    while ((ret = compact_finished(cc)) == COMPACT_CONTINUE) {
        /* 1단계: migrate scanner로 이동 가능 페이지 격리 */
        isolate_migratepages(cc);

        if (cc->nr_migratepages == 0) {
            /* 이동할 페이지가 없으면 다음 pageblock으로 */
            if (cc->direct_compaction && need_resched())
                break;  /* 비동기 모드: 스케줄러에 양보 */
            continue;
        }

        /* 2단계: migrate_pages()로 실제 마이그레이션 수행 */
        err = migrate_pages(&cc->migratepages,
                            compaction_alloc,
                            compaction_free,
                            (unsigned long)cc,
                            cc->mode, MR_COMPACTION, NULL);

        /* 마이그레이션 실패한 페이지를 LRU로 복귀 */
        if (err) {
            putback_movable_pages(&cc->migratepages);
            cc->nr_migratepages = 0;
        }

        /* 잠금 경쟁 발생 시 조기 종료 */
        if (cc->contended)
            break;
    }

    /* 스캐너 위치 캐시 갱신 */
    cc->zone->compact_cached_migrate_pfn[cc->sync] = cc->migrate_pfn;
    cc->zone->compact_cached_free_pfn = cc->free_pfn;

    return ret;
}
코드 설명
  • 10~11행이전 compaction에서 캐시된 스캐너 위치를 복원합니다. compact_cached_migrate_pfn은 ASYNC/SYNC별로 별도 캐시를 유지하여 서로 간섭하지 않습니다.
  • 14~17행whole_zone 모드(SYNC_FULL priority)에서는 캐시를 무시하고 zone의 처음부터 끝까지 전체를 스캔합니다. 최후의 수단으로 사용됩니다.
  • 19행migrate_prep()은 per-CPU LRU 캐시를 드레인(Drain)하여, 이후 isolate_migratepages()에서 누락되는 페이지를 최소화합니다.
  • 22행compact_finished()COMPACT_CONTINUE를 반환하는 한 루프를 계속합니다. 이 함수는 두 스캐너가 만났는지(migrate_pfn >= free_pfn), 요청 order 블록이 확보되었는지를 확인합니다.
  • 24행isolate_migratepages()는 내부적으로 isolate_migratepages_block()을 호출하여 pageblock 단위로 이동 가능 페이지를 cc->migratepages 리스트에 수집합니다.
  • 33~38행migrate_pages()compaction_alloc 콜백은 free scanner가 격리한 빈 페이지를 대상 페이지로 제공합니다. compaction_free는 마이그레이션 실패 시 빈 페이지를 반환하는 콜백입니다.
  • 41~44행마이그레이션에 실패한 페이지(잠금 경쟁, writeback 중 등)는 putback_movable_pages()로 원래 LRU 리스트에 되돌립니다.
  • 51~52행루프 종료 시 현재 스캐너 위치를 zone의 캐시에 저장합니다. 다음 compaction은 이 위치부터 재개하여 이미 처리한 영역을 다시 스캔하지 않습니다.

Dual Scanner 구현 상세: isolate_migratepages + isolate_freepages

Two-Scanner 알고리즘의 두 축인 isolate_migratepages()isolate_freepages()는 서로 독립적으로 동작하면서, struct compact_control의 리스트를 통해 데이터를 교환합니다.

isolate_migratepages() 구현

/* mm/compaction.c - migrate scanner: 이동 가능 페이지 수집 */
static isolate_migrate_t isolate_migratepages(
        struct compact_control *cc)
{
    unsigned long block_start_pfn;
    unsigned long block_end_pfn;
    unsigned long low_pfn = cc->migrate_pfn;

    /* pageblock 단위로 순회 */
    for (; low_pfn < cc->free_pfn;
         low_pfn = block_end_pfn) {

        block_start_pfn = pageblock_start_pfn(low_pfn);
        block_end_pfn = pageblock_end_pfn(low_pfn);

        /* skip 비트가 설정된 pageblock 건너뛰기 */
        if (!cc->finish_pageblock &&
            get_pageblock_skip(pfn_to_page(block_start_pfn)))
            continue;

        /* pageblock의 migrate type 확인 */
        if (!suitable_migration_source(cc, pfn_to_page(block_start_pfn)))
            continue;

        /* 이 pageblock에서 이동 가능 페이지 격리 */
        isolate_migratepages_block(cc, low_pfn, block_end_pfn,
                                    ISOLATE_UNEVICTABLE);

        /* 충분한 페이지를 수집했으면 중단 */
        if (cc->nr_migratepages >= COMPACT_CLUSTER_MAX)
            break;
    }

    cc->migrate_pfn = low_pfn;  /* 스캐너 위치 갱신 */
    return cc->nr_migratepages ? ISOLATE_SUCCESS : ISOLATE_NONE;
}
코드 설명
  • 10~11행migrate scanner는 cc->migrate_pfn부터 cc->free_pfn(free scanner 위치)까지 pageblock 단위로 순회합니다. 두 스캐너가 만나면 자연스럽게 루프가 종료됩니다.
  • 17~19행PB_migrate_skip 비트가 설정된 pageblock은 이미 이전 compaction에서 처리되었으므로 건너뜁니다. 단, finish_pageblock이 true(SYNC_FULL)이면 skip 비트를 무시합니다.
  • 22~23행suitable_migration_source()는 pageblock의 migrate type이 UNMOVABLE이 아닌지, CMA 영역인지 등을 확인합니다. UNMOVABLE pageblock의 페이지는 이동할 수 없으므로 건너뜁니다.
  • 30~31행COMPACT_CLUSTER_MAX(기본 32)개의 페이지를 수집하면 중단합니다. 한 번에 너무 많은 페이지를 격리하면 LRU 잠금 보유 시간이 길어져 다른 경로에 영향을 줍니다.
  • 34행현재 스캔 위치를 cc->migrate_pfn에 기록하여, 다음 루프 반복에서 이어서 스캔합니다.

isolate_freepages() 구현

/* mm/compaction.c - free scanner: 빈 페이지 수집 (단순화) */
static void isolate_freepages(struct compact_control *cc)
{
    unsigned long block_start_pfn, block_end_pfn;
    unsigned long isolate_start_pfn;

    /* zone 끝(높은 PFN)부터 아래로 스캔 */
    for (block_start_pfn = pageblock_start_pfn(cc->free_pfn);
         block_start_pfn >= cc->migrate_pfn;
         block_start_pfn -= pageblock_nr_pages) {

        block_end_pfn = block_start_pfn + pageblock_nr_pages;

        /* skip 비트 확인 */
        if (get_pageblock_skip(pfn_to_page(block_start_pfn)))
            continue;

        /* Buddy에서 빈 페이지 격리 */
        isolate_start_pfn = block_start_pfn;
        isolate_freepages_block(cc, &isolate_start_pfn,
                                block_end_pfn, true);

        /* 충분한 빈 페이지를 확보했으면 중단 */
        if (cc->nr_freepages >= cc->nr_migratepages)
            break;
    }

    cc->free_pfn = block_start_pfn;  /* free scanner 위치 갱신 */
}
코드 설명
  • 8~10행free scanner는 cc->free_pfn(zone의 높은 PFN 쪽)부터 cc->migrate_pfn(migrate scanner 위치)까지 역방향으로 pageblock 단위로 스캔합니다.
  • 20~21행isolate_freepages_block()은 Buddy Allocator의 free_area에서 빈 페이지를 분리하여 cc->freepages 리스트에 추가합니다. Buddy의 freelist에서 제거되므로 다른 할당자가 이 페이지를 사용할 수 없습니다.
  • 24~25행격리된 빈 페이지 수(nr_freepages)가 이동 대상 페이지 수(nr_migratepages) 이상이면 충분하므로 스캔을 중단합니다. 필요 이상의 빈 페이지를 격리하면 Buddy의 가용 메모리가 줄어들어 다른 할당에 영향을 줍니다.

struct compact_control 필드별 해설

struct compact_control은 하나의 compaction 세션 전체를 제어합니다. 각 필드의 역할과 생명주기를 정확히 이해하면 커널 소스를 추적할 때 큰 도움이 됩니다.

/* include/linux/compaction.h - struct compact_control 필드별 해설 */
struct compact_control {
    /* === 페이지 리스트 (격리 단계 산출물) === */
    struct list_head freepages;       /* free scanner가 격리한 빈 페이지 */
    struct list_head migratepages;    /* migrate scanner가 격리한 이동 대상 */

    /* === 카운터 (리스트 크기 추적) === */
    unsigned int nr_freepages;        /* freepages 리스트의 현재 크기 */
    unsigned int nr_migratepages;     /* migratepages 리스트의 현재 크기 */

    /* === 스캐너 위치 (PFN 단위) === */
    unsigned long migrate_pfn;        /* migrate scanner 현재 위치 */
    unsigned long free_pfn;           /* free scanner 현재 위치 */

    /* === 통계 (디버깅/ftrace용) === */
    unsigned long total_migrate_scanned; /* 총 스캔한 migrate 페이지 수 */
    unsigned long total_free_scanned;    /* 총 스캔한 free 페이지 수 */

    /* === 대상 zone과 할당 정보 === */
    struct zone *zone;                /* compaction 대상 zone */
    int order;                        /* 요청된 할당 order (-1=전체) */
    gfp_t gfp_mask;                   /* 할당 GFP 플래그 */
    int highest_zoneidx;              /* 최고 허용 zone 인덱스 */
    int migratetype;                  /* 대상 migrate type */

    /* === 동작 모드 제어 === */
    enum migrate_mode mode;           /* ASYNC / SYNC_LIGHT / SYNC */
    bool whole_zone;                  /* true: 캐시 무시, zone 전체 스캔 */
    bool contended;                   /* true: 잠금 경쟁으로 조기 종료 */
    bool finish_pageblock;            /* true: 현재 pageblock 강제 완료 */
    bool alloc_contig;                /* true: alloc_contig_range 경로 */
    bool direct_compaction;           /* true: 할당 경로에서 호출 */

    /* === 결과 === */
    enum compact_result result;       /* compaction 최종 결과 */
};
코드 설명
  • freepages / migratepages두 리스트는 compaction의 핵심 데이터 흐름입니다. isolate_migratepages()migratepages에 페이지를 추가하면, migrate_pages()compaction_alloc() 콜백이 freepages에서 대상 페이지를 꺼내 마이그레이션합니다.
  • migrate_pfn / free_pfn두 스캐너의 현재 위치(PFN)입니다. migrate_pfn은 증가 방향, free_pfn은 감소 방향으로 이동합니다. compact_finished()에서 migrate_pfn >= free_pfn이면 두 스캐너가 만난 것으로 판단합니다.
  • modeMIGRATE_ASYNC: 잠금 경쟁 시 즉시 포기, writeback 페이지 건너뜀. MIGRATE_SYNC_LIGHT: 일부 대기 허용. MIGRATE_SYNC: writeback 완료 대기까지 수행. kcompactd는 ASYNC, direct compaction은 priority에 따라 단계적으로 강도를 높입니다.
  • contendedzone->lock이나 lru_lock 경쟁이 감지되면 true로 설정되어, compact_zone()의 메인 루프가 조기 종료됩니다. 높은 부하 상황에서 compaction이 다른 경로를 blocking하는 것을 방지합니다.
  • alloc_contig / direct_compactionalloc_contig가 true이면 CMA alloc_contig_range() 경로에서 호출된 것으로, UNMOVABLE 페이지도 이동 대상에 포함할 수 있습니다. direct_compaction은 페이지 할당 slow path에서 직접 호출된 경우를 나타냅니다.

migrate_pages() 경로 분석

migrate_pages()는 격리된 페이지 리스트를 순회하면서 각 페이지를 새 위치로 복사하고, PTE(Page Table Entry)를 갱신하며, 원본을 해제합니다. v6.0 이후 folio 기반으로 전환되면서 unmap과 move가 분리되어 batch 처리가 가능해졌습니다.

migrate_pages() 호출 체인 (v6.0+ folio 기반) migrate_pages(&migratepages, ...) Phase 1: Unmap (batch) migrate_folio_unmap(src, dst) folio_trylock() -> try_to_migrate(TTU_BATCH_FLUSH) try_to_unmap_flush() -- batch TLB flush Phase 2: Move migrate_folio_move(src, dst) folio_copy() -> remove_migration_ptes() move_to_new_folio() 내용 복사 (memcpy) remove_migration_ptes() migration entry -> 새 PTE folio_putback_lru() 새 folio를 LRU에 추가 완료 * migration entry: 마이그레이션 중 PTE를 스왑 유사 엔트리로 대체하여, 접근 시 page fault -> 대기하게 합니다
folio 기반 마이그레이션은 unmap(batch TLB flush)과 move를 분리하여 IPI 오버헤드를 줄입니다
/* mm/migrate.c - migrate_pages() 메인 루프 (v6.0+ 단순화) */
int migrate_pages(struct list_head *from,
                  new_folio_t get_new_folio,
                  free_folio_t put_new_folio,
                  unsigned long private,
                  enum migrate_mode mode,
                  int reason, struct list_head *ret_folios)
{
    struct folio *src, *dst, *src2;
    int rc, nr_failed = 0, nr_succeeded = 0;
    int pass = 0;
    LIST_HEAD(unmap_folios);   /* unmap 완료된 folio 리스트 */
    LIST_HEAD(dst_folios);     /* 대상 folio 리스트 */

retry:
    list_for_each_entry_safe(src, src2, from, lru) {
        /* 대상 folio 할당 (compaction_alloc 콜백) */
        dst = get_new_folio(src, private);
        if (!dst) {
            nr_failed++;
            continue;
        }

        /* Phase 1: unmap (migration entry 설치) */
        rc = migrate_folio_unmap(get_new_folio, put_new_folio,
                                 private, src, dst, mode, reason,
                                 &unmap_folios);
        if (rc == MIGRATEPAGE_UNMAP) {
            list_move_tail(&dst->lru, &dst_folios);
            continue;  /* 다음 folio의 unmap으로 진행 */
        }
    }

    /* batch TLB flush: 모든 unmap을 한 번에 flush */
    try_to_unmap_flush();

    /* Phase 2: move (folio 내용 복사 + PTE 교체) */
    list_for_each_entry_safe(src, src2, &unmap_folios, lru) {
        dst = list_first_entry(&dst_folios, struct folio, lru);
        rc = migrate_folio_move(put_new_folio, private,
                                src, dst, mode, reason, ret_folios);
        if (rc == MIGRATEPAGE_SUCCESS)
            nr_succeeded++;
    }

    /* 일부 실패 시 재시도 (최대 10회) */
    if (!list_empty(from) && pass++ < 10)
        goto retry;

    return nr_failed;
}
코드 설명
  • 12~13행unmap_foliosdst_folios는 Phase 1(unmap)과 Phase 2(move) 사이의 버퍼 역할을 합니다. unmap이 완료된 folio를 모아두었다가 한꺼번에 move를 수행합니다.
  • 18~19행get_new_folio 콜백은 compaction 경로에서 compaction_alloc()으로 설정됩니다. 이 함수는 cc->freepages 리스트에서 free scanner가 격리한 빈 페이지를 꺼내 반환합니다.
  • 25~28행migrate_folio_unmap()은 rmap을 통해 원본 folio의 모든 PTE를 migration entry로 교체합니다. TTU_BATCH_FLUSH 플래그로 TLB flush를 지연시킵니다.
  • 35행try_to_unmap_flush()로 지연된 TLB flush를 한 번에 수행합니다. N개 folio의 unmap에 대해 IPI가 1~2회로 줄어듭니다.
  • 38~43행migrate_folio_move()move_to_new_folio()로 내용을 복사하고, remove_migration_ptes()로 migration entry를 새 folio의 PTE로 교체합니다. 이후 원본 folio는 Buddy에 반환됩니다.
  • 46~47행일시적 실패(잠금 경쟁, 참조 카운트 변동 등)를 위해 최대 10회 재시도합니다. 반복적으로 실패하는 페이지는 ret_folios에 남아 호출자가 처리합니다.

Proactive Compaction 커널 구현 상세

Proactive Compaction은 커널 v5.9에서 Nitin Gupta에 의해 도입되었습니다. kcompactd의 메인 루프에서 주기적으로 단편화 점수를 확인하고, 임계치를 초과하면 할당 실패 없이도 사전에 compaction을 수행합니다.

단편화 점수 계산과 워터마크

/* mm/compaction.c - proactive compaction 점수 계산 */
static unsigned int fragmentation_score_zone(struct zone *zone)
{
    unsigned long score = 0;
    unsigned long managed = zone_managed_pages(zone);

    if (!managed)
        return 0;

    /* 각 order에 대해 외부 단편화 점수 가중 합산 */
    for (int order = 1; order < MAX_ORDER; order++) {
        int index;
        struct contig_page_info info;

        fill_contig_page_info(zone, order, &info);
        index = __fragmentation_index(order, &info);

        /* 음수(충분)이면 0으로 치환 */
        if (index <= 0)
            continue;

        /* order가 클수록 가중치를 높여 합산 */
        score += index;
    }

    return div_u64(score * 100, managed);
}

static unsigned int fragmentation_score_node(struct pg_data_t *pgdat)
{
    unsigned int score = 0;

    for (int zoneid = 0; zoneid < MAX_NR_ZONES; zoneid++) {
        struct zone *zone = &pgdat->node_zones[zoneid];
        score += fragmentation_score_zone_weighted(zone);
    }
    return score;
}

/* kcompactd 루프에서 호출되는 판단 함수 */
static bool should_proactive_compact_node(struct pg_data_t *pgdat)
{
    int wmark_high;

    if (!sysctl_compaction_proactiveness)
        return false;  /* 0이면 완전 비활성 */

    /* wmark_low = 100 - proactiveness */
    /* wmark_high = wmark_low + 10 */
    wmark_high = (100 - sysctl_compaction_proactiveness) + 10;

    return fragmentation_score_node(pgdat) > wmark_high;
}
코드 설명
  • fragmentation_score_zone()zone 내 각 order(1~MAX_ORDER)에 대해 __fragmentation_index()를 호출하여 단편화 점수를 합산합니다. order가 높을수록(큰 블록일수록) 단편화의 영향이 크므로 가중치가 부여됩니다.
  • fragmentation_score_node()노드의 모든 zone에 대해 가중 점수를 합산합니다. 이 값이 wmark_high를 초과하면 proactive compaction이 트리거됩니다.
  • should_proactive_compact_node()sysctl_compaction_proactiveness 기본값 20에서: wmark_low = 80, wmark_high = 90. 노드 점수가 90을 초과하면 시작, 80 이하로 떨어지면 중지합니다. 값을 50으로 높이면 wmark_high = 60이 되어 더 적극적으로 compaction합니다.

kcompactd의 proactive compaction 루프

/* mm/compaction.c - kcompactd에서 proactive compaction 수행 */
static void proactive_compact_node(struct pg_data_t *pgdat)
{
    struct compact_control cc = {
        .order          = -1,        /* 특정 order 없음, 전반적 최적화 */
        .mode           = MIGRATE_SYNC_LIGHT,
        .whole_zone     = true,     /* 전체 zone 스캔 */
        .gfp_mask       = GFP_KERNEL,
    };
    int zoneid;

    for (zoneid = 0; zoneid < MAX_NR_ZONES; zoneid++) {
        struct zone *zone = &pgdat->node_zones[zoneid];

        if (!populated_zone(zone))
            continue;

        cc.zone = zone;
        compact_zone(&cc);

        /* 점수가 wmark_low 이하로 내려가면 중단 */
        if (fragmentation_score_zone(zone) <=
            (100 - sysctl_compaction_proactiveness))
            continue;  /* 이 zone은 충분히 정리됨 */
    }
}
코드 설명
  • 5행order = -1은 특정 order의 블록 확보가 목적이 아니라, zone 전반의 단편화를 낮추는 것이 목표임을 나타냅니다. compact_finished()에서 order 기반 종료 조건을 건너뜁니다.
  • 6~7행MIGRATE_SYNC_LIGHT 모드와 whole_zone = true를 사용합니다. 백그라운드 작업이므로 SYNC_FULL까지는 사용하지 않지만, ASYNC보다는 적극적으로 페이지를 이동합니다.
  • 21~23행각 zone의 compaction 후 단편화 점수를 재확인합니다. wmark_low 이하로 떨어진 zone은 추가 작업이 필요 없으므로 건너뜁니다. 이 히스테리시스(Hysteresis) 설계로 과도한 compaction을 방지합니다.
운영 가이드: /proc/sys/vm/compaction_proactiveness 값은 워크로드에 따라 조정합니다.
  • THP 집약 서버 (HPC, VM): 40~60으로 설정하여 order-9 블록을 사전 확보
  • 지연 민감 서버 (DB, RT): 20~30으로 유지하되, echo madvise > /sys/kernel/mm/transparent_hugepage/defrag로 THP 트리거를 제한
  • compaction 오버헤드 최소화: 0으로 설정하여 proactive compaction 완전 비활성화

참고자료

커널 문서

LWN 기사

커널 소스

서적