커널 메모리 컴팩션 (Compaction) 외부 단편화 런타임 제거 백그라운드 스레드 구조
핵심 인사이트 (3줄 요약)
- 본질: 리눅스 커널의 메모리 컴팩션(Memory Compaction)은 장기간 실행된 시스템에서 4KB 페이지들이 뿔뿔이 흩어져 **외부 단편화(External Fragmentation)**가 발생했을 때, 흩어진 페이지들을 한쪽으로 몰아서(조각 모음) 큰 덩어리의 연속된 빈 메모리를 만들어내는 기술이다.
- 메커니즘:
kcompactd라는 백그라운드 스레드가 메모리 존(Zone)의 양 끝에서 출발하여, 한쪽은 사용 중인(Movable) 페이지를 찾고 다른 한쪽은 빈(Free) 페이지를 찾아 중간에서 만나며 사용 중인 페이지를 빈 공간으로 **이사(Migration)**시킨다.- 가치: THP(Transparent Huge Pages)와 같이 연속된 2MB 단위의 거대 페이지 할당이 필수적인 현대 고성능 워크로드(DB, KVM)가 재부팅 없이도 성능 저하 늪에 빠지지 않고 안정적으로 메모리를 확보할 수 있게 해 주는 핵심 기반 기술이다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: 메모리 컴팩션(Compaction)은 디스크의 조각 모음(Defragmentation)과 똑같은 원리를 램(RAM)에 적용한 것이다. 사용 중인 물리 메모리 페이지들을 이동시켜 빈 공간을 연속적으로 확보하는 커널의 런타임 서브시스템이다.
-
필요성 (Buddy System의 한계와 단편화의 저주):
- 리눅스는 버디 할당기(Buddy Allocator)를 통해 메모리를 $2^n$ 단위로 쪼개고 합친다. 서버가 한 달 이상 돌면 수백만 개의 4KB 페이지가 할당되고 해제되기를 반복하면서, 빈 공간은 많은데 '연속된' 빈 공간이 없는 외부 단편화가 극심해진다.
- 이때 애플리케이션(예: KVM, DPDK, DB)이 성능 향상을 위해 연속된 거대한 2MB 메모리(Huge Page)를 요구하면, 커널은 줄 게 없어서 OOM을 내거나 2MB 할당을 포기하고 느린 4KB로 응답해 버린다(THP Fallback). 성능이 30% 이상 폭락한다.
- 해결책: 스와핑(Swapping)으로 남의 메모리를 디스크로 쫓아내는 가혹한 방식(Reclaim) 대신, 메모리 안에서 사용 중인 데이터를 옆으로 쓱 밀어서 큰 빈자리를 만들어 내는 평화적인 압축(Compaction) 알고리즘이 필요했다.
-
💡 비유:
- 버스(메모리)에 40개의 빈자리가 있지만, 승객(4KB 페이지)들이 전부 퐁당퐁당 한 자리씩 건너 띄고 앉아 있다(단편화).
- 이때 4인 가족(거대 페이지, THP)이 타서 "같이 붙어 앉을 자리 주세요!"라고 한다.
- Reclaim(과거): "자리 없네요, 손님 4명 강제로 버스 밖으로 쫓아내고(Swap Out) 그 자리 드릴게요."
- Compaction(현대): 안내원(
kcompactd)이 "손님들, 죄송하지만 전부 앞쪽으로 땡겨서 앉아주세요~"라고 부탁한다(Migration). 승객들이 앞으로 몰려 앉자, 버스 뒤쪽에 4인 가족이 앉을 수 있는 연속된 큰 빈자리가 마술처럼 생겨난다.
-
발전 과정:
- Lumpy Reclaim (초기): 거대 페이지가 필요하면 무식하게 주변 페이지들을 디스크(Swap)로 쫓아내어 빈자리를 만듦. (I/O 폭증으로 폐기됨)
- Memory Compaction (Linux 2.6.35+): 멜 고먼(Mel Gorman)이 제안. 스왑 없이 메모리 내에서만 데이터를 이동시켜 빈자리를 창출하는 혁신적 알고리즘 도입.
- kcompactd 데몬화 (Linux 4.6+): 앱이 메모리를 요청할 때 직접 압축하느라 멈추는(Direct Compaction 지연) 현상을 막기 위해, 백그라운드 스레드가 평소에 미리미리 압축해 두는 구조로 발전.
-
📢 섹션 요약 비유: 방이 지저분할 때 물건을 창고(디스크)에 던져버리는 것(스와핑)이 아니라, 테트리스 블록을 한쪽으로 차곡차곡 예쁘게 밀어 넣어(컴팩션) 큰 덩어리의 가구를 들일 수 있는 빈 공간을 만드는 수납의 달인 기술입니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
리눅스 페이지의 이동성 (Mobility) 분류
컴팩션이 가능하려면 페이지를 이동시켜야 하는데, 모든 페이지를 다 이동시킬 수는 없다. 리눅스는 할당 시점부터 페이지를 3가지로 분리해서 관리한다 (Anti-fragmentation 원칙).
| 이동성 (Mobility) | 대상 데이터 | 이동 가능 여부 | 비유 |
|---|---|---|---|
| Movable (이동 가능) | User Space 앱의 힙/스택 데이터, 페이지 캐시 | 자유롭게 이동 가능 (컴팩션의 주 타겟) | 짐가방 (언제든 옮길 수 있음) |
| Unmovable (고정) | 커널 자료구조 (task_struct 등) | 절대 이동 불가 (커널 패닉 발생) | 기둥 (움직이면 집이 무너짐) |
| Reclaimable (회수 가능) | inode 캐시, dentry 캐시 | 이동은 못 하지만 지울(버릴) 수는 있음 | 쓰레기통 (치우면 공간 확보) |
컴팩션의 성공률은 이 Unmovable 페이지들이 메모리 공간 중간에 얼마나 알박기를 하고 있느냐에 달려 있다. 커널은 Unmovable 페이지들을 한쪽 구역에 몰아서 할당하려 노력한다.
투 포인터(Two-Pointer) 기반 컴팩션 알고리즘
kcompactd 스레드의 알고리즘은 매우 우아한 '양방향 스캐닝' 기법을 사용한다.
┌───────────────────────────────────────────────────────────────────┐
│ 메모리 컴팩션 (Memory Compaction) 투 포인터 원리 │
├───────────────────────────────────────────────────────────────────┤
│ │
│ [상황 1: 파편화된 메모리 존 (Zone)] │
│ 메모리 시작(Bottom) 메모리 끝(Top) │
│ [ M ] [ F ] [ M ] [ U ] [ F ] [ F ] [ M ] [ F ] [ M ] [ F ] │
│ (M: Movable 사용 중, F: Free 빈 공간, U: Unmovable 고정됨) │
│ ▲ ▲ │
│ Free Scanner ──▶ (빈 공간 탐색) (Movable 탐색) ◀── Migration Scanner │
│ │
│ [상황 2: 스캐너들의 교차 스캔 및 복사 준비] │
│ - Migration Scanner(오른쪽)는 옮길 수 있는 [ M ]을 찾는다. │
│ - Free Scanner(왼쪽)는 그 [ M ]을 집어넣을 [ F ]를 찾는다. │
│ │
│ [ M ] [ F ] [ M ] [ U ] [ F ] [ F ] [ M ] [ F ] [ M ] [ F ] │
│ ▲ ▲ │
│ (여기에 넣자) (얘를 옮기자) │
│ │
│ [상황 3: 데이터 이동 (Migration) 및 포인터 업데이트] │
│ 1. 오른쪽의 M(데이터)를 왼쪽의 F(빈 공간)로 물리적 복사한다. │
│ 2. 해당 애플리케이션의 Page Table을 새 물리 주소로 고친다. │
│ 3. 원래 오른쪽 M이 있던 자리는 빈 공간(F)으로 바꾼다. │
│ │
│ [ M ] [ M ] [ M ] [ U ] [ F ] [ F ] [ M ] [ F ] [ F ] [ F ] │
│ │
│ [상황 4: 두 스캐너가 중간에서 만남 (완료)] │
│ 결과: 사용 중인 페이지는 전부 왼쪽으로 몰리고, 오른쪽에는 연속된 거대한 │
│ [ F ] [ F ] [ F ] 공간이 창출됨! (Huge Page 할당 가능) │
└───────────────────────────────────────────────────────────────────┘
[다이어그램 해설] 커널 스레드는 메모리의 맨 밑에서 위로 올라가는 Free Scanner(빈칸 찾기)와 맨 위에서 아래로 내려오는 Migration Scanner(옮길 짐 찾기) 두 개의 포인터를 돌린다. 둘이 서로를 향해 다가오며 짐을 빈칸으로 복사해 던진다. 결과적으로 두 스캐너가 쾅 하고 부딪히는 순간 압축이 종료되며, 메모리의 앞단은 데이터로 꽉 차고(Packed), 메모리의 뒷단은 텅 빈 운동장(Contiguous Free Space)이 된다.
Direct Compaction (동기식) vs kcompactd (비동기식)
컴팩션을 수행하는 주체의 차이는 성능에 결정적 영향을 미친다.
- Direct Compaction: 앱이 2MB 거대 페이지를
malloc했다. 커널이 찾아보니 연속된 2MB가 없다. 즉시 앱을 강제로 블로킹(Sleep)시키고, 커널이 그 자리에서 헥헥대며 메모리 컴팩션을 수행한다. (앱 응답 속도 수백 밀리초 지연 발생 - 최악의 지터(Jitter)) - kcompactd (백그라운드): 시스템의 단편화 지수(Fragmentation Index)가 높아지면, 커널 백그라운드 스레드인
kcompactd가 앱 모르게 미리 깨어나서 뒤에서 조용히 메모리를 정리해 둔다. 나중에 앱이 2MB를 요구하면 즉시 할당해 준다. (현대 리눅스 튜닝의 핵심)
- 📢 섹션 요약 비유: 손님(앱)이 큰 방을 달라고 할 때 그제야 직원들이 빗자루질을 시작하며 손님을 세워두는 것(Direct)이 아니라, 야간 청소부(kcompactd)가 미리 큰 방을 만들어두어 손님이 오자마자 바로 키를 내어주는 구조입니다.
Ⅲ. 융합 비교 및 다각도 분석
커널 메모리 회수(Reclaim) 삼형제 비교
물리 메모리가 부족하거나 단편화되었을 때 커널이 꺼내는 3가지 카드다.
| 메커니즘 | kswapd (Page Reclaim) | kcompactd (Memory Compaction) | OOM Killer |
|---|---|---|---|
| 목적 | 물리적인 절대 용량 확보 | 연속된 빈 공간 (거대 페이지) 확보 | 시스템 붕괴 방어 |
| 방식 | 캐시 버리기, 디스크(Swap)로 내쫓기 | 디스크 I/O 없이 램 내부에서 위치만 이동 | 프로세스 강제 사살 |
| I/O 부하 | 매우 높음 (디스크 쓰기) | 거의 없음 (램-램 복사 연산) | 없음 |
| 성능 영향 | 치명적 (Thrashing 발생 시) | CPU는 다소 소모하나 I/O 락은 안 걸림 | 특정 서비스 사망 |
| 발동 조건 | Free 메모리가 워터마크 이하일 때 | 단편화 지수 높음 + THP 요청 대기 시 | 모든 수단이 실패했을 때 |
과목 융합 관점
-
자료구조 (Data Structure): 컴팩션의 성공을 가로막는 가장 큰 적은 C/C++ 프로그램이 남발하는 동적 메모리 할당(malloc)의 외부 단편화 파편들이다. 그래서 커널 내부 메모리(Unmovable)는 Slab Allocator 구조를 통해 자기들끼리만 단편화를 모아 관리하는 별도의 캐시를 구축했다.
-
가상화 (Cloud): 가상머신(VM) 하나에 32GB 램을 할당할 때, 하이퍼바이저가 4KB 페이지를 800만 번 쪼개서 주는 것과 2MB 튜지 페이지 1만 6천 개를 주는 것은 EPT(확장 페이지 테이블) 탐색 속도와 TLB 캐시 히트율에서 30% 이상의 성능 차이를 낸다. KVM 환경에서 Compaction 기능(
khugepaged)은 가상화 성능 최적화의 필수 전제조건이다. -
📢 섹션 요약 비유: kswapd가 빚쟁이를 내쫓아 집 전체의 평수를 늘리는 것이라면, kcompactd는 내부에 있는 짐들을 테트리스처럼 몰아서 거대한 안방 하나를 빼내는 인테리어 공사입니다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — Redis / JVM 서버의 순간적인 P99 Latency 스파이크 (수백 ms 멈춤): Redis 인메모리 DB 서버에서 1시간에 한 번씩 아무 이유 없이 명령어가 500ms 동안 멈추는(Hang) 지연 현상 발생.
- 원인 분석: 커널의 THP(Transparent Huge Pages) 설정이
always로 되어 있었다. Redis가 메모리를 요청할 때마다 커널은 2MB 페이지를 주려 했는데 단편화 때문에 빈 공간이 없었다. 그래서 커널이 Redis 프로세스를 멈춰 세우고 **Direct Compaction (동기식 압축)**을 돌리는 바람에 응답 속도가 박살 난 것이다. (Thp Defra 현상) - 대응 (기술사적 가이드):
- 1단계:
/sys/kernel/mm/transparent_hugepage/enabled를madvise로 변경하여, 애플리케이션이 명시적으로 요청할 때만 THP를 주게 하여 불필요한 컴팩션을 막는다. - 2단계:
/sys/kernel/mm/transparent_hugepage/defrag를defer+madvise로 변경하여, 당장 2MB가 없으면 일단 4KB로 먼저 빨리 응답해주고, 백그라운드 스레드(khugepaged,kcompactd)가 나중에 천천히 조각을 모아 2MB로 합쳐주게끔 비동기 튜닝을 적용한다.
- 1단계:
- 원인 분석: 커널의 THP(Transparent Huge Pages) 설정이
-
시나리오 — 임베디드 기기 / 안드로이드의 메모리 단편화 최적화 (ZRAM + Compaction): 스마트폰에서 앱을 여러 개 켜면 금방 버벅거리는 현상. 플래시 메모리(eMMC/UFS)로 스왑을 하면 수명이 닳아버림.
- 아키텍처 적용: 스왑을 디스크 대신 램 공간의 압축 영역으로 보내는 ZRAM을 활용한다. ZRAM 자체가 내부적으로 극심한 외부 단편화를 일으키므로, 최신 안드로이드(Linux) 커널의
zram모듈에 내장된 자체 컴팩션 알고리즘(zramctl --compact)을 주기적으로 트리거하여 물리 메모리 한계를 극복하고 앱 전환(Context Switch) 속도를 부드럽게 유지한다.
- 아키텍처 적용: 스왑을 디스크 대신 램 공간의 압축 영역으로 보내는 ZRAM을 활용한다. ZRAM 자체가 내부적으로 극심한 외부 단편화를 일으키므로, 최신 안드로이드(Linux) 커널의
의사결정 및 튜닝 플로우
┌───────────────────────────────────────────────────────────────────┐
│ 메모리 파편화 및 THP 튜닝 의사결정 플로우 │
├───────────────────────────────────────────────────────────────────┤
│ │
│ [서버 성능 모니터링 중 CPU %sys(시스템) 또는 %wait가 비정상적으로 튐] │
│ │ │
│ ▼ │
│ dmesg 또는 perf 커널 트레이스에 `compact_zone` 함수가 빈발하는가? │
│ ├─ 예 ─────▶ [THP(거대 페이지) Direct Compaction 오버헤드 확인] │
│ └─ 아니오 │
│ │ │
│ ▼ │
│ 워크로드가 TLB 미스율에 극도로 민감한가? (예: KVM 가상화, Oracle DB) │
│ ├─ 예 ─────▶ [THP 활성화 유지 및 kcompactd 튜닝] │
│ │ (compaction_proactiveness 값을 올려 백그라운드에서 │
│ │ 더 공격적으로 미리 조각 모음을 돌리게 세팅) │
│ │ │
│ └─ 아니오 ──▶ [THP 완전 비활성화 (never)] │
│ (Redis, Nginx 등 작은 할당이 많은 앱은 THP와 │
│ Compaction이 오히려 독이 됨) │
└───────────────────────────────────────────────────────────────────┘
[다이어그램 해설] 컴팩션은 "공짜"가 아니다. 램의 데이터를 램으로 복사하는 행위는 CPU 캐시를 모조리 오염시키고 메모리 버스를 낭비한다. 초보자는 KVM 서버의 성능을 높이겠다며 THP를 무조건 켜지만, 시스템이 단편화의 늪에 빠지면 KVM 전체가 멈추는 악몽을 겪는다. 아키텍트는 "미리 조각 모음을 해둘 것인가(CPU 약간 낭비), 아니면 아예 큰 방(THP)을 포기할 것인가(성능 약간 하락)" 중 하나를 선택해야 한다.
도입 체크리스트
-
Unmovable 페이지 비율 (Slab 팽창):
cat /proc/pagetypeinfo를 쳤을 때Unmovable블록이 메모리 전역에 흩뿌려져 있다면, 컴팩션 스캐너가 아무리 돌아도 거대 페이지를 만들 수 없다 (기둥이 너무 많아 벽을 못 허뭄). 이 경우 VFS 캐시(dentry/inode) 누수를 의심하고echo 3 > /proc/sys/vm/drop_caches를 통한 캐시 정리가 선행되어야 한다. -
NUMA 와의 관계: 컴팩션은 '동일한 NUMA 노드' 안에서만 이루어진다. 노드 0번이 꽉 차서 컴팩션이 도는데, 노드 1번에 거대한 빈 공간이 있다면 이는 컴팩션 설정의 문제가 아니라 NUMA 밸런싱의 문제임을 명확히 분리해서 진단해야 한다.
-
📢 섹션 요약 비유: 컴팩션 튜닝은 건물의 리모델링과 같습니다. 짐(Movable)은 치울 수 있지만 내력벽(Unmovable)은 못 치웁니다. 내력벽이 방 한가운데 박혀있다면, 아무리 청소부(kcompactd)를 돌려도 큰 거실(THP)은 절대 나오지 않습니다.
Ⅴ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 컴팩션 기능 부재 (오직 스왑만 의존) | kcompactd 백그라운드 컴팩션 | 개선 효과 |
|---|---|---|---|
| 정량 (I/O 부하) | 단편화 해소를 위해 디스크 I/O 발생 | 순수 메모리 연산으로 I/O 제로 | 스토리지 I/O 부하 대폭 감소 |
| 정량 (TLB 효율) | 파편화된 4K 페이지 사용으로 TLB Miss 폭증 | THP 확보로 2MB 연속 할당 지원 | KVM / 데이터베이스 성능 최대 20~30% 향상 |
| 정성 (지연 시간) | Direct Compaction으로 인한 앱 멈춤 | 비동기 선제적 압축(Proactive) | 시스템 지터(Jitter) 및 테일 레이턴시 억제 |
미래 전망
- Proactive Compaction 고도화: Linux 5.6 이후 컴팩션이 완전히 자동화된 Proactive Compaction 기능으로 진화했다. 과거에는 단편화가 임계치를 넘어야 헐레벌떡 동작했다면, 이제는 커널이 시스템의 유휴 시간(Idle)을 기가 막히게 감지해 앱 성능에 영향을 안 주는 순간에만 몰래 메모리를 빚어놓는 지능형 백그라운드 엔진이 되었다.
- eBPF를 통한 맞춤형 페이지 이주: 범용 컴팩션 알고리즘을 넘어서, 시스템 관리자가 eBPF를 통해 특정 애플리케이션의 메모리 주소 대역만을 집중적으로 모니터링하고 컴팩션을 지시하는 유저 스페이스 주도형 메모리 관리(User-driven Memory Management)가 활발히 연구되고 있다.
결론
커널 메모리 컴팩션(Compaction)은 "디스크가 아닌 메모리에서 왜 조각 모음이 필요한가?"라는 질문에 대한 리눅스 커널의 명쾌한 대답이다. 수 기가바이트에서 수 테라바이트에 달하는 램을 사용하는 현대의 인메모리 및 가상화 워크로드에서, 4KB 단위의 잘게 쪼개진 메모리는 TLB 캐시의 한계로 인해 치명적인 성능 저하를 부른다. 컴팩션 스레드(kcompactd)는 마치 백조의 물갈퀴처럼, 겉으로는 평온해 보이는 앱의 런타임 아래에서 끊임없이 메모리를 재배열하여 시스템이 재부팅 없이 수년간 최고 성능을 낼 수 있도록 지탱하는 OS의 숨은 영웅이다.
- 📢 섹션 요약 비유: 수만 권의 책(페이지)이 흩어진 도서관에서, 손님(앱)이 눈치채지 못하게 책장을 무빙워크로 끝없이 재배치하여 언제든 전집 세트(거대 페이지)를 한 번에 빌려갈 수 있게 만드는 커널 사서의 보이지 않는 땀방울입니다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| THP (Transparent Huge Pages) | 컴팩션 기능이 존재하는 궁극적 이유. 4KB 페이지 512개를 묶어 2MB짜리 연속된 거대 페이지를 만들어 TLB 히트율을 올리는 기술. |
| 외부 단편화 (External Fragmentation) | 총 빈 공간은 많지만 연속된 덩어리가 없어서 큰 메모리(THP) 할당이 실패하는 현상으로, 컴팩션의 척결 대상 |
| Buddy Allocator (버디 할당기) | 리눅스의 물리 메모리 할당 기본 알고리즘으로, 태생적으로 외부 단편화를 유발하기 때문에 컴팩션과 영원히 공생함 |
| Page Migration (페이지 이주) | NUMA 노드 간 이동이나 컴팩션에서 물리적으로 메모리 데이터를 복사하고 페이지 테이블(PTE)을 갈아 끼우는 핵심 메커니즘 |
| Direct Compaction | kcompactd가 미처 일하지 못한 상태에서 앱이 강제로 2MB 할당을 요청했을 때, 앱을 멈추고 강제로 압축을 돌려 레이턴시 스파이크를 유발하는 주범 |
👶 어린이를 위한 3줄 비유 설명
- 버스(메모리)에 빈자리는 20개나 있는데, 사람들이 전부 한 자리씩 건너 띄어서 흩어져 앉아 있어요(단편화).
- 이때 한 가족 4명(거대 페이지)이 타서 "저희 4명이 다 같이 붙어 앉고 싶어요!"라고 해요. 자리가 없어서 태울 수가 없죠?
- 그래서 버스 안내원(컴팩션)이 나타나서 "손님들~ 전부 앞쪽으로 쫙 밀착해서 앉아주세요!"라고 합니다. 사람들이 앞으로 몰려 앉으니, 뒤쪽에 4명이 다 같이 앉을 수 있는 큰 빈 공간이 생겨났어요!