NUMA·메모리 압축·KSM으로 다루는 Linux 대용량 메모리

NUMA 할당 정책, Memory Compaction, KSM의 동작 원리와 Linux 대용량 메모리 환경에서의 조합 방식을 정리한다.

2026-08-14 · 최초 발행 2026-01-25

메모리 위치와 연속성이 대용량 시스템의 병목이 된다

대용량 메모리 시스템에서는 할당과 해제만으로 성능을 설명하기 어렵다. NUMA 환경에서는 페이지가 어느 노드에 놓였는지가 접근 비용을 바꾸고, 장기 실행 시스템에서는 단편화가 큰 연속 영역의 확보를 막는다. 동일한 페이지가 여러 곳에 존재하는 가상화 환경에서는 중복 자체도 관리 대상이 된다.

Linux에서 이 문제를 다룰 때 중심이 되는 기능은 NUMA-Aware Allocation, Memory Compaction, KSM(Kernel Same-page Merging)이다. 각각 지역성, 연속 여유 공간, 중복 페이지를 대상으로 하며 같은 워크로드에도 선택과 설정이 달라진다.

NUMA에서는 로컬 메모리와 원격 메모리의 비용이 다르다

NUMA(Non-Uniform Memory Access) 시스템에서 프로세서는 직접 연결된 로컬 메모리 컨트롤러를 통해 메모리에 접근한다. 다른 노드의 메모리에는 인터커넥트를 거쳐 접근하므로 비용이 더 커진다.

원격 메모리 접근은 로컬 접근보다 1.5배~3배 이상 지연될 수 있다. 또한 원격 접근은 인터커넥트 대역폭을 공유하므로 대역폭 경쟁이 병목으로 이어질 수 있다.

NUMA Node 1NUMA Node 0빠른 접근빠른 접근빠른 접근빠른 접근느린 접근느린 접근CPU 0CPU 1Local Memory 0CPU 2CPU 3Local Memory 1Interconnect (QPI/UPI)

NUMA 구성은 /sys/devices/system/node/에서 확인할 수 있다.

# NUMA 노드 개수 확인
numactl --hardware

# 각 노드의 메모리 용량과 거리 행렬
cat /sys/devices/system/node/node0/distance

# 프로세스별 NUMA 메모리 사용 현황
numastat -p <pid>

할당 정책은 지역성과 대역폭 사이의 선택이다

Linux는 프로세스와 메모리 영역에 NUMA 할당 정책을 적용할 수 있다.

MPOL_DEFAULT는 시스템의 기본 정책을 따르며, 일반적으로 현재 실행 중인 CPU의 노드에서 할당한다. MPOL_BIND는 지정한 노드에만 메모리를 할당하므로 해당 노드의 메모리가 부족하면 할당이 실패할 수 있다.

MPOL_INTERLEAVE는 여러 노드에 페이지를 라운드로빈 방식으로 나눈다. 대역폭을 최대화하는 데 유리하며 대용량 메모리 접근에 효과적이다. MPOL_PREFERRED는 선호 노드를 먼저 사용하되 불가능하면 다른 노드를 사용하므로 소프트 바인딩으로 동작한다.

// NUMA 메모리 정책 설정 예제
#include <numaif.h>

