zswap (압축 스왑 캐시) 심화
zswap은 리눅스 커널의 압축 스왑 캐시 메커니즘으로, 스왑 아웃되는 익명 페이지를 RAM 내에서 압축하여 실제 디스크 I/O를 크게 줄여줍니다. frontswap 인터페이스를 통한 페이지 가로채기, zpool 백엔드의 메모리 할당, 다양한 압축 알고리즘(lzo, lz4, zstd) 선택, writeback을 통한 배킹 스왑 연동, same-filled pages 최적화까지 전 영역을 상세히 다룹니다.
핵심 요약
- zswap -- 스왑 아웃 대상 페이지를 RAM 내에서 압축 저장하는 캐시 계층
- frontswap -- 스왑 쓰기/읽기를 가로채어 zswap에 전달하는 커널 인터페이스
- zpool -- 압축된 데이터를 저장하는 메모리 풀 추상화 (zbud, z3fold, zsmalloc)
- writeback -- zswap 캐시가 가득 찰 때 LRU 순서로 배킹 스왑 디바이스에 기록
- same-filled pages -- 동일 값으로 채워진 페이지를 압축 없이 값만 저장하는 최적화
- accept_threshold -- zswap 풀이 최대 크기의 일정 비율에 도달하면 신규 저장을 거부하는 임계치
단계별 이해
- 메모리 압박 발생
kswapd 또는 직접 회수 경로에서 익명 페이지를 스왑 아웃하려 합니다. - frontswap 가로채기
swap_writepage()가 호출되면, frontswap 인터페이스가 이를 가로채어 zswap_store()를 호출합니다. - 페이지 압축
선택된 알고리즘(lzo/lz4/zstd)으로 4KB 페이지를 압축합니다. 압축 결과가 원본보다 크면 거부합니다. - zpool 저장
압축된 데이터를 zpool 백엔드(zbud/z3fold/zsmalloc)에 할당받은 메모리에 저장합니다. - 읽기 시 복원
해당 페이지에 접근하면 zswap_load()가 호출되어 압축 해제 후 원본 페이지를 복원합니다.
개요
zswap이란?
zswap은 리눅스 커널 3.11(2013년 9월)에 도입된 압축 스왑 캐시(compressed swap cache)입니다. 커널의 메모리 회수 경로에서 익명 페이지가 스왑 아웃될 때, 실제 디스크에 기록하기 전에 RAM 내에서 페이지를 압축하여 보관합니다. 이를 통해 다음과 같은 이점을 제공합니다:
- 디스크 I/O 감소: 대부분의 스왑 페이지를 RAM에 압축 저장하므로 느린 디스크 접근을 크게 줄임
- 응답 지연 개선: 압축/해제는 수 마이크로초, 디스크 I/O는 수 밀리초 -- 수백 배 차이
- SSD 수명 연장: 불필요한 쓰기를 줄여 NAND Flash 마모 감소
- 실질적 메모리 확장: 압축률만큼 더 많은 페이지를 RAM에 보관 가능
zswap의 위치: 메모리 회수 경로
zswap은 frontswap 인터페이스를 통해 커널의 스왑 경로에 투명하게 삽입됩니다.
유저스페이스 프로세스나 스왑 디바이스 설정을 변경할 필요 없이,
커널 파라미터 하나로 활성화할 수 있습니다.
6.x 커널부터는 기본 빌드에서 zswap이 활성화되어 배포되는 경우가 증가하고 있습니다.
# 커널 6.x 기본 부팅 시 zswap 상태 확인
$ cat /sys/module/zswap/parameters/enabled
Y
$ cat /sys/module/zswap/parameters/compressor
lz4
$ cat /sys/module/zswap/parameters/zpool
zsmalloc
CONFIG_ZSWAP_DEFAULT_ON=y로 기본 활성화되는 추세이며,
6.8부터 기본 zpool이 zsmalloc으로, 기본 압축 알고리즘이 lz4로 변경되었습니다.
이전 커널에서는 기본이 zbud + lzo-rle이었습니다.
zswap 아키텍처
zswap의 전체 아키텍처는 세 가지 핵심 계층으로 구성됩니다: frontswap 인터페이스, zswap 코어, 그리고 배킹 스왑 디바이스입니다.
frontswap 인터페이스 역할
frontswap은 커널의 스왑 서브시스템과 백엔드 구현 사이의 추상화 계층입니다.
swap_writepage()가 호출될 때 frontswap이 먼저 개입하여,
등록된 백엔드(zswap)에 페이지 저장을 시도합니다.
성공하면 실제 디스크 I/O가 발생하지 않고, 실패하면 원래의 스왑 경로로 폴백합니다.
/* mm/frontswap.c - frontswap 저장 경로 (간략화) */
int __frontswap_store(struct page *page)
{
int type = swp_type(page_private(page));
pgoff_t offset = swp_offset(page_private(page));
struct frontswap_ops *ops;
list_for_each_entry(ops, &frontswap_ops, list) {
if (ops->store(type, offset, page) == 0)
return 0; /* zswap 저장 성공 → 디스크 I/O 불필요 */
}
return -1; /* 실패 → 일반 스왑 경로로 폴백 */
}
코드 설명
- 3-4행 스왑 엔트리에서 스왑 타입(어느 스왑 영역인지)과 오프셋(해당 영역 내 위치)을 추출합니다.
-
7행
등록된 모든 frontswap 백엔드를 순회하며 저장을 시도합니다. zswap이 등록되어 있으면
zswap_frontswap_store()가 호출됩니다. - 8-9행 백엔드가 0을 반환하면 저장 성공입니다. 이 경우 실제 디스크에 쓰기를 하지 않습니다.
- 11행 모든 백엔드가 실패하면 -1을 반환하여, 커널이 원래의 스왑 디바이스에 기록하도록 합니다.
swap_writepage() 내부에서 zswap_store()를 직접 호출하는 구조로 변경되어
간접 호출 오버헤드가 제거되었습니다.
zswap vs zram vs 전통적 swap 비교
리눅스 커널에는 스왑 관련 세 가지 주요 메커니즘이 있습니다. 각각의 동작 방식과 장단점을 비교합니다.
| 특성 | 전통적 swap | zswap | zram |
|---|---|---|---|
| 동작 방식 | 디스크 블록 직접 I/O | RAM 압축 캐시 + writeback | RAM 압축 블록 디바이스 |
| 디스크 필요 | 필수 | 필수 (배킹 스왑) | 불필요 |
| RAM 소비 | 없음 | 설정 가능 (max_pool_percent) | 설정 가능 (disksize) |
| I/O 발생 | 항상 | writeback 시에만 | 없음 |
| 최대 용량 | 디스크 크기 | RAM + 디스크 | RAM 크기 |
| 적합한 환경 | 대용량 스왑 필요 | 서버, 범용 | 임베디드, 모바일 |
| 커널 설정 | 기본 지원 | CONFIG_ZSWAP | CONFIG_ZRAM |
zswap 내부 구현
zswap의 핵심 자료구조를 이해하면 전체 동작을 파악할 수 있습니다.
주요 구조체는 zswap_entry, zswap_tree, zswap_pool입니다.
/* mm/zswap.c - 주요 자료구조 (Linux 6.x 기준) */
/* 개별 압축 페이지를 나타내는 엔트리 */
struct zswap_entry {
struct rb_node rbnode; /* RB-tree 노드 */
swp_entry_t swpentry; /* swap 엔트리 (type + offset) */
int length; /* 압축된 데이터 크기 */
struct zswap_pool *pool; /* 사용 중인 풀 */
union {
unsigned long handle; /* zpool 할당 핸들 */
unsigned long value; /* same-filled 값 */
};
struct obj_cgroup *objcg; /* 메모리 cgroup 추적 */
struct list_head lru; /* LRU 리스트 (writeback용) */
};
/* 스왑 타입별 RB-tree (인덱스) */
struct zswap_tree {
struct rb_root rbroot; /* RB-tree 루트 */
spinlock_t lock; /* 트리 접근 동기화 */
};
/* 압축 풀: 압축기 + zpool 조합 */
struct zswap_pool {
struct zpool *zpool; /* 메모리 할당 백엔드 */
struct crypto_acomp *acomp; /* 비동기 압축 알고리즘 */
struct kref kref; /* 참조 카운팅 */
struct list_head list; /* 전역 풀 리스트 */
struct list_head lru_list; /* LRU 순서 리스트 */
spinlock_t lru_lock; /* LRU 리스트 잠금 */
struct hlist_node node; /* shrink 해시 테이블 노드 */
atomic_t nr_stored; /* 저장된 엔트리 수 */
};
코드 설명
-
4-15행
zswap_entry는 압축된 하나의 페이지를 나타냅니다.handle과value가 union으로 묶여 있는 이유는, same-filled 페이지는 zpool 할당 없이 값만 저장하기 때문입니다. -
18-21행
zswap_tree는 스왑 타입(스왑 파티션/파일)별로 하나씩 존재하며, swap offset을 키로 하는 RB-tree입니다. -
24-33행
zswap_pool은 압축 알고리즘과 zpool 백엔드의 조합입니다. 런타임에 압축기나 zpool을 변경하면 새 풀이 생성되고, 기존 풀은 참조 카운트가 0이 될 때까지 유지됩니다.
압축/해제 흐름
zswap의 핵심 경로인 저장(store)과 로드(load)의 상세 흐름을 살펴봅니다.
/* mm/zswap.c - zswap_store() 핵심 경로 (간략화, Linux 6.x) */
bool zswap_store(struct folio *folio)
{
struct zswap_entry *entry;
struct zswap_pool *pool;
unsigned int dlen = PAGE_SIZE;
u8 *dst;
/* 1. zswap 활성화 및 threshold 확인 */
if (!zswap_enabled || !zswap_can_accept())
return false;
/* 2. same-filled page 최적화 검사 */
if (zswap_is_page_same_filled(folio, &value)) {
entry->length = 0;
entry->value = value;
goto insert;
}
/* 3. 압축 수행 */
pool = zswap_pool_current_get();
crypto_acomp_compress(acomp_ctx->req);
dlen = acomp_ctx->req->dlen;
/* 4. 압축 크기가 원본 이상이면 거부 */
if (dlen >= PAGE_SIZE) {
zswap_reject_compress_poor++;
goto put_pool;
}
/* 5. zpool에 메모리 할당 및 복사 */
zpool_malloc(pool->zpool, dlen, &handle);
dst = zpool_map_handle(pool->zpool, handle, ZPOOL_MM_WO);
memcpy(dst, acomp_ctx->buffer, dlen);
zpool_unmap_handle(pool->zpool, handle);
/* 6. entry에 정보 기록 */
entry->handle = handle;
entry->length = dlen;
entry->pool = pool;
insert:
/* 7. RB-tree 삽입 + LRU 추가 */
zswap_tree_insert(&tree->rbroot, entry);
list_add_tail(&entry->lru, &pool->lru_list);
atomic_inc(&zswap_stored_pages);
return true;
}
로드(복원) 경로
/* mm/zswap.c - zswap_load() 핵심 경로 (간략화) */
bool zswap_load(struct folio *folio)
{
struct zswap_entry *entry;
/* 1. RB-tree에서 entry 검색 */
entry = zswap_tree_search(tree, offset);
if (!entry)
return false;
/* 2. same-filled page인 경우 값으로 채우기 */
if (entry->length == 0) {
zswap_fill_page(dst, entry->value);
goto freeentry;
}
/* 3. zpool에서 매핑 후 압축 해제 */
src = zpool_map_handle(entry->pool->zpool, entry->handle, ZPOOL_MM_RO);
crypto_acomp_decompress(acomp_ctx->req);
zpool_unmap_handle(entry->pool->zpool, entry->handle);
freeentry:
/* 4. entry 제거 및 메모리 해제 */
zswap_tree_delete(tree, entry);
zswap_entry_free(entry);
atomic_dec(&zswap_stored_pages);
return true;
}
코드 설명
- 7행 swap offset을 키로 RB-tree를 검색합니다. O(log n) 시간 복잡도입니다.
- 12-15행 length가 0이면 same-filled 페이지입니다. zpool 접근 없이 저장된 값으로 페이지를 채웁니다.
- 18-20행 zpool에서 핸들을 매핑하여 압축 데이터에 접근하고, crypto API로 압축을 해제합니다.
- 24-26행 로드 후에는 entry를 RB-tree에서 제거하고 zpool 메모리를 해제합니다. 페이지가 다시 활성화되었으므로 zswap 캐시에 남아있을 필요가 없습니다.
zpool 백엔드
zpool은 가변 크기의 압축 객체를 저장하는 메모리 할당자의 추상화 계층입니다. 세 가지 백엔드가 존재하며, 각각 메모리 효율성과 성능 특성이 다릅니다.
| 특성 | zbud | z3fold | zsmalloc |
|---|---|---|---|
| 페이지당 최대 객체 | 2 | 3 | 크기 클래스별 가변 |
| 이론 최대 밀도 | ~50% | ~75% | ~98% |
| writeback 지원 | 가능 | 가능 | 6.2부터 가능 |
| 메모리 오버헤드 | 높음 | 중간 | 낮음 |
| 구현 복잡도 | 낮음 | 중간 | 높음 |
| 커널 기본값 (6.8+) | - | - | 기본 |
/* mm/zpool.c - zpool 추상화 인터페이스 */
struct zpool_ops {
int (*malloc)(struct zpool *pool, size_t size,
gfp_t gfp, unsigned long *handle);
void (*free)(struct zpool *pool, unsigned long handle);
void *(*map)(struct zpool *pool, unsigned long handle,
enum zpool_mapmode mm);
void (*unmap)(struct zpool *pool, unsigned long handle);
u64 (*total_size)(struct zpool *pool);
};
압축 알고리즘
zswap은 커널의 crypto API를 통해 다양한 압축 알고리즘을 지원합니다. 각 알고리즘은 속도와 압축률의 트레이드오프가 다르므로, 워크로드에 맞게 선택해야 합니다.
| 알고리즘 | 압축 속도 | 해제 속도 | 압축률 | CPU 사용량 | 적합한 환경 |
|---|---|---|---|---|---|
| lzo / lzo-rle | 매우 빠름 | 매우 빠름 | 보통 (~2:1) | 낮음 | 범용 (이전 기본값) |
| lz4 | 매우 빠름 | 극히 빠름 | 보통 (~2:1) | 매우 낮음 | 지연 민감 (현재 기본값) |
| zstd | 빠름 | 빠름 | 높음 (~3:1) | 중간 | 메모리 절약 우선 |
| deflate | 느림 | 보통 | 높음 (~3.5:1) | 높음 | 극한 메모리 절약 |
| 842 | 빠름 (HW) | 빠름 (HW) | 높음 | 낮음 (HW) | Power8/9 하드웨어 |
# 런타임에 압축 알고리즘 변경
$ echo lz4 > /sys/module/zswap/parameters/compressor
$ cat /sys/module/zswap/parameters/compressor
lz4
# 사용 가능한 알고리즘 확인
$ cat /proc/crypto | grep -A1 "name.*lz4\|name.*lzo\|name.*zstd"
name : lz4
driver : lz4-generic
name : lzo-rle
driver : lzo-rle-generic
name : zstd
driver : zstd-generic
- 일반 서버/데스크톱:
lz4(낮은 지연, 충분한 압축률) - 메모리 부족 환경:
zstd(높은 압축률, 약간의 CPU 비용) - 실시간/지연 민감:
lz4(해제 속도 최우선) - 레거시 호환:
lzo-rle(오래된 커널 기본값)
Writeback 메커니즘
zswap의 RAM 풀이 가득 차거나 메모리 압박이 심해지면, LRU 순서로 오래된 압축 페이지를 배킹 스왑 디바이스로 기록(writeback)하여 RAM 공간을 확보합니다. 이 과정은 shrink 콜백을 통해 수행됩니다.
/* mm/zswap.c - writeback 핵심 경로 (간략화) */
static int zswap_writeback_entry(struct zswap_entry *entry,
struct zswap_tree *tree)
{
struct page *page;
u8 *src;
/* 1. 페이지 할당 */
page = alloc_page(GFP_KERNEL);
/* 2. 압축 해제 */
src = zpool_map_handle(entry->pool->zpool, entry->handle, ZPOOL_MM_RO);
crypto_acomp_decompress(acomp_ctx->req);
zpool_unmap_handle(entry->pool->zpool, entry->handle);
/* 3. 배킹 스왑에 기록 */
__swap_writepage(page, &wbc);
/* 4. entry 해제 */
zswap_tree_delete(tree, entry);
zswap_entry_free(entry);
put_page(page);
return 0;
}
non_same_filled_pages_enabled=N 또는 zpool이 writeback 미지원),
zswap 풀이 가득 찰 때 새로운 페이지를 거부(reject)하게 되어
결과적으로 모든 신규 스왑 I/O가 직접 디스크로 향합니다.
동적 메모리 제한과 accept_threshold
zswap은 두 가지 메커니즘으로 RAM 사용량을 제어합니다:
max_pool_percent로 최대 크기를 설정하고,
accept_threshold_percent로 조기 거부 임계치를 설정합니다.
/* mm/zswap.c - accept threshold 로직 */
static bool zswap_can_accept(void)
{
unsigned long cur_pages = zswap_pool_total_size() >> PAGE_SHIFT;
unsigned long max_pages = zswap_max_pages();
unsigned long threshold;
/* max_pool_percent에 의한 절대 제한 */
if (cur_pages >= max_pages) {
zswap_reject_alloc_fail++;
return false;
}
/* accept_threshold에 의한 조기 거부 */
threshold = max_pages * zswap_accept_thr_percent / 100;
if (cur_pages > threshold) {
zswap_reject_over_threshold++;
return false;
}
return true;
}
# 현재 설정 확인
$ cat /sys/module/zswap/parameters/max_pool_percent
20 # 전체 RAM의 20%까지 사용
$ cat /sys/module/zswap/parameters/accept_threshold_percent
90 # max_pool의 90%에 도달하면 거부 시작
# 예: 16GB RAM → max_pool = 3.2GB → threshold = 2.88GB
# zswap 풀이 2.88GB 이상이면 신규 저장 거부
# 동적으로 변경
$ echo 30 > /sys/module/zswap/parameters/max_pool_percent
$ echo 85 > /sys/module/zswap/parameters/accept_threshold_percent
Same-Filled Pages 최적화
커널 4.18에 도입된 same-filled pages 최적화는, 모든 바이트가 동일한 값으로 채워진 페이지를 감지하여 실제 압축 없이 해당 값(unsigned long 하나)만 저장합니다. 전형적인 워크로드에서 약 10-30%의 스왑 페이지가 0으로 채워져 있습니다.
/* mm/zswap.c - same-filled page 검사 */
static bool zswap_is_page_same_filled(struct folio *folio,
unsigned long *valuep)
{
unsigned long *page;
unsigned long val;
unsigned int pos;
page = kmap_local_folio(folio, 0);
val = page[0];
/* 페이지 전체가 동일 값인지 검사 */
for (pos = 1; pos < PAGE_SIZE / sizeof(*page); pos++) {
if (page[pos] != val) {
kunmap_local(page);
return false;
}
}
kunmap_local(page);
*valuep = val;
return true; /* 압축 없이 val만 저장 */
}
코드 설명
- 9-10행 페이지를 커널 주소 공간에 매핑하고 첫 번째 unsigned long 값을 읽습니다.
- 13-17행 페이지 전체를 unsigned long 단위로 순회하며, 첫 번째 값과 다른 값이 하나라도 있으면 false를 반환합니다.
-
20-21행
전체가 동일 값이면 해당 값을 반환합니다. zswap_entry의
value필드에 저장되며,length=0으로 표시됩니다.
# same-filled pages 최적화 활성/비활성
$ cat /sys/module/zswap/parameters/same_filled_pages_enabled
Y
# debugfs에서 same-filled 통계 확인
$ cat /sys/kernel/debug/zswap/same_filled_pages
42831
# 전체 저장 페이지 대비 비율 확인
$ cat /sys/kernel/debug/zswap/stored_pages
158742
# → same-filled 비율: 42831 / 158742 = 약 27%
커널 설정
Kconfig 옵션
# 핵심 설정
CONFIG_ZSWAP=y # zswap 기능 활성화
CONFIG_ZSWAP_DEFAULT_ON=y # 부팅 시 기본 활성화 (6.5+)
CONFIG_ZSWAP_COMPRESSOR_DEFAULT="lz4" # 기본 압축 알고리즘
CONFIG_ZSWAP_ZPOOL_DEFAULT="zsmalloc" # 기본 zpool 백엔드
# zpool 백엔드
CONFIG_ZBUD=y # zbud 할당자
CONFIG_Z3FOLD=y # z3fold 할당자
CONFIG_ZSMALLOC=y # zsmalloc 할당자
# 압축 알고리즘
CONFIG_CRYPTO_LZO=y # LZO 압축
CONFIG_CRYPTO_LZ4=y # LZ4 압축
CONFIG_CRYPTO_ZSTD=y # ZSTD 압축
CONFIG_CRYPTO_DEFLATE=y # Deflate 압축
# 의존성
CONFIG_SWAP=y # 스왑 지원 (필수)
CONFIG_CRYPTO=y # 암호화 프레임워크 (필수)
CONFIG_FRONTSWAP=y # frontswap (6.5 이전 필수)
부팅 파라미터
# GRUB 설정 예시 (/etc/default/grub)
GRUB_CMDLINE_LINUX="zswap.enabled=1 zswap.compressor=lz4 zswap.zpool=zsmalloc zswap.max_pool_percent=25"
# 또는 개별 설정
zswap.enabled=1 # 활성화 (0=비활성화)
zswap.compressor=lz4 # 압축 알고리즘
zswap.zpool=zsmalloc # zpool 백엔드
zswap.max_pool_percent=20 # 최대 풀 크기 (RAM %)
zswap.same_filled_pages_enabled=1 # same-filled 최적화
zswap.accept_threshold_percent=90 # accept threshold
sysfs 인터페이스
# 런타임 파라미터 경로
/sys/module/zswap/parameters/
enabled # Y/N - 활성화 상태
compressor # 압축 알고리즘 이름
zpool # zpool 백엔드 이름
max_pool_percent # 최대 풀 크기 (RAM %)
accept_threshold_percent # accept threshold (%)
same_filled_pages_enabled # same-filled 최적화
non_same_filled_pages_enabled # 일반 페이지 저장
exclusive_loads # 로드 후 entry 제거 (6.5+)
# 런타임 변경 예시
$ echo Y > /sys/module/zswap/parameters/enabled
$ echo zstd > /sys/module/zswap/parameters/compressor
$ echo 25 > /sys/module/zswap/parameters/max_pool_percent
성능 튜닝과 모니터링
zswap의 효과를 극대화하려면 워크로드에 맞는 파라미터 조정과 지속적인 모니터링이 필요합니다.
#!/bin/bash
# zswap 모니터링 스크립트
ZSWAP_DEBUG="/sys/kernel/debug/zswap"
ZSWAP_PARAM="/sys/module/zswap/parameters"
# 기본 상태
echo "=== zswap 설정 ==="
echo "활성화: $(cat $ZSWAP_PARAM/enabled)"
echo "압축기: $(cat $ZSWAP_PARAM/compressor)"
echo "zpool: $(cat $ZSWAP_PARAM/zpool)"
echo "최대풀: $(cat $ZSWAP_PARAM/max_pool_percent)%"
# 통계
if [ -d "$ZSWAP_DEBUG" ]; then
stored=$(cat $ZSWAP_DEBUG/stored_pages)
pool_bytes=$(cat $ZSWAP_DEBUG/pool_total_size)
same=$(cat $ZSWAP_DEBUG/same_filled_pages)
wb=$(cat $ZSWAP_DEBUG/written_back_pages)
echo ""
echo "=== zswap 통계 ==="
echo "저장 페이지: $stored"
echo "풀 크기: $((pool_bytes / 1024 / 1024)) MB"
echo "원본 크기: $((stored * 4096 / 1024 / 1024)) MB"
if [ "$pool_bytes" -gt 0 ]; then
ratio=$((stored * 4096 * 100 / pool_bytes))
echo "압축률: ${ratio}%"
fi
echo "same-filled: $same ($((same * 100 / (stored + 1)))%)"
echo "writeback: $wb"
echo ""
echo "=== 거부 카운터 ==="
for f in $ZSWAP_DEBUG/reject_*; do
echo "$(basename $f): $(cat $f)"
done
fi
성능 튜닝 가이드
| 시나리오 | 권장 설정 | 이유 |
|---|---|---|
| 범용 서버 (16GB+) | lz4 + zsmalloc + 20% | 낮은 지연, 충분한 캐시 |
| 메모리 부족 (4GB 이하) | zstd + zsmalloc + 30% | 높은 압축률로 메모리 절약 |
| SSD 수명 연장 | lz4 + zsmalloc + 35% | writeback 최소화 |
| 실시간 시스템 | lz4 + zbud + 15% | 예측 가능한 지연 |
| 데이터베이스 서버 | lz4 + zsmalloc + 25% | 빈번한 page-in 최적화 |
| 컨테이너 호스트 | zstd + zsmalloc + 30% | 다수 컨테이너 메모리 절약 |
zswap 디버깅
zswap이 기대만큼 동작하지 않을 때, 거부(reject) 원인을 분석하고 적절한 조치를 취하는 방법을 설명합니다.
reject 원인 분석
| 카운터 | 원인 | 조치 |
|---|---|---|
reject_compress_poor |
압축 결과가 원본보다 큼 (이미 압축된 데이터, 랜덤 데이터) | 정상 동작. 압축 불가 페이지는 직접 스왑 |
reject_alloc_fail |
zpool 메모리 할당 실패 (풀 크기 제한 도달) | max_pool_percent 증가 또는 writeback 활성화 |
reject_reclaim_fail |
shrink 중 메모리 회수 실패 | 배킹 스왑 디바이스 성능 확인 |
reject_kmemcache_fail |
zswap_entry 슬랩 할당 실패 | 시스템 전반 메모리 부족, OOM 상황 점검 |
# reject 카운터 실시간 모니터링
$ watch -n 1 'cat /sys/kernel/debug/zswap/reject_*'
# 특정 시간 동안 reject 증가율 확인
$ for i in 1 2 3 4 5; do
echo "--- $(date) ---"
cat /sys/kernel/debug/zswap/reject_compress_poor
cat /sys/kernel/debug/zswap/reject_alloc_fail
sleep 5
done
# ftrace로 zswap 함수 추적
$ echo 'zswap_store' > /sys/kernel/debug/tracing/set_ftrace_filter
$ echo 'zswap_load' >> /sys/kernel/debug/tracing/set_ftrace_filter
$ echo function > /sys/kernel/debug/tracing/current_tracer
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
$ cat /sys/kernel/debug/tracing/trace_pipe
일반적인 문제와 해결
CONFIG_ZSWAP=y가 커널에 빌드되었는지 확인:grep ZSWAP /boot/config-$(uname -r)- 부팅 파라미터에
zswap.enabled=1확인:cat /proc/cmdline - 압축 알고리즘 모듈이 로드되었는지 확인:
lsmod | grep lz4 - dmesg에서 zswap 초기화 메시지 확인:
dmesg | grep zswap
# zswap 초기화 성공 시 dmesg 출력 예시
$ dmesg | grep zswap
[ 0.892345] zswap: loaded using pool lz4/zsmalloc
[ 0.892356] zswap: default zswap pool created with: lz4, zsmalloc, 20%
# 압축 알고리즘 변경 실패 시
$ echo invalid_algo > /sys/module/zswap/parameters/compressor
$ dmesg | tail -1
[ 123.456789] zswap: compressor invalid_algo not available
실전 사용 사례
서버 환경
데이터베이스 서버나 웹 애플리케이션 서버에서 zswap은 메모리 압박 시 응답 시간을 안정적으로 유지하는 데 핵심적인 역할을 합니다.
# 프로덕션 서버 추천 설정 (64GB RAM)
zswap.enabled=1
zswap.compressor=lz4
zswap.zpool=zsmalloc
zswap.max_pool_percent=20 # 12.8GB까지 사용
zswap.accept_threshold_percent=90
vm.swappiness=60 # 적당한 스왑 경향
임베디드 / 모바일 환경
메모리가 제한된 임베디드 시스템에서는 zstd 압축과 높은 풀 비율을 조합하여 실질적인 가용 메모리를 극대화할 수 있습니다.
# 임베디드 시스템 설정 (2GB RAM)
zswap.enabled=1
zswap.compressor=zstd # 높은 압축률
zswap.zpool=zsmalloc
zswap.max_pool_percent=35 # 700MB까지 사용
zswap.accept_threshold_percent=85
vm.swappiness=80 # 적극적 스왑
컨테이너 환경
다수의 컨테이너가 동작하는 호스트에서는 cgroup 메모리 제한과 함께 zswap을 활용하면 개별 컨테이너의 메모리 초과를 효과적으로 완충할 수 있습니다. Linux 6.1부터 zswap의 cgroup 통합이 개선되어, 컨테이너별 압축 페이지 추적이 가능합니다.
Writeback과 블록 I/O 결합
zswap의 실제 효과는 압축률만으로 결정되지 않습니다. writeback이 시작된 뒤 블록 레이어에서 어떤 지연이 발생하는지까지 봐야 전체 tail latency를 줄일 수 있습니다.
/* zswap writeback 경로 개념 (간략화) */
static unsigned long zswap_writeback_entry(struct zswap_entry *entry)
{
struct folio *folio;
/* 1) zpool에서 압축 데이터 읽기 + 4KB 복원 */
folio = zswap_decompress(entry);
if (!folio)
return 0;
/* 2) 실제 스왑 디바이스로 기록 (blk-mq 경유) */
if (swap_writepage(&folio->page, NULL))
return 0;
/* 3) 성공 시 zswap 엔트리 제거 */
zswap_invalidate_entry(entry);
return 1;
}
Reject 카운터 기반 문제 분기
reject 카운터는 단순한 실패 집계가 아니라, 병목 위치를 빠르게 좁히는 신호입니다. 카운터별 해석을 고정하면 운영 대응 속도가 크게 빨라집니다.
| 카운터 | 주요 의미 | 우선 조치 |
|---|---|---|
reject_compress_poor | 압축 이득 부족 | 정상 가능성 높음, 데이터 성격 확인 |
reject_alloc_fail | zpool 할당 실패 | 풀 제한/메모리 압박/단편화 점검 |
reject_reclaim_fail | 회수 실패 | writeback 경로 및 backing swap 상태 확인 |
reject_kmemcache_fail | 메타데이터 할당 실패 | 시스템 전반 OOM 근접 여부 점검 |
# reject 증가율 기반 단기 진단 예시
BASE=/sys/kernel/debug/zswap
before=$(cat $BASE/reject_alloc_fail)
sleep 10
after=$(cat $BASE/reject_alloc_fail)
echo "reject_alloc_fail delta=$((after-before)) / 10s"
# store 성공률 근사 계산
stored=$(cat $BASE/stored_pages)
r1=$(cat $BASE/reject_alloc_fail)
r2=$(cat $BASE/reject_compress_poor)
echo "stored=$stored rejects=$((r1+r2))"
swap-subsystem과의 중복 검토
두 문서를 함께 보면 중복되는 기초 설명이 일부 존재합니다. 학습 경로를 명확히 하기 위해 아래처럼 역할을 분리해서 읽는 것을 권장합니다.
- 중복되는 주제 — zswap/zram 비교, 압축 알고리즘 개요, 기본 모니터링 명령
- 이 페이지의 핵심 — zswap 내부 구조, store/load 경로, zpool 선택, reject/writeback 디버깅
- swap-subsystem 페이지 핵심 — swap out/in 전체 경로, swappiness, cgroup/PSI, 운영 플레이북
운영 순서는 Swapping 서브시스템에서 시스템 정책을 먼저 고정하고, 이후 이 페이지에서 zswap 내부 파라미터를 세부 조정하는 방식이 가장 재현성이 높습니다.
| 운영 질문 | 먼저 볼 페이지 | 이 페이지에서 다루는 범위 | 중복 방지 방식 |
|---|---|---|---|
| 왜 swap fault가 느려졌는가 | swap 지연시간 분해 | zswap hit/miss 및 writeback 비중 분석 | 시스템 지연 원인 판별은 swap-subsystem 우선 |
| 서비스별 swap 허용치를 어떻게 정할까 | cgroup v2 Swap 제어 | 정해진 한계 내 zswap 효율 최대화 | 정책 값은 이 페이지에서 재정의하지 않음 |
| reject가 급증할 때 어디부터 볼까 | 이 페이지 | 주관: reject 카운터 분기와 zpool 상태 | 원인 분기는 zswap 페이지 단일화 |
| writeback 때문에 tail latency가 늘 때 | 이 페이지 + block I/O 문서 | 주관: writeback-io coupling 분석 | swappiness 논의는 링크만 유지 |
| 최종 튜닝 검증은 무엇으로 할까 | Swap 튜닝 플레이북 | zswap 지표를 정책 지표와 함께 제출 | 검증 결과 형식은 swap-subsystem 기준 사용 |
참고자료
커널 소스
- mm/zswap.c -- zswap 핵심 구현
- mm/zpool.c -- zpool 추상화 인터페이스
- mm/zbud.c -- zbud 할당자
- mm/z3fold.c -- z3fold 할당자
- mm/zsmalloc.c -- zsmalloc 할당자
공식 문서
관련 커밋과 패치
- 2b281117 -- zswap: 최초 도입 (v3.11)
- v4.18 -- same-filled pages 최적화 추가
- v6.2 -- zsmalloc writeback 지원
- v6.5 -- frontswap 제거, 직접 통합, CONFIG_ZSWAP_DEFAULT_ON
- v6.8 -- 기본 zpool을 zsmalloc으로, 기본 압축을 lz4로 변경
관련 발표 및 자료
- LWN: In-kernel memory compression (2013)
- LWN: Removing frontswap (2023)
- Swapping 서브시스템 -- swap 전체 메커니즘 이해
- 메모리 관리 개요 (기초) -- 메모리 관리 전반
- 메모리 관리 (심화) -- VMA, 페이지 폴트, 단편화
- LRU Cache -- 페이지 회수 메커니즘
- Shrinker -- 메모리 압박 대응 콜백