Swapping 서브시스템
Linux 커널 Swapping 메커니즘: swap 공간 설정, swap cache, swap out/in 경로, swappiness 튜닝, zswap/zram 압축 스왑, 성능 모니터링 종합 가이드.
핵심 요약
- Swap 공간 — 물리 메모리 부족 시 익명 페이지를 임시 저장하는 디스크 영역입니다.
- Swap Cache — swap out된 페이지를 RAM에 캐싱하여, 다시 접근 시 디스크 I/O 없이 빠르게 복원합니다.
- kswapd — 백그라운드에서 메모리 부족을 미리 감지하고 페이지를 회수하는 커널 스레드입니다.
- swappiness — 익명 페이지(swap)와 파일 페이지(page cache) 중 어느 쪽을 먼저 회수할지 비율을 조정하는 커널 파라미터입니다.
- zswap / zram — 디스크 대신 압축된 RAM 영역을 swap으로 사용하여 성능을 크게 개선합니다.
단계별 이해
- Swap의 필요성 — 익명 페이지(힙, 스택, mmap anonymous)는 backing store가 없어 swap 없이는 회수 불가능합니다.
파일 페이지는 원본 파일에서 다시 읽을 수 있지만, 익명 페이지는 swap이 유일한 저장소입니다.
- Swap Out/In 과정 — kswapd가 메모리 압력을 감지하면 LRU 리스트에서 오래 사용되지 않은 페이지를 선택하여 디스크에 기록하고, 물리 페이지를 회수합니다.
나중에 해당 페이지에 접근하면 page fault가 발생하고, 커널이 swap 영역에서 다시 읽어옵니다(swap in).
- Swap Cache 최적화 — swap out된 페이지가 아직 RAM에 남아 있으면, 다시 접근 시 디스크 I/O 없이 즉시 복원됩니다.
이를 통해 반복적으로 접근되는 페이지의 swap in 비용을 크게 줄입니다.
- zswap/zram 압축 스왑 — 디스크 대신 압축된 메모리를 사용하여 swap 성능을 10배 이상 개선할 수 있습니다.
zswap은 기존 swap의 캐시 역할, zram은 독립적인 블록 디바이스로 동작하며 모바일/임베디드 환경에서 필수입니다.
Swapping 서브시스템
Swapping은 물리 메모리(RAM) 부족 시 익명 페이지(anonymous page)를 디스크의 스왑 영역으로 내보내고, 다시 접근할 때 복원하는 메커니즘입니다. 파일 기반 페이지는 원본 파일에서 다시 읽을 수 있지만, 힙·스택·mmap(MAP_ANONYMOUS) 등 backing store가 없는 익명 페이지는 스왑 영역이 유일한 저장소입니다.
Swap vs Page Cache 회수: 커널의 메모리 회수(reclaim)는 두 가지 경로로 동작합니다. 파일 페이지(page cache)는 clean이면 즉시 버리고 dirty면 원본 파일에 writeback 후 회수합니다. 익명 페이지는 스왑 영역에 기록(swap out)해야만 회수할 수 있습니다. vm.swappiness로 이 두 경로의 비율을 조정합니다.
Swap 공간 설정
스왑 영역은 전용 파티션 또는 스왑 파일로 구성할 수 있습니다. 여러 스왑 영역을 동시에 사용할 수 있으며, 우선순위(priority)로 사용 순서를 제어합니다.
# === 스왑 파티션 설정 ===
mkswap /dev/sda2 # 파티션을 스왑으로 포맷
swapon /dev/sda2 # 스왑 활성화
swapon -p 10 /dev/sda2 # 우선순위 10으로 활성화
# === 스왑 파일 설정 ===
fallocate -l 4G /swapfile # 4GB 파일 생성
chmod 600 /swapfile # 권한 제한 (필수)
mkswap /swapfile # 스왑 포맷
swapon /swapfile # 활성화
# === /etc/fstab 영구 설정 ===
# /dev/sda2 none swap sw,pri=10 0 0
# /swapfile none swap sw,pri=5 0 0
# === 스왑 상태 확인 ===
swapon --show # 활성 스왑 영역 목록
cat /proc/swaps # 동일 정보 (proc 인터페이스)
free -h # 스왑 사용량 요약
# === 스왑 비활성화 ===
swapoff /dev/sda2 # 스왑 인 후 비활성화 (시간 소요)
swapoff -a # 모든 스왑 비활성화
스왑 파일 주의사항: Btrfs에서 스왑 파일을 사용하려면 chattr +C로 COW를 비활성화하고 별도 서브볼륨에 생성해야 합니다. ext4에서 fallocate 대신 dd를 사용해야 하는 오래된 커널(< 5.0)도 있으므로 주의하십시오. 스왑 파일은 반드시 chmod 600으로 권한을 제한해야 합니다.
우선순위(Priority) 동작 방식: 동일한 우선순위를 가진 스왑 영역들은 라운드 로빈으로 사용되어 I/O가 분산됩니다(RAID-0과 유사). 우선순위가 다르면 높은 우선순위의 영역을 먼저 사용하고, 가득 차면 낮은 우선순위로 넘어갑니다.
# 우선순위 기반 스왑 계층 구성 예시
swapon -p 100 /dev/zram0 # 1순위: zram (압축 메모리, 가장 빠름)
swapon -p 10 /dev/nvme0n1p2 # 2순위: NVMe SSD
swapon -p 1 /swapfile # 3순위: HDD 스왑 파일 (가장 느림)
# 결과 확인
swapon --show
# NAME TYPE SIZE USED PRIO
# /dev/zram0 partition 2G 0B 100
# /dev/nvme0n1p2 partition 8G 0B 10
# /swapfile file 4G 0B 1
Swap 핵심 자료구조
스왑 서브시스템은 세 가지 핵심 자료구조로 구성됩니다.
/* include/linux/swap.h — 스왑 영역 정보 */
struct swap_info_struct {
unsigned long flags; /* SWP_USED | SWP_WRITEOK 등 */
signed short prio; /* 스왑 우선순위 */
struct plist_node list; /* 우선순위 정렬 리스트 */
signed char type; /* 스왑 영역 인덱스 (0~MAX_SWAPFILES-1) */
unsigned int max; /* 최대 스왑 슬롯 수 */
unsigned char *swap_map; /* 슬롯별 참조 카운트 배열 */
struct swap_cluster_info *cluster_info; /* 클러스터별 정보 */
struct swap_cluster_list free_clusters; /* 빈 클러스터 리스트 */
unsigned int lowest_bit; /* 빈 슬롯 탐색 힌트 (시작) */
unsigned int highest_bit; /* 빈 슬롯 탐색 힌트 (끝) */
unsigned int pages; /* 사용 가능 총 페이지 수 */
unsigned int inuse_pages; /* 사용 중인 페이지 수 */
unsigned int cluster_next; /* 다음 할당 위치 힌트 */
unsigned int cluster_nr; /* 현재 클러스터 내 위치 */
struct percpu_cluster __percpu *percpu_cluster; /* Per-CPU 할당 */
struct block_device *bdev; /* 스왑 블록 디바이스 */
struct file *swap_file; /* 스왑 파일 (파일 기반 시) */
unsigned int old_block_size; /* 이전 블록 크기 */
};
/* include/linux/swapops.h — 스왑 엔트리 인코딩 */
/*
* swp_entry_t: PTE에 저장되는 스왑 위치 정보
* 페이지가 스왑 아웃되면, PTE의 present 비트가 0이 되고
* 나머지 비트에 스왑 영역 인덱스(type)와 오프셋(offset)이 인코딩됩니다.
*
* x86_64 레이아웃 (64비트 PTE):
* ┌──────────────────────────────────────────────────────┐
* │ bit 63..58 │ bit 57..5 │ bit 4..1 │ bit 0 │
* │ (unused) │ offset (53 bits) │ type (4b) │ P=0 │
* └──────────────────────────────────────────────────────┘
* P=0이므로 MMU는 page fault 발생 → do_swap_page() 호출
*/
typedef struct {
unsigned long val;
} swp_entry_t;
/* swp_entry_t 조작 매크로/함수 */
swp_type(entry) /* 스왑 영역 인덱스 추출 (0~MAX_SWAPFILES-1) */
swp_offset(entry) /* 스왑 영역 내 슬롯 오프셋 추출 */
swp_entry(type, off) /* type + offset → swp_entry_t 생성 */
/* PTE ↔ swp_entry_t 변환 */
pte_to_swp_entry(pte) /* non-present PTE → swp_entry_t */
swp_entry_to_pte(ent) /* swp_entry_t → non-present PTE */
/* mm/swap_state.c — swap_map: 슬롯별 참조 카운트 */
/*
* swap_map[offset] 값의 의미:
* 0 : 빈 슬롯 (할당 가능)
* 1~SWAP_MAP_MAX : 참조 카운트 (해당 슬롯을 참조하는 PTE 수)
* SWAP_MAP_BAD : 불량 슬롯 (사용 불가)
* SWAP_HAS_CACHE : 스왑 캐시에 존재 (비트 OR)
*
* 참조 카운트가 여러 개인 경우:
* fork() 시 CoW로 공유된 익명 페이지가 스왑 아웃되면
* 부모와 자식 프로세스의 PTE가 동일한 swap entry를 가리킴
*/
#define SWAP_HAS_CACHE 0x40 /* 스왑 캐시에 페이지 존재 */
#define SWAP_MAP_MAX 0x3e /* 최대 참조 카운트 (62) */
#define SWAP_MAP_BAD 0x3f /* 불량 슬롯 표시 */
#define SWAP_MAP_SHMEM 0x20 /* shmem/tmpfs 전용 참조 */
Swap Cache
Swap Cache는 스왑 영역과 메모리 사이의 중간 캐시 계층입니다. 페이지가 스왑 아웃/인될 때 일시적으로 스왑 캐시에 존재하며, 동일 페이지에 대한 중복 I/O를 방지하고 fork된 프로세스 간 일관성을 보장합니다.
/* mm/swap_state.c — Swap Cache 핵심 함수 */
/*
* swapper_spaces[]: 스왑 영역별 address_space 배열
* 각 address_space의 XArray에 swap offset → struct page 매핑 저장
* 일반 파일의 page cache와 동일한 인터페이스(find_get_page 등) 사용
*/
struct address_space *swapper_spaces[MAX_SWAPFILES];
/* 페이지를 Swap Cache에 추가 (swap out 시) */
int add_to_swap_cache(struct page *page, swp_entry_t entry,
gfp_t gfp, void **shadowp)
{
struct address_space *address_space = swap_address_space(entry);
pgoff_t idx = swp_offset(entry);
SetPageSwapCache(page); /* PG_swapcache 플래그 설정 */
set_page_private(page, entry.val); /* page->private에 swap entry 저장 */
/* XArray에 page 삽입 (page cache와 동일한 방식) */
xa_store(&address_space->i_pages, idx, page, gfp);
...
}
/* Swap Cache에서 페이지 검색 (swap in 시) */
struct page *lookup_swap_cache(swp_entry_t entry,
struct vm_area_struct *vma,
unsigned long addr)
{
struct page *page;
page = find_get_page(swap_address_space(entry),
swp_offset(entry));
if (page) {
/* Swap Cache 히트 — 디스크 I/O 없이 즉시 반환 */
mark_page_accessed(page);
}
return page;
}
Swap Cache vs Page Cache: Swap Cache는 사실상 Page Cache의 특수한 형태입니다. 일반 파일 페이지의 page->mapping이 파일의 address_space를 가리키듯, 스왑 캐시 페이지의 page->mapping은 swapper_spaces[]의 address_space를 가리킵니다. /proc/meminfo의 SwapCached 항목이 현재 스왑 캐시 크기를 나타냅니다.
페이지 Swap Out 경로
메모리 회수(reclaim) 과정에서 익명 페이지를 스왑 영역에 기록하는 전체 흐름입니다.
/*
* Swap Out 전체 경로 (간략화):
*
* kswapd / direct_reclaim
* → shrink_node()
* → shrink_lruvec()
* → shrink_list() ← inactive anon LRU 리스트 순회
* → shrink_folio_list()
* → add_to_swap() ← 1. 스왑 슬롯 할당 + 스왑 캐시 등록
* → pageout()
* → swap_writepage() ← 2. 디스크에 기록
* → try_to_unmap() ← 3. 모든 PTE에서 매핑 제거
* → rmap walk
* → try_to_unmap_one() ← PTE를 swap entry로 교체
* → free the page ← 4. 페이지 프레임 해제
*/
/* mm/vmscan.c — add_to_swap(): 스왑 슬롯 할당 핵심 */
bool add_to_swap(struct folio *folio)
{
swp_entry_t entry;
/* 1. 빈 스왑 슬롯 할당 (우선순위 기반) */
entry = folio_alloc_swap(folio);
if (!entry.val)
return false; /* 스왑 공간 부족 */
/* 2. Swap Cache에 등록 */
if (add_to_swap_cache(folio, entry, ...))
return true;
/* 실패 시 슬롯 반환 */
put_swap_folio(folio, entry);
return false;
}
/* mm/page_io.c — swap_writepage(): 실제 디스크 기록 */
int swap_writepage(struct page *page, struct writeback_control *wbc)
{
/* zswap이 활성화되어 있으면 압축 저장 시도 */
if (zswap_store(folio)) {
count_vm_event(ZSWPOUT);
return 0; /* zswap에 저장 성공 → 디스크 I/O 회피 */
}
/* 블록 디바이스에 비동기 기록 */
__swap_writepage(page, wbc);
...
}
/* mm/rmap.c — try_to_unmap_one(): PTE를 swap entry로 교체 */
static bool try_to_unmap_one(struct folio *folio,
struct vm_area_struct *vma, unsigned long address, ...)
{
pte_t pteval;
swp_entry_t entry;
/* 현재 PTE 값 읽기 및 unmap */
pteval = ptep_clear_flush(vma, address, pvmw.pte);
/* page->private에서 swap entry 추출 */
entry = make_readable_migration_entry(page_to_pfn(page));
if (PageSwapCache(page)) {
entry.val = page_private(page); /* swap entry */
}
/* PTE를 swap entry로 교체 (present=0) */
set_pte_at(mm, address, pvmw.pte,
swp_entry_to_pte(entry));
...
}
페이지 Swap In 경로
프로세스가 스왑 아웃된 페이지에 접근하면 page fault가 발생하고, do_swap_page()가 호출되어 페이지를 복원합니다.
/*
* Swap In 전체 경로:
*
* CPU가 PTE 접근 → present=0 → page fault
* → handle_pte_fault()
* → 비어있지 않은 non-present PTE → do_swap_page()
*
* do_swap_page() 내부:
* 1. PTE에서 swp_entry_t 추출
* 2. Swap Cache에서 페이지 검색 (hit이면 I/O 불필요)
* 3. Cache miss → swap_readpage()로 디스크에서 읽기
* 4. 읽은 페이지를 Swap Cache에 등록
* 5. PTE를 유효한 매핑으로 복원 (present=1)
* 6. swap_map 참조 카운트 감소
* 7. 참조 카운트가 0이면 → Swap Cache에서 제거 + 슬롯 해제
*/
/* mm/memory.c — do_swap_page() 핵심 로직 (간략화) */
vm_fault_t do_swap_page(struct vm_fault *vmf)
{
swp_entry_t entry;
struct page *page;
pte_t pte;
/* 1. PTE에서 swap entry 추출 */
entry = pte_to_swp_entry(vmf->orig_pte);
/* 2. Swap Cache 검색 */
page = lookup_swap_cache(entry, vma, vmf->address);
if (!page) {
/* 3. Cache miss → 디스크에서 읽기 */
page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE,
vmf);
if (!page)
return VM_FAULT_OOM;
}
/* 4. 페이지 잠금 및 유효성 검증 */
lock_page(page);
/* 5. PTE를 유효한 매핑으로 복원 */
pte = mk_pte(page, vma->vm_page_prot);
if (vmf->flags & FAULT_FLAG_WRITE)
pte = maybe_mkwrite(pte_mkdirty(pte), vma);
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
/* 6. swap_map 참조 카운트 감소 */
swap_free(entry);
/* 참조 카운트가 0이면 Swap Cache에서도 제거됨 */
return 0;
}
/* mm/page_io.c — swap_readpage(): 디스크에서 읽기 */
int swap_readpage(struct page *page, bool synchronous,
struct swap_iocb **plug)
{
/* zswap에서 먼저 검색 (압축 저장된 경우) */
if (zswap_load(folio)) {
count_vm_event(ZSWPIN);
SetPageUptodate(page);
return 0; /* zswap에서 복원 성공 */
}
/* 블록 디바이스에서 비동기/동기 읽기 */
submit_bio(bio);
...
}
Swap Readahead
스왑 인 시 인접 페이지를 미리 읽어 성능을 개선합니다. 커널은 두 가지 readahead 전략을 사용합니다.
| 전략 | 방식 | 적합한 상황 | 설정 |
|---|---|---|---|
| 클러스터 readahead | 스왑 영역에서 물리적으로 인접한 슬롯들을 함께 읽기 | 순차 접근 패턴, HDD | /proc/sys/vm/page-cluster (2^n 페이지, 기본=3 → 8페이지) |
| VMA readahead | 가상 주소 공간에서 인접한 스왑 엔트리들을 함께 읽기 | 연속적 가상 메모리 접근, SSD | 자동 (swap in 패턴 분석) |
# Swap readahead 크기 설정
# page-cluster: 2^N 페이지를 한 번에 readahead
cat /proc/sys/vm/page-cluster # 기본값: 3 (2^3 = 8 페이지 = 32KB)
# SSD에서는 줄이는 것이 유리 (랜덤 읽기 비용이 낮음)
echo 0 > /proc/sys/vm/page-cluster # readahead 비활성화
echo 1 > /proc/sys/vm/page-cluster # 2페이지만 readahead
# HDD에서는 높은 값이 유리 (순차 읽기가 빠름)
echo 4 > /proc/sys/vm/page-cluster # 16페이지 readahead
Swappiness 튜닝
vm.swappiness는 커널의 메모리 회수 시 익명 페이지(swap out)와 파일 페이지(page cache 회수)의 상대적 비율을 조절합니다.
| 값 | 동작 | 적합한 워크로드 |
|---|---|---|
0 |
가능한 한 스왑 안 함 (파일 캐시를 우선 회수, 메모리 극히 부족할 때만 스왑) | 데이터베이스, 실시간 시스템 |
10 |
스왑을 최소화하되 필요 시 약간 허용 | 데스크톱, 일반 서버 |
60 |
기본값 — 파일 캐시와 익명 페이지를 균형 있게 회수 | 범용 서버 |
100 |
익명 페이지와 파일 페이지를 동일 비율로 회수 | 대용량 파일 캐시 유지가 중요한 경우 |
200 |
익명 페이지를 적극적으로 스왑 (cgroup v2 전용, 6.1+) | 메모리 오버커밋, 컨테이너 환경 |
# 전역 swappiness 설정
sysctl vm.swappiness=10 # 스왑 최소화
sysctl -w vm.swappiness=60 # 기본값으로 복원
# 영구 설정 (/etc/sysctl.conf)
# vm.swappiness = 10
# cgroup v2: 그룹별 독립 swappiness 설정
echo 0 > /sys/fs/cgroup/mydb/memory.swap.max # 해당 cgroup 스왑 금지
echo 10 > /sys/fs/cgroup/myapp/memory.swappiness # 그룹별 swappiness (커널/배포판별 지원 범위 확인)
/* mm/vmscan.c — swappiness가 reclaim 비율에 미치는 영향 (간략화) */
static void get_scan_count(struct lruvec *lruvec,
struct scan_control *sc,
unsigned long *nr)
{
unsigned long ap, fp; /* anon pressure, file pressure */
unsigned long swappiness = mem_cgroup_swappiness(memcg);
/* swappiness가 0이면 → 메모리 극히 부족할 때만 anon 회수 */
if (!swappiness) {
/* 파일 캐시를 우선 회수, free가 극히 낮으면 anon도 회수 */
fraction[0] = 0; /* anon scan = 0 */
fraction[1] = 1; /* file scan = 전체 */
return;
}
/*
* anon과 file의 스캔 비율 결정:
* ap = swappiness * (최근 anon 참조 빈도의 역수)
* fp = (200 - swappiness) * (최근 file 참조 빈도의 역수)
* → swappiness가 높을수록 anon 스캔 비율 증가
*/
ap = swappiness * (total_cost + 1);
ap /= anon_cost + 1;
fp = (200 - swappiness) * (total_cost + 1);
fp /= file_cost + 1;
fraction[0] = ap; /* anon LRU 스캔 비율 */
fraction[1] = fp; /* file LRU 스캔 비율 */
}
swappiness=0은 스왑 완전 비활성이 아닙니다. vm.swappiness=0으로 설정해도, 시스템 전체 free 메모리가 zone의 high watermark + file cache보다 낮아지면 커널은 여전히 익명 페이지를 스왑 아웃합니다. 스왑을 완전히 금지하려면 swapoff -a로 스왑 영역을 비활성화하거나, cgroup v2에서 memory.swap.max=0으로 설정해야 합니다.
Multi-Gen LRU (MGLRU)
커널 6.1에 도입된 MGLRU는 기존의 active/inactive 2-리스트 LRU를 다중 세대(generation)로 확장하여, 페이지의 접근 빈도를 더 정밀하게 추적합니다. 이를 통해 스왑 아웃/페이지 회수 결정의 정확도가 크게 향상되어, 특히 메모리 부족 시 성능 저하가 줄어듭니다.
# MGLRU 활성화 상태 확인 (CONFIG_LRU_GEN 필요)
cat /sys/kernel/mm/lru_gen/enabled
# 0x0007 = 모든 기능 활성화 (Y+Y+Y)
# 비트 0: lru_gen 코어 활성화
# 비트 1: lru_gen에 의한 reclaim 활성화
# 비트 2: mm_walk(페이지 테이블 스캔)으로 세대 결정 활성화
# MGLRU 활성화/비활성화
echo 7 > /sys/kernel/mm/lru_gen/enabled # 전체 활성화
echo 0 > /sys/kernel/mm/lru_gen/enabled # 비활성화 (기존 LRU로 복귀)
# 세대별 페이지 분포 확인
cat /sys/kernel/mm/lru_gen/memcg_path
# memcg nid gen anon_pages file_pages birth_time
MGLRU 성능 효과: Google의 벤치마크에서 MGLRU는 기존 LRU 대비 메모리 부족 워크로드에서 최대 40%의 성능 향상을 보였습니다. 특히 대규모 서버, Android, ChromeOS 등에서 메모리 압박 시 OOM 발생률이 감소하고, swap thrashing으로 인한 성능 저하가 크게 줄어듭니다.
Swap 모니터링과 디버깅
# === /proc/meminfo — 스왑 관련 항목 ===
cat /proc/meminfo | grep -i swap
# SwapCached: 10240 kB ← Swap Cache 크기 (RAM에 캐시된 스왑 페이지)
# SwapTotal: 8388604 kB ← 전체 스왑 공간
# SwapFree: 7340032 kB ← 미사용 스왑 공간
# === /proc/vmstat — 스왑 I/O 통계 ===
cat /proc/vmstat | grep -E "pswp|swap"
# pswpin 123456 ← 스왑 인 된 총 페이지 수 (누적)
# pswpout 234567 ← 스왑 아웃 된 총 페이지 수 (누적)
# === vmstat 명령으로 실시간 스왑 활동 모니터링 ===
vmstat 1
# procs ---memory--- ---swap-- -----io---- ...
# r b swpd free si so bi bo ...
# 1 0 10240 65432 0 0 12 8 ...
# swpd: 사용 중인 스왑 (KB)
# si: 초당 스왑 인 (KB/s) — 높으면 스왑 thrashing 의심
# so: 초당 스왑 아웃 (KB/s)
# === 프로세스별 스왑 사용량 ===
cat /proc/<pid>/status | grep -i swap
# VmSwap: 1024 kB ← 해당 프로세스의 스왑 사용량
# 스왑 사용량이 큰 프로세스 상위 10개
for f in /proc/[0-9]*/status; do
awk '/^(Name|VmSwap)/{printf "%s ", $2}' "$f" 2>/dev/null
echo
done | sort -k2 -n -r | head -10
# === /proc/<pid>/smaps — 상세 VMA별 스왑 정보 ===
cat /proc/<pid>/smaps | grep -A 20 "heap" | grep Swap
# Swap: 1024 kB ← 해당 VMA의 스왑 사용량
# SwapPss: 512 kB ← PSS 비례 스왑 (공유 시 분할)
# === ftrace로 스왑 이벤트 추적 ===
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_lru_shrink_inactive/enable
echo 1 > /sys/kernel/debug/tracing/events/swap/swap_readpage/enable
echo 1 > /sys/kernel/debug/tracing/events/swap/swap_writepage/enable
cat /sys/kernel/debug/tracing/trace_pipe
Swap Thrashing 감지: vmstat의 si/so 값이 지속적으로 높으면(수 MB/s 이상) 스왑 thrashing 상태입니다. 이는 물리 메모리가 워크로드에 비해 크게 부족하다는 신호이며, 시스템 전체 성능이 급격히 저하됩니다. 해결 방법: 메모리 증설, 불필요한 프로세스 종료, vm.swappiness 조정, zswap/zram 도입, 또는 cgroup memory.high로 throttling 적용.
Swap 관련 커널 설정 요약
| 설정 | 기본값 | 설명 |
|---|---|---|
CONFIG_SWAP |
y | 스왑 서브시스템 활성화. 비활성 시 익명 페이지 회수 불가 |
CONFIG_SWAP_STATS |
y | 스왑 통계 수집 (/proc/vmstat의 pswpin/pswpout) |
CONFIG_ZSWAP |
y | zswap 압축 스왑 캐시 활성화 |
CONFIG_ZRAM |
m | zram 압축 블록 디바이스 (보통 모듈) |
CONFIG_LRU_GEN |
y (6.1+) | MGLRU 활성화 — 페이지 회수 정확도 향상 |
vm.swappiness |
60 | anon vs file 회수 비율 (0~200, cgroup v2) |
vm.page-cluster |
3 | swap readahead 크기: 2^N 페이지 |
vm.min_free_kbytes |
자동 | MIN watermark 기준 — 스왑/회수 트리거에 간접 영향 |
vm.watermark_boost_factor |
15000 | 단편화 방지 워터마크 부스트 팩터 |
MAX_SWAPFILES |
32 | 동시에 활성화 가능한 최대 스왑 영역 수 |
zswap과 zram
zswap과 zram은 압축을 활용하여 스왑 성능을 크게 개선하는 메커니즘입니다. 디스크 I/O를 줄이고 스왑 영역의 실질적 용량을 확장합니다.
zswap — 압축 스왑 캐시
zswap은 스왑 아웃될 페이지를 압축하여 RAM의 동적 풀에 캐시하는 커널 기능입니다. 실제 디스크/SSD 스왑 I/O가 발생하기 전에 RAM에서 압축 저장을 시도하므로, 디스크 I/O를 극적으로 줄입니다. zswap은 기존 스왑 영역 위에서 동작하는 write-back 캐시이며, 풀이 가득 차면 가장 오래된 페이지를 실제 스왑 영역으로 writeback합니다.
# === zswap 설정 ===
# 런타임 활성화
echo Y > /sys/module/zswap/parameters/enabled
# 압축 알고리즘 선택
echo lz4 > /sys/module/zswap/parameters/compressor
# 선택지: lzo (기본, 균형), lz4 (빠름), zstd (높은 압축률)
# 메모리 풀 할당자 선택
echo z3fold > /sys/module/zswap/parameters/zpool
# zbud: 2:1 압축 비율, 간단하고 예측 가능
# z3fold: 3:1 압축 비율, zbud보다 효율적 (권장)
# zsmalloc: 최고 압축 효율, 약간의 CPU 오버헤드
# 최대 풀 크기 (전체 RAM 대비 퍼센트)
echo 20 > /sys/module/zswap/parameters/max_pool_percent
# same-filled page 최적화 (0으로 채워진 페이지 특수 처리)
echo Y > /sys/module/zswap/parameters/same_filled_pages_enabled
# 커널 부트 파라미터 (권장 설정)
# zswap.enabled=1 zswap.compressor=lz4 zswap.zpool=z3fold zswap.max_pool_percent=25
# === zswap 상태 모니터링 ===
grep -r . /sys/kernel/debug/zswap/ 2>/dev/null
# pool_total_size: 압축 데이터가 차지하는 메모리 (바이트)
# stored_pages: 현재 저장된 페이지 수
# pool_limit_hit: 풀 크기 제한에 도달한 횟수
# reject_reclaim_fail: writeback 실패로 거절된 횟수
# reject_compress_poor: 압축 효율 낮아 거절된 횟수
# written_back_pages: 디스크로 writeback된 페이지 수
# same_filled_pages: same-filled로 최적화된 페이지 수
# 압축 비율 계산
# 원본 크기 = stored_pages × 4096
# 압축 크기 = pool_total_size
# 압축 비율 = 원본 / 압축
zswap 압축 알고리즘 선택 가이드: lz4는 압축/해제 속도가 가장 빨라 CPU 오버헤드가 적으며 대부분의 환경에서 권장됩니다. zstd는 압축률이 높아 메모리 절약이 최우선인 서버에 적합하지만 CPU 사용량이 증가합니다. lzo는 기본값으로 lz4와 유사한 성능을 보입니다. Android에서는 lz4가 표준입니다.
zram — 압축 RAM 블록 디바이스
zram은 RAM의 일부를 압축 블록 디바이스로 만들어 스왑 영역으로 사용하는 모듈입니다. zswap과 달리 독립적인 스왑 디바이스로 동작하며, 실제 디스크 스왑 영역이 없어도 사용할 수 있습니다. 디스크 없는 임베디드 시스템이나 SSD 수명을 보호하려는 환경에서 유용합니다.
# === zram 설정 ===
# 모듈 로드 (디바이스 수 지정)
modprobe zram num_devices=2
# 압축 알고리즘 설정 (디바이스 생성 전에 설정)
echo lz4 > /sys/block/zram0/comp_algorithm
# 지원 알고리즘 확인:
cat /sys/block/zram0/comp_algorithm
# lzo lzo-rle lz4 [lz4hc] zstd (대괄호=현재 선택)
# 디스크 크기 설정 (압축 전 논리적 크기)
echo 4G > /sys/block/zram0/disksize
# 메모리 사용량 제한 (선택사항)
echo 1G > /sys/block/zram0/mem_limit # 실제 RAM 사용 상한
# 스왑으로 활성화
mkswap /dev/zram0
swapon -p 100 /dev/zram0 # 높은 우선순위 (디스크보다 먼저 사용)
# === zram 상태 모니터링 ===
cat /sys/block/zram0/mm_stat
# orig_data_size compr_data_size mem_used_total mem_limit ...
# 4096000000 1024000000 1073741824 1073741824 ...
# ↑ 원본 크기 ↑ 압축 크기 ↑ 실제 메모리 ↑ 메모리 제한
zramctl # zram 디바이스 상태 요약
# NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 lz4 4G 1.2G 320M 340M 4 [SWAP]
# === zram 비활성화 및 리셋 ===
swapoff /dev/zram0
echo 1 > /sys/block/zram0/reset # 디바이스 초기화
zswap vs zram 비교
| 특성 | zswap | zram |
|---|---|---|
| 동작 방식 | 기존 스왑 영역의 write-back 캐시 | 독립적인 스왑 블록 디바이스 |
| 디스크 스왑 필요 | 필수 (디스크 스왑 위에서 동작) | 불필요 (RAM만으로 동작 가능) |
| 풀 가득 참 시 | 디스크 스왑으로 writeback | 스왑 공간 부족 처리 (할당 실패) |
| 메모리 풀 | zbud/z3fold/zsmalloc (동적) | 자체 메모리 할당자 (zsmalloc) |
| 투명성 | 완전 투명 (기존 스왑 앞단에 삽입) | 별도 스왑 디바이스로 명시적 설정 |
| 주요 용도 | 디스크 스왑 I/O 감소, 서버 | 디스크 없는 시스템, SSD 보호, Android |
| 병용 가능 | 가능하지만 비권장 — 이중 압축 오버헤드 발생. 보통 하나만 선택 | |
권장 구성: SSD 기반 서버에서는 zswap + SSD 스왑 조합이 효과적입니다 (디스크 I/O 감소 + writeback 안전망). 디스크 없는 임베디드/IoT에서는 zram 단독이 유일한 선택입니다. 데스크톱/Android에서는 zram이 일반적입니다. 대규모 서버 환경에서 메모리 절약이 최우선이면 zswap + zstd 조합을 고려하십시오.
압축 알고리즘 비교
| 알고리즘 | 압축률 | 속도 | CPU 사용 | 권장 환경 |
|---|---|---|---|---|
lz4 |
2.0× | 매우 빠름 | 낮음 | 일반 서버/데스크톱 — CPU 비용 최소화 |
zstd |
2.5× | 빠름 | 중간 | 메모리 절약 우선, 여유 CPU 있을 때 |
lzo |
1.8× | 빠름 | 낮음 | 레거시 커널 (lz4 없을 때) |
lz4hc |
2.3× | 느림 | 높음 | 압축률 우선, CPU 여유 충분한 경우 |
lzo-rle |
1.9× | 빠름 | 낮음 | 희소(sparse) 페이지 많은 환경 |
권장: 대부분의 경우 lz4가 최선입니다. 메모리 절약이 최우선이고 CPU 여유가 있다면 zstd를 고려하세요. ARM/저전력 장치에서는 lzo-rle도 좋은 선택입니다.
성능 벤치마크
| 시나리오 | 일반 스왑 (SSD) | Zswap (lz4) | Zram (lz4) |
|---|---|---|---|
| 읽기 지연 (µs) | 500 | 50 | 30 |
| 쓰기 지연 (µs) | 1,000 | 80 | 60 |
| 처리량 (MB/s) | 500 | 3,000 | 5,000 |
| 메모리 절약 | 없음 | 15~20% (RAM의 20~25%로 제한) | 40~60% (전체 RAM 활용 가능) |
| CPU 오버헤드 | 없음 | 낮음 (~1% 단일 코어) | 낮음 (~1% 단일 코어) |
최적 설정 예시
# === 8GB RAM 서버 — Zswap + SSD 스왑 ===
# /etc/default/grub
GRUB_CMDLINE_LINUX="zswap.enabled=1 zswap.compressor=lz4 zswap.max_pool_percent=25 zswap.zpool=z3fold"
# swappiness (높여도 OK — RAM 내 압축이 먼저)
echo 80 > /proc/sys/vm/swappiness
# === 4GB RAM 임베디드/데스크톱 — Zram 단독 ===
modprobe zram
echo lz4 > /sys/block/zram0/comp_algorithm
echo 2G > /sys/block/zram0/disksize # RAM의 50%
mkswap /dev/zram0
swapon /dev/zram0 -p 10
# swappiness 높여서 Zram 적극 활용
echo 100 > /proc/sys/vm/swappiness
# === systemd-zram-setup (Fedora/Ubuntu 22.04+) ===
# /etc/systemd/zram-generator.conf
# [zram0]
# zram-size = ram / 2
# compression-algorithm = lz4
모니터링
# === Zswap 모니터링 ===
grep -r "" /sys/kernel/debug/zswap/
# pool_total_size 524288000 # 풀 사용 (Bytes)
# stored_pages 128000 # 압축 저장된 페이지 수
# written_back_pages 1024 # 디스크로 writeback된 페이지
# reject_compress_poor 512 # 압축률 불량으로 거부된 페이지
# duplicate_entry 0 # 중복 엔트리
# === Zram 모니터링 ===
zramctl
# NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 lz4 4G 1.2G 400M 450M 4 [SWAP]
cat /sys/block/zram0/mm_stat
# orig_data_size compr_data_size mem_used_total ...
# 1073741824 357564928 367001600
# 압축률 = orig / compr = 3.0x
# /proc/meminfo 관련 항목
grep -E "Swap|Zswap" /proc/meminfo
# SwapTotal: 4194304 kB
# SwapFree: 3145728 kB
# SwapCached: 8192 kB
알려진 문제
- CPU 오버헤드: 압축/해제는 CPU 사이클을 소모합니다. CPU가 이미 포화 상태라면 지연이 더 커질 수 있습니다.
lz4선택 및 CPU 사용률 모니터링으로 완화 가능합니다. - 비압축성 데이터: 이미 압축된 데이터(JPEG·MP4·암호화 파일 등)는 압축률이 1.0배 미만으로,
reject_compress_poor카운터가 증가하며 곧장 디스크 스왑으로 넘어갑니다. - Zswap 풀 고갈:
max_pool_percent에 도달하면 기존 항목을 디스크 스왑으로 writeback합니다. SSD가 느리다면 일시적으로 성능이 떨어질 수 있습니다. - Zram + Zswap 병용: 이중 압축 오버헤드가 발생하므로 비권장입니다. 하나만 선택하십시오.
- 메모리 부족 시 Zram OOM: Zram만 사용 중이고 압축 풀이 가득 찬 경우 백업 스왑이 없어 OOM Killer가 동작할 수 있습니다. 중요 프로세스에는
oom_score_adj = -1000을 설정하십시오.
Swap 튜닝 플레이북
Swap 성능 문제는 저장장치 속도, 압축 정책, 워크로드 working set 크기가 동시에 영향을 줍니다. 단순 swappiness 변경보다 먼저 병목 위치를 분리하세요.
| 상황 | 우선 점검 | 권장 조치 |
|---|---|---|
| 응답 지연 급증 | vmstat si/so, iowait | zswap/zram 적용, hot set 보호 |
| CPU 과부하 | 압축 알고리즘 비용 | lz4/zstd 압축 정책 재검토 |
| 지속적 swap-out | 메모리 과할당 | 메모리 상한 조정, workload 분리 |
# swap 상태 핵심 지표
free -h
vmstat 1 10
grep -E "Swap|Zswap" /proc/meminfo
cat /sys/module/zswap/parameters/enabled 2>/dev/null || true
Swap 클러스터 할당 심화
실제 swap I/O 성능은 빈 슬롯을 "어떻게" 찾고 묶어서 쓰는지에 크게 좌우됩니다. 최신 커널은 단일 비트 탐색보다 클러스터 단위 할당과 Per-CPU 힌트를 활용해 락 경합과 탐색 비용을 줄입니다.
/* mm/swapfile.c - 클러스터 기반 할당 개념 (간략화) */
#define SWAPFILE_CLUSTER 256 /* 256 페이지 단위 (아키텍처/설정에 따라 다를 수 있음) */
struct swap_cluster_info {
unsigned int flags;
unsigned int count; /* 사용 중 슬롯 수 */
};
struct percpu_cluster {
unsigned int index; /* 현재 CPU가 소비 중인 클러스터 */
unsigned int next; /* 다음 슬롯 힌트 */
};
/* 핵심 흐름: 클러스터 우선, 실패 시 글로벌 탐색 */
swp_entry_t get_swap_page(struct folio *folio)
{
swp_entry_t entry;
/* 1) 현재 CPU 클러스터에서 빠른 할당 시도 */
entry = scan_swap_map_try_ssd_cluster(si, cpu);
if (entry.val)
return entry;
/* 2) free_clusters 리스트에서 새 클러스터 획득 */
entry = alloc_swap_scan_cluster(si);
if (entry.val)
return entry;
/* 3) 최후: lowest_bit~highest_bit 선형 탐색 */
return scan_swap_map_slots(si);
}
cgroup v2 Swap 제어 심화
현대 운영 환경에서 swap 정책은 전역 `vm.swappiness`보다 cgroup 단위 제어가 더 중요합니다. 컨테이너별 상한과 보호 정책으로 swap thrashing 전파를 차단할 수 있습니다.
| 파일 | 의미 | 실전 활용 |
|---|---|---|
memory.swap.max | 해당 cgroup swap 사용 상한 | 0이면 swap 금지, 장애 전파 차단 |
memory.high | 소프트 상한 (초과 시 reclaim/스로틀) | OOM 전에 완충 구간 형성 |
memory.max | 하드 상한 | 초과 시 강제 회수/킬 |
memory.events | high/max/oom 이벤트 카운터 | 자동 스케일링/알람 트리거 |
memory.pressure | PSI 지표 | stall 기반 조기 경보 |
# cgroup v2 기준 예시
CG=/sys/fs/cgroup/workloads/api
mkdir -p $CG
# 메모리 8GB, swap 2GB 상한
echo $((8*1024*1024*1024)) > $CG/memory.max
echo $((2*1024*1024*1024)) > $CG/memory.swap.max
# high를 먼저 설정해 스로틀+완충
echo $((7*1024*1024*1024)) > $CG/memory.high
# 이벤트/압력 모니터링
watch -n 1 "cat $CG/memory.events; echo; cat $CG/memory.pressure"
memory.swap.max=0, 배치/비동기 워크로드는 제한된 swap 허용이 안정적입니다. 같은 호스트에서 두 유형을 분리하지 않으면 swap 지연이 서비스 전반으로 전파됩니다.
Swap 지연시간 분해
swap fault 한 번의 비용은 단일 값이 아닙니다. lock 대기, 압축 해제, 블록 I/O, 페이지 테이블 복원 비용이 합산됩니다. 병목 구간을 분리 측정해야 튜닝이 정확해집니다.
swap-subsystem과 zswap 페이지 중복 검토
두 문서를 비교하면 아래 주제가 중복됩니다. 중복 자체는 탐색 편의에는 유리하지만, 심화 관점에서는 역할 분리가 더 명확해야 합니다.
- 중복 영역 — zswap/zram 비교표, 압축 알고리즘 소개, 기본 모니터링 명령
- 이 페이지의 책임 — 전체 swap 정책, reclaim/swap out/in 경로, swappiness/cgroup/PSI 기반 운영
- zswap 페이지의 책임 — zswap 내부 구조(RB-tree, zpool), reject 원인, writeback/shrinker, 디버깅
실무에서는 먼저 이 페이지에서 시스템 정책을 고정하고, zswap 세부 튜닝은 zswap 심화 페이지에서 별도로 최적화하는 순서가 가장 안정적입니다.
| 주제 | 이 페이지 (swap-subsystem) | zswap 페이지 | 중복 처리 원칙 |
|---|---|---|---|
| 회수 정책 | 주관: LRU/kswapd/direct reclaim, swappiness, PSI | 참조만 유지 | 정책 결정은 이 페이지에서 1차 고정 |
| zswap 내부 | 개요만 유지 | 주관: zpool, reject, writeback, shrinker | 내부 구조 설명은 zswap으로 단일화 |
| 튜닝 순서 | 주관: 시스템 한계선과 보호 규칙 | 주관: 파라미터 미세 조정 | 정책 이후 파라미터 조정 순서 고정 |
| 모니터링 지표 | PSI, vmstat, cgroup 이벤트 | zswap debugfs, reject/writeback 지표 | 대시보드 계층 분리(시스템/기능) |
| 장애 대응 | 주관: 시스템 스로틀/격리 | 주관: zswap 병목 원인 분기 | 공통 증상이라도 진단 관문을 분리 |
관련 문서
Swapping 서브시스템과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.