void set_numa_policy_example() {
    unsigned long nodemask = 0x01;  // Node 0

    // 현재 프로세스에 NUMA 정책 설정
    set_mempolicy(MPOL_BIND, &nodemask, sizeof(nodemask) * 8);

    // 특정 메모리 영역에 정책 적용
    void *addr = mmap(NULL, size, PROT_READ|PROT_WRITE,
                      MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

    mbind(addr, size, MPOL_INTERLEAVE, &nodemask,
          sizeof(nodemask) * 8, MPOL_MF_MOVE);
}

커널의 자동 NUMA 밸런싱은 원격 접근이 반복되는 페이지를 로컬 노드로 옮기거나, 메모리가 있는 노드의 CPU로 태스크를 이동시킨다. 페이지 테이블을 주기적으로 무효화해 접근 패턴을 추적하는 샘플링 방식도 사용한다.

# 자동 NUMA 밸런싱 활성화 확인
cat /proc/sys/kernel/numa_balancing

# NUMA 밸런싱 통계
cat /proc/vmstat | grep numa

단편화가 큰 페이지 할당을 막는 방식

시스템이 오래 실행되면 전체 여유 메모리가 충분해도 연속된 물리 메모리 블록을 찾기 어려워질 수 있다. 이것이 외부 단편화다. 버디 시스템의 높은 order 할당 실패율이 증가하고, 2MB/1GB Huge Page 할당도 영향을 받는다.

압축사용사용사용 여유 공간단편화사용작은 여유사용작은 여유사용단편화사용여유사용여유

Memory Compaction은 이동 가능한 페이지를 한쪽으로 모아 연속된 여유 공간을 만든다. Free scanner는 메모리 영역 끝에서 여유 페이지를 찾고, Migration scanner는 시작점부터 이동 가능한 페이지를 찾는다.

페이지는 페이지 테이블 업데이트만으로 옮길 수 있는 Movable, 필요할 때 회수할 수 있는 Reclaimable, 커널 데이터처럼 이동할 수 없는 Unmovable로 나뉜다. 이동 단계에서는 페이지 내용을 새 위치로 복사하고 페이지 테이블을 갱신한 뒤, 이전 페이지를 여유 풀에 반환한다.

// 커널 코드 예시 (단순화)
static int compact_zone(struct zone *zone, struct compact_control *cc) {
    unsigned long migrate_pfn, free_pfn;

    migrate_pfn = zone->zone_start_pfn;
    free_pfn = zone_end_pfn(zone) - 1;

    while (migrate_pfn < free_pfn) {
        // 이동 가능 페이지 찾기
        migrate_pfn = isolate_migratepages(zone, cc, migrate_pfn);

        // 여유 페이지 찾기
        free_pfn = isolate_freepages(zone, cc, free_pfn);

        // 페이지 마이그레이션 수행
        migrate_pages(&cc->migratepages, compaction_alloc,
                     NULL, (unsigned long)cc, MIGRATE_SYNC);
    }
}

Compaction은 자동으로 실행될 수 있고, 필요하면 수동으로 트리거하거나 설정을 조정할 수 있다.

# 수동 compaction 트리거
echo 1 > /proc/sys/vm/compact_memory

# Proactive compaction 설정 (5.8+ 커널)
echo 20 > /sys/kernel/mm/compaction/proactiveness

# Compaction 통계 확인
cat /proc/vmstat | grep compact

KSM은 같은 내용을 가진 페이지를 공유한다

가상화 환경에서 같은 OS 이미지를 실행하는 여러 VM, 동일한 베이스 이미지를 공유하는 컨테이너, fork() 이후 읽기 전용 데이터 영역에는 같은 내용을 가진 메모리 페이지가 중복될 수 있다.

KSM은 이 페이지를 찾아 하나의 물리 페이지로 통합하고 COW(Copy-On-Write)로 관리한다. 병합된 페이지는 읽기 전용으로 공유되며, 쓰기 시 COW가 발생한다.

페이지 스캔중복 확인다른 페이지와 병합내용 변경 감지페이지 해제UnstableStableMerged읽기 전용으로 공유쓰기 COW 발생

ksmd 커널 스레드는 주기적으로 실행되며 madvise(MADV_MERGEABLE)로 표시된 영역만 스캔한다. 페이지 내용의 체크섬을 계산한 뒤 Unstable tree와 Stable tree에서 중복을 찾는다. 중복 페이지의 PTE는 읽기 전용으로 바뀌고, 모든 참조가 하나의 물리 페이지를 가리키도록 수정된다.

// KSM 활성화 예제 코드
#include <sys/mman.h>

void enable_ksm_for_region(void *addr, size_t length) {
    // 메모리 영역을 KSM 대상으로 표시
    if (madvise(addr, length, MADV_MERGEABLE) == -1) {
        perror("madvise MADV_MERGEABLE failed");
    }
}

void disable_ksm_for_region(void *addr, size_t length) {
    // KSM 비활성화
    madvise(addr, length, MADV_UNMERGEABLE);
}

KSM 동작은 sysfs에서 제어할 수 있다.

# KSM 활성화
echo 1 > /sys/kernel/mm/ksm/run

# 스캔 주기 설정 (밀리초)
echo 100 > /sys/kernel/mm/ksm/sleep_millisecs

# 한 번에 스캔할 페이지 수
echo 500 > /sys/kernel/mm/ksm/pages_to_scan

# KSM 통계 확인
cat /sys/kernel/mm/ksm/pages_shared    # 병합된 페이지 수
cat /sys/kernel/mm/ksm/pages_sharing   # 공유 중인 페이지 수
cat /sys/kernel/mm/ksm/pages_unshared  # 고유 페이지 수

워크로드별로 조합하는 메모리 관리 기능

대규모 데이터베이스 서버에서는 MPOL_INTERLEAVE로 대역폭을 확보하고 Proactive Compaction으로 Huge Page를 준비하며, 성능을 우선해 KSM을 비활성화할 수 있다.

numactl --interleave=all mysqld --huge-pages=ON
echo 30 > /sys/kernel/mm/compaction/proactiveness
echo 0 > /sys/kernel/mm/ksm/run

KVM 가상화 호스트에서는 VM별 노드 바인딩, 기본 Compaction 설정, 적극적인 KSM 활성화가 조합될 수 있다.

# VM을 특정 NUMA 노드에 배치
numactl --cpunodebind=0 --membind=0 qemu-system-x86_64 ...

# KSM 최적화 설정
echo 1 > /sys/kernel/mm/ksm/run
echo 20 > /sys/kernel/mm/ksm/sleep_millisecs
echo 1000 > /sys/kernel/mm/ksm/pages_to_scan

컨테이너 오케스트레이션 환경은 컨테이너별 동적 NUMA 할당, 적극적인 Compaction을 통한 단편화 방지, 중간 수준의 KSM 활성화를 함께 고려할 수 있다.

운영 중에는 NUMA 상태, Compaction 이벤트, KSM 효율과 NUMA 밸런싱 효과를 함께 확인한다.

# 종합 메모리 상태 확인
numastat -m

# Compaction 이벤트 추적
cat /proc/vmstat | grep -E "compact|thp"

# KSM 효율 계산
pages_saved=$(cat /sys/kernel/mm/ksm/pages_sharing)
pages_shared=$(cat /sys/kernel/mm/ksm/pages_shared)
ratio=$((pages_saved / pages_shared))
echo "Deduplication ratio: $ratio:1"

# NUMA 밸런싱 효과 측정
perf stat -e 'sched:sched_migrate_task,migrate:mm_migrate_pages' -p <pid>

THP와 메모리 풀, KSM 보안 설정

THP는 Compaction에 크게 의존하므로 두 기능의 설정을 함께 관찰해야 한다.

# THP 모드 설정
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag

# Compaction과 THP 통계 동시 관찰
watch -n 1 'grep -E "thp|compact" /proc/vmstat'

애플리케이션 레벨에서는 현재 CPU가 속한 NUMA 노드의 메모리 풀을 우선 사용하고, 필요할 때 다른 노드로 대체하는 설계를 적용할 수 있다.

#include <numa.h>

typedef struct {
    void *pools[MAX_NUMA_NODES];
    size_t pool_size;
} numa_aware_pool_t;

void* numa_pool_alloc(numa_aware_pool_t *pool, size_t size) {
    int node = numa_node_of_cpu(sched_getcpu());

    // 현재 NUMA 노드의 풀에서 할당
    void *ptr = pool_allocate(pool->pools[node], size);

    if (!ptr && pool->allow_fallback) {
        // 실패 시 다른 노드에서 할당
        for (int i = 0; i < numa_num_configured_nodes(); i++) {
            if (i != node) {
                ptr = pool_allocate(pool->pools[i], size);
                if (ptr) break;
            }
        }
    }

    return ptr;
}

KSM은 타이밍 공격에 취약할 수 있으므로 보안이 중요한 환경에서는 사용에 주의가 필요하다.

# KSM 사용 시 보안 옵션
echo 1 > /sys/kernel/mm/ksm/use_zero_pages  # 제로 페이지 병합 허용
echo 1 > /sys/kernel/mm/ksm/stable_node_chains_prune_millisecs

NUMA-Aware Allocation은 메모리 접근 지역성을, Memory Compaction은 단편화를, KSM은 중복 메모리 페이지를 다룬다. 이 기능들은 워크로드의 특성에 따라 함께 사용할 수 있으며, 실제 설정은 모니터링 데이터를 바탕으로 지속적으로 조정한다.

NUMA메모리 관리Linux 커널KSMHuge Pages