메모리는 넉넉히 남아 있는데 huge page나 큰 DMA 버퍼 할당이 실패하는 경우가 있다. 총 여유 메모리량과 별개로, 그 여유분이 4KB 단위로 잘게 흩어져 있으면 연속된 큰 블록이 필요한 할당은 실패한다. 리눅스는 이 상태를 /proc/buddyinfo로 그대로 노출하는데, 이 글에서는 일부러 메모리를 체커보드 패턴으로 할당·해제해 단편화를 만들고, /proc/sys/vm/compact_memory로 강제 compaction을 걸어 얼마나 회복되는지 실측한다. buddyinfo 하나만으로는 판단이 애매할 때 참고할 extfrag_index/unusable_index, compaction 관련 /proc/vmstat 카운터와 sysctl 튜너블도 함께 정리한다.
/proc/buddyinfo 읽는 법
$ cat /proc/buddyinfo
Node 0, zone DMA 0 0 0 0 0 0 0 0 0 1 3
Node 0, zone DMA32 2127 709 403 267 191 127 106 68 50 33 794
Node 0, zone Normal 5247 5882 3554 2132 1123 639 528 317 192 86 89
존(zone) 이름 뒤로 order 0부터 10까지, 각 order 크기의 연속된 free 블록이 몇 개 있는지 나열된다. order는 2order 페이지(4KB) 단위 블록 크기를 뜻한다.
| order | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 블록 크기 | 4KB | 8KB | 16KB | 32KB | 64KB | 128KB | 256KB | 512KB | 1MB | 2MB | 4MB |
order 9(2MB)는 huge page 한 장, order 10(4MB) 블록의 개수가 적으면 그만큼 큰 연속 할당이 실패하기 쉽다는 신호다.
단편화 재현 — 체커보드 할당
4KB 페이지 3만 개를 개별 mmap으로 할당해 전부 fault-in한 뒤, 짝수 인덱스만 munmap으로 해제했다. 남은 페이지들이 한 칸씩 건너뛰며 흩어져 있어 buddy 시스템이 인접한 free 페이지를 합칠 수 없는 상태다.
import mmap
regions = []
for i in range(30000):
m = mmap.mmap(-1, 4096)
m[0] = 1 # 페이지 폴트 유발
regions.append(m)
for i in range(0, 30000, 2): # 절반만 해제(체커보드)
regions[i].close()
regions[i] = None같은 프로세스가 살아있는 상태로 /proc/buddyinfo(Normal zone)를 order 0~10 전체를 베이스라인·단편화 직후·compaction 후로 나란히 비교했다.
| order | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 베이스라인 | 5247 | 5882 | 3554 | 2132 | 1123 | 639 | 528 | 317 | 192 | 86 | 89 |
| 단편화 후 | 11506 | 473 | 939 | 907 | 971 | 634 | 520 | 314 | 192 | 84 | 85 |
| compaction 후 | 4609 | 2325 | 1070 | 651 | 419 | 239 | 150 | 96 | 74 | 118 | 178 |
단편화 직후 order 0(단일 페이지) free 개수는 오히려 늘었지만(해제한 페이지 자체가 여기 잡힘) order 1의 free 블록은 5882→473으로 급감했다. 해제한 페이지 양옆이 전부 사용 중이라 buddy가 짝을 맞춰 병합하지 못하기 때문이다. compaction 후에는 order 1~10 대부분이 베이스라인 수준 이상으로 회복됐다.
extfrag_index / unusable_index로 더 정확히 보기
buddyinfo는 order별 블록 개수만 보여줘서 “이 정도면 문제인지” 판단이 애매하다. 커널은 debugfs에 이를 지수로 정리한 두 파일을 추가로 제공한다.
| 파일 | 의미 |
/sys/kernel/debug/extfrag/unusable_index | 해당 order 할당에 쓸 수 없는 free 메모리의 비율. 0=전부 사용 가능, 1=전부 단편화로 사용 불가 |
/sys/kernel/debug/extfrag/extfrag_index | 할당 실패가 단편화 때문일 확률(0~1, 1에 가까울수록 단편화가 원인). 메모리 자체가 넉넉해 실패 확률이 없으면 -1(해당 없음)로 표시된다 |
# 단편화 직후 (Normal zone, order 0~10)
$ sudo cat /sys/kernel/debug/extfrag/unusable_index | grep Normal
Node 0, zone Normal 0.000 0.047 0.050 0.061 0.084 0.134 0.198 0.303 0.431 0.587 0.723
# compaction 후
$ sudo cat /sys/kernel/debug/extfrag/unusable_index | grep Normal
Node 0, zone Normal 0.000 0.013 0.028 0.041 0.057 0.078 0.103 0.133 0.172 0.232 0.423
단편화 직후 order 10(4MB) 할당은 72.3%가 단편화로 쓸 수 없는 상태였는데, compaction 후에는 42.3%로 줄었다 — buddyinfo의 개수 변화를 하나의 비율로 요약해서 보여준다.
강제 compaction과 vmstat 카운터
체커보드 상태를 유지한 채 compact_memory에 1을 써서 강제로 압축을 실행하고, /proc/vmstat의 compaction 관련 카운터를 실행 전후로 비교했다.
$ grep '^compact_' /proc/vmstat # 실행 전(전부 0)
compact_migrate_scanned 0
compact_free_scanned 0
compact_isolated 0
compact_stall 0
compact_fail 0
compact_success 0
$ echo 1 | sudo tee /proc/sys/vm/compact_memory
$ grep '^compact_' /proc/vmstat # 실행 후
compact_migrate_scanned 532991
compact_free_scanned 408111
compact_isolated 340566
compact_stall 0
compact_fail 0
compact_success 0
compact_migrate_scanned/free_scanned/isolated는 크게 늘어 실제로 페이지를 스캔하고 옮겼다는 걸 보여주지만, compact_stall/compact_success/compact_fail은 0 그대로다. 이 세 카운터는 할당 실패로 커널이 직접(direct) compaction을 트리거했을 때만 증가하고, compact_memory로 수동 실행한 경우에는 집계되지 않기 때문이다. 그래서 프로덕션에서 compaction이 실제로 얼마나 자주, 왜 도는지 보려면 이 카운터들을 수동 실행이 아니라 평소 워크로드 상태로 관찰해야 한다.
단편화 재현 실험에서 절반(1만 5천 개)은 여전히 실제로 사용 중인 페이지라 그만큼은 어차피 자리를 차지하고 있어 완전한 병합이 불가능하다. compaction은 이동 가능한(movable) 페이지를 옮겨 파편화를 줄여줄 뿐, 실제 사용 중인 메모리 자체를 없애주지는 못한다.
실전 영향 — huge page 할당 성공률
buddyinfo 숫자가 실제로 무슨 의미인지 체감하려면 hugepage(2MB, order 9) 할당을 직접 걸어보면 된다. 처음에는 4KB 페이지 50만 개(2GB)만 체커보드로 깔았는데, 총 7.8GB 메모리 중 건드리지 않은 나머지 영역과 회수 가능한 페이지 캐시만으로도 buddy가 order 9/10 블록을 손쉽게 채워 4GB짜리 hugepage 요청(2000개)이 compaction 없이도 그냥 성공해버렸다. 단편화가 할당 실패로 이어지려면 시스템 전체 free 메모리 대부분이 흩어져 있어야 한다는 뜻이다.
그래서 vm.max_map_count(기본 1,048,576 — 프로세스당 최대 mmap 매핑 개수)를 일시적으로 늘리고, 170만 개 페이지(6.8GB)를 체커보드로 깔아 시스템 전체를 실제로 압박한 뒤 다시 테스트했다.
# 단편화 상태에서(order 9/10 블록이 거의 남지 않은 상태)
$ echo 1000 | sudo tee /proc/sys/vm/nr_hugepages
$ grep HugePages_Total /proc/meminfo
HugePages_Total: 112 ← 1000개(2GB) 요청했는데 112개(11.2%)만 확보
$ echo 1 | sudo tee /proc/sys/vm/compact_memory # compaction 실행
$ echo 1000 | sudo tee /proc/sys/vm/nr_hugepages # 재요청
$ grep HugePages_Total /proc/meminfo
HugePages_Total: 809 ← compaction 후 809개(80.9%) 확보
진짜 시스템 전체가 눌린 상태에서는 compaction 한 번으로 확보율이 11.2%→80.9%까지 뛰었다. 100%가 아닌 건 스왑까지 쓰던 상태라 일부 페이지 이동이 지연됐기 때문으로 보인다 — 그래도 /proc/buddyinfo의 높은 order 컬럼이 왜 중요한지, 그리고 왜 “메모리가 남는데 할당이 실패한다”는 증상이 실제로 재현되려면 단편화가 국지적이지 않고 zone 전체에 걸쳐 있어야 하는지를 이 결과가 직접 보여준다.
관련 sysctl 튜너블
| sysctl | 기본값(테스트 호스트) | 설명 |
vm.compact_memory | – | 1을 쓰면 즉시 전체 zone에 강제 compaction 실행(쓰기 전용) |
vm.compaction_proactiveness | 20 | 0~100. kcompactd가 백그라운드에서 얼마나 적극적으로 미리 압축해둘지 결정. 0이면 사실상 비활성화 |
vm.extfrag_threshold | 500 | 0~1000. direct compaction을 시도할지 말지 가르는 임계값(레거시 경로, 커널 버전에 따라 영향이 제한적) |
vm.min_free_kbytes | 67584 | 커널이 항상 비워두려는 최소 free 메모리. 값이 크면 워터마크 여유가 늘어 compaction/reclaim이 더 일찍 발동 |
vm.compact_unevictable_allowed | 1 | mlock된 unevictable 페이지도 compaction 대상에 포함할지 여부 |
주의사항
| 항목 | 내용 |
|---|---|
| compaction도 CPU/지연 비용이 있다 | 페이지를 옮기는 동안 해당 페이지에 접근하는 스레드는 잠깐 블로킹될 수 있다. 프로덕션에서 수동 트리거는 트래픽이 적은 시간대에 |
| 모든 페이지가 이동 가능한 건 아니다 | 커널 슬랩 할당이나 mlock된 페이지, DMA 버퍼 일부는 movable로 분류되지 않아 compaction으로도 옮길 수 없다 |
| 자동 compaction도 동작한다 | kcompactd가 백그라운드에서 주기적으로 낮은 강도의 compaction을 수행한다. 수동 트리거는 huge page 할당 직전처럼 즉각적인 회복이 필요할 때만 |
| vmstat 카운터 해석 주의 | compact_stall/compact_success/compact_fail은 할당 실패로 인한 direct compaction만 집계한다. 수동 compact_memory 실행은 migrate_scanned/free_scanned/isolated만 늘려 놓친다 |
| extfrag_index의 -1은 “괜찮다”는 뜻이 아닐 수 있다 | 해당 order 할당에 실패할 상황 자체가 아니라서 지수 계산이 생략된 것뿐이다. 실제 단편화 정도는 unusable_index로 함께 확인한다 |
| root 권한 필요 | /proc/sys/vm/compact_memory 쓰기, /sys/kernel/debug/extfrag/* 읽기 모두 sudo 없이 불가능 |
마무리
여유 메모리가 충분한데도 큰 블록 할당이 실패한다면 /proc/buddyinfo의 높은 order 컬럼과 unusable_index부터 확인할 것. compaction은 파편화를 완화할 뿐 만능은 아니고, 실험에서 보였듯 zone 전체가 실제로 눌려 있어야 효과가 극적으로 드러난다. 근본적으로 큰 연속 블록이 자주 필요한 워크로드(huge page 등)라면 애초에 부팅 초기에 hugepages= 파라미터로 미리 확보해두는 편이 안전하다. 메모리 회수·OOM 관련해서는 아래 글도 같이 참고할 만하다.
참고
- Kernel.org – Documentation for /proc/sys/vm/*: compact_memory, compaction_proactiveness, extfrag_threshold, min_free_kbytes 등 공식 sysctl 설명
- Kernel.org – Transparent Hugepage Support
- proc(5) man page:
/proc/buddyinfo,/proc/vmstat필드 설명