zswap (압축 스왑 캐시) 심화

zswap은 리눅스 커널의 압축 스왑 캐시 메커니즘으로, 스왑 아웃되는 익명 페이지를 RAM 내에서 압축하여 실제 디스크 I/O를 크게 줄여줍니다. frontswap 인터페이스를 통한 페이지 가로채기, zpool 백엔드의 메모리 할당, 다양한 압축 알고리즘(lzo, lz4, zstd) 선택, writeback을 통한 배킹 스왑 연동, same-filled pages 최적화까지 전 영역을 상세히 다룹니다.

전제 조건: Swapping 서브시스템 문서와 메모리 관리 개요 (기초) 문서를 먼저 읽으세요. 가상 메모리, 익명 페이지(anonymous page), 스왑 공간의 기본 개념을 알고 있어야 합니다.
일상 비유: zswap은 압축 파일 보관함과 비슷합니다. 책장(RAM)이 가득 차서 창고(디스크 스왑)로 옮겨야 할 때, 바로 옮기지 않고 먼저 책을 압축해서 책장 한쪽에 보관합니다. 나중에 실제로 필요하면 압축을 풀어 즉시 사용하고, 정말 공간이 부족할 때만 창고로 최종 이동합니다.

핵심 요약

  • zswap -- 스왑 아웃 대상 페이지를 RAM 내에서 압축 저장하는 캐시 계층
  • frontswap -- 스왑 쓰기/읽기를 가로채어 zswap에 전달하는 커널 인터페이스
  • zpool -- 압축된 데이터를 저장하는 메모리 풀 추상화 (zbud, z3fold, zsmalloc)
  • writeback -- zswap 캐시가 가득 찰 때 LRU 순서로 배킹 스왑 디바이스에 기록
  • same-filled pages -- 동일 값으로 채워진 페이지를 압축 없이 값만 저장하는 최적화
  • accept_threshold -- zswap 풀이 최대 크기의 일정 비율에 도달하면 신규 저장을 거부하는 임계치

단계별 이해

  1. 메모리 압박 발생
    kswapd 또는 직접 회수 경로에서 익명 페이지를 스왑 아웃하려 합니다.
  2. frontswap 가로채기
    swap_writepage()가 호출되면, frontswap 인터페이스가 이를 가로채어 zswap_store()를 호출합니다.
  3. 페이지 압축
    선택된 알고리즘(lzo/lz4/zstd)으로 4KB 페이지를 압축합니다. 압축 결과가 원본보다 크면 거부합니다.
  4. zpool 저장
    압축된 데이터를 zpool 백엔드(zbud/z3fold/zsmalloc)에 할당받은 메모리에 저장합니다.
  5. 읽기 시 복원
    해당 페이지에 접근하면 zswap_load()가 호출되어 압축 해제 후 원본 페이지를 복원합니다.

개요

zswap이란?

zswap은 리눅스 커널 3.11(2013년 9월)에 도입된 압축 스왑 캐시(compressed swap cache)입니다. 커널의 메모리 회수 경로에서 익명 페이지가 스왑 아웃될 때, 실제 디스크에 기록하기 전에 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
커널 버전 참고: Linux 6.5부터 zswap은 CONFIG_ZSWAP_DEFAULT_ON=y로 기본 활성화되는 추세이며, 6.8부터 기본 zpool이 zsmalloc으로, 기본 압축 알고리즘이 lz4로 변경되었습니다. 이전 커널에서는 기본이 zbud + lzo-rle이었습니다.

zswap 아키텍처

zswap의 전체 아키텍처는 세 가지 핵심 계층으로 구성됩니다: frontswap 인터페이스, zswap 코어, 그리고 배킹 스왑 디바이스입니다.

zswap 전체 아키텍처 유저스페이스 프로세스 (익명 페이지) swap out 메모리 회수 (kswapd / direct reclaim) swap_writepage() frontswap 인터페이스 zswap_store() zswap 코어 압축 엔진 (lzo / lz4 / zstd) zpool 백엔드 (zbud / z3fold / zsmalloc) zswap_tree (RB-tree 인덱스) same-filled 최적화 writeback (LRU) 배킹 스왑 디바이스 (SSD / HDD / zram) 가로채기 RAM 압축 저장 디스크 I/O 빠름 (수 us) 느림 (수 ms) zswap_load() (swap in)

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을 반환하여, 커널이 원래의 스왑 디바이스에 기록하도록 합니다.
참고: Linux 6.5부터 frontswap 인터페이스가 제거되고, zswap이 스왑 서브시스템에 직접 통합되었습니다. swap_writepage() 내부에서 zswap_store()를 직접 호출하는 구조로 변경되어 간접 호출 오버헤드가 제거되었습니다.

zswap vs zram vs 전통적 swap 비교

리눅스 커널에는 스왑 관련 세 가지 주요 메커니즘이 있습니다. 각각의 동작 방식과 장단점을 비교합니다.

zswap vs zram vs 전통적 swap 전통적 swap 배킹 스토어: 디스크/SSD 압축: 없음 RAM 사용: 없음 속도: 느림 (ms 단위) SSD 마모 증가 I/O 대역폭 소비 장점: RAM 소비 없음 용량 제한 없음 (디스크) zswap 배킹 스토어: RAM + 디스크 압축: lzo / lz4 / zstd RAM 사용: 풀 크기만큼 속도: 빠름 (us 단위) writeback으로 I/O 최소화 기존 스왑과 호환 장점: 캐시 역할로 I/O 감소 배킹 스왑 필수 zram 배킹 스토어: RAM만 압축: lzo / lz4 / zstd RAM 사용: 전체 스왑 영역 속도: 매우 빠름 (us 단위) 디스크 I/O 완전 제거 블록 디바이스로 동작 장점: 독립 스왑 디바이스 RAM 부족 시 OOM 위험
특성전통적 swapzswapzram
동작 방식디스크 블록 직접 I/ORAM 압축 캐시 + writebackRAM 압축 블록 디바이스
디스크 필요필수필수 (배킹 스왑)불필요
RAM 소비없음설정 가능 (max_pool_percent)설정 가능 (disksize)
I/O 발생항상writeback 시에만없음
최대 용량디스크 크기RAM + 디스크RAM 크기
적합한 환경대용량 스왑 필요서버, 범용임베디드, 모바일
커널 설정기본 지원CONFIG_ZSWAPCONFIG_ZRAM

zswap 내부 구현

zswap의 핵심 자료구조를 이해하면 전체 동작을 파악할 수 있습니다. 주요 구조체는 zswap_entry, zswap_tree, zswap_pool입니다.

zswap 핵심 자료구조 관계 zswap_tree (per swap type) rbroot: RB-tree root spinlock: lock key: swap offset value: zswap_entry* zswap_entry rbnode: RB-tree 노드 offset: swap 오프셋 length: 압축 크기 pool: zswap_pool* handle: zpool 핸들 value: same-fill 값 objcg: 메모리 cgroup lru: LRU 리스트 노드 zswap_pool zpool: zpool* tfm: crypto_acomp* list: 풀 리스트 노드 kref: 참조 카운트 lru_list: LRU 헤드 nr_stored: 저장 수 next_shrink: 회수 커서 조회 참조 zpool 메모리 저장소 압축 페이지 A (1.2KB) 압축 페이지 B (0.8KB) 압축 페이지 C (2.1KB) handle 참조 관리
/* 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는 압축된 하나의 페이지를 나타냅니다. handlevalue가 union으로 묶여 있는 이유는, same-filled 페이지는 zpool 할당 없이 값만 저장하기 때문입니다.
  • 18-21행 zswap_tree는 스왑 타입(스왑 파티션/파일)별로 하나씩 존재하며, swap offset을 키로 하는 RB-tree입니다.
  • 24-33행 zswap_pool은 압축 알고리즘과 zpool 백엔드의 조합입니다. 런타임에 압축기나 zpool을 변경하면 새 풀이 생성되고, 기존 풀은 참조 카운트가 0이 될 때까지 유지됩니다.

압축/해제 흐름

zswap의 핵심 경로인 저장(store)과 로드(load)의 상세 흐름을 살펴봅니다.

zswap_store() 압축 저장 흐름 1. swap_writepage() 호출 2. zswap 활성화 확인 3. accept_threshold 초과 여부 확인 거부 (reject) 4. same-filled page 검사 값만 저장 5. crypto_acomp_compress() 호출 원본보다 크면 폴백 6. zpool_malloc() 메모리 할당 7. 압축 데이터 zpool에 복사 8. zswap_tree에 entry 삽입 + LRU 추가 저장 완료 (성공) 진입점 핵심 경로 인덱싱
/* 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은 가변 크기의 압축 객체를 저장하는 메모리 할당자의 추상화 계층입니다. 세 가지 백엔드가 존재하며, 각각 메모리 효율성과 성능 특성이 다릅니다.

zpool 백엔드별 페이지 내 객체 배치 zbud (2-buddy) 페이지당 최대 2개 객체 4KB 페이지 객체 A (1.8KB) 객체 B (1.5KB) 장점: writeback 지원 낮은 구현 복잡도 단점: 최대 50% 밀도 메모리 효율 낮음 압축률: ~1.7:1 z3fold (3-fold) 페이지당 최대 3개 객체 4KB 페이지 A (1.1K) B (1.0K) C (0.8K) 장점: writeback 지원 zbud 대비 밀도 향상 단점: 최대 75% 밀도 큰 객체에 비효율 압축률: ~2.5:1 zsmalloc 페이지 스팬, 다수 객체 연속 페이지 스팬 N개 객체 장점: 최고 메모리 효율 크기별 클래스 할당 단점: 구현 복잡 페이지 이동 불가(초기) 압축률: ~3.0:1
특성zbudz3foldzsmalloc
페이지당 최대 객체23크기 클래스별 가변
이론 최대 밀도~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 콜백을 통해 수행됩니다.

zswap Writeback 흐름 메모리 압박 또는 풀 초과 zswap_shrinker scan_objects() LRU 오래된 entry 선택 압축 해제 4KB 복원 배킹 스왑에 기록 __swap_writepage() zswap entry 해제 Writeback 상세 과정 1. LRU 리스트에서 가장 오래된 entry를 선택 2. zpool에서 압축 데이터를 읽어 임시 페이지에 해제 3. 임시 페이지를 __swap_writepage()로 디스크에 기록 4. 디스크 기록 완료 후 zswap entry와 zpool 메모리를 해제 5. 해제된 RAM 공간에 새로운 압축 페이지 저장 가능 * same-filled entry는 writeback 없이 바로 해제 (디스크 I/O 불필요)
/* 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;
}
Writeback 비활성화: writeback을 비활성화하면(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
설계 의도: accept_threshold는 zswap 풀이 최대 크기에 근접했을 때 갑작스러운 writeback 폭풍을 방지합니다. 미리 신규 저장을 거부하여 writeback에 시간을 주고, 시스템 전반의 메모리 압박을 완화합니다.

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 최적화 효과 일반 페이지 (4096 바이트) 압축 엔진 (CPU 사용) zpool 할당 (~1.5KB 사용) 저장 완료 entry + zpool 메모리 Same-filled (0x00...00) 값만 저장 8바이트 (length=0) 압축 엔진 우회, zpool 할당 없음 절약: CPU 시간 + zpool 메모리
# 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의 효과를 극대화하려면 워크로드에 맞는 파라미터 조정과 지속적인 모니터링이 필요합니다.

zswap 모니터링 지표 구조 /sys/kernel/debug/zswap/ stored_pages pool_total_size same_filled_pages duplicate_entry 거부(Reject) 카운터 reject_reclaim_fail reject_alloc_fail reject_kmemcache_fail reject_compress_poor Writeback 카운터 written_back_pages pool_limit_hit 핵심 모니터링 수식 압축률: stored_pages * 4KB / pool_total_size 절약 메모리: (stored_pages * 4KB) - pool_total_size 저장 성공률: stored / (stored + 모든 reject 합) Same-filled 비율: same_filled_pages / stored_pages Writeback 비율: written_back_pages / stored_pages
#!/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

일반적인 문제와 해결

문제: zswap이 활성화되지 않음
  • 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 통합이 개선되어, 컨테이너별 압축 페이지 추적이 가능합니다.

컨테이너 환경에서의 zswap 컨테이너 A mem limit: 4GB cgroup: /docker/a 컨테이너 B mem limit: 2GB cgroup: /docker/b 컨테이너 C mem limit: 8GB cgroup: /docker/c zswap 공유 풀 (objcg 추적) entry->objcg로 컨테이너별 메모리 사용량 추적 cgroup 통합 (6.1+) - objcg 기반 추적 - 컨테이너별 충전 - 메모리 제한 연동 - writeback 시 충전 대상 cgroup 복원 공정한 메모리 회계 배킹 스왑 (writeback)

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;
}
zswap Writeback 이후의 I/O 지연 전파 zswap LRU shrink entry 선택 압축 해제 CPU 시간 소비 swap_writepage() bio 제출 blk-mq 큐 대기 관측 포인트 1) written_back_pages 상승 + iowait 상승 동시 발생 여부 2) pool_limit_hit 급증 시 max_pool_percent만 늘리지 말고 스토리지 큐 지연도 확인 3) NVMe에서도 혼잡 시 writeback latency가 tail latency를 지배할 수 있음 4) 컨테이너 혼합 환경은 cgroup I/O 제어와 함께 튜닝해야 함 실무 권장 - zswap 저장 성공률과 블록 큐 대기시간을 한 대시보드에서 함께 본다 - writeback 급증 시 zswap 튜닝보다 I/O 병목 완화가 우선일 때가 많다
zswap이 빨라도 writeback이 느리면 전체 체감은 느립니다. 압축 계층과 블록 계층을 함께 계측해야 합니다.

Reject 카운터 기반 문제 분기

reject 카운터는 단순한 실패 집계가 아니라, 병목 위치를 빠르게 좁히는 신호입니다. 카운터별 해석을 고정하면 운영 대응 속도가 크게 빨라집니다.

카운터주요 의미우선 조치
reject_compress_poor압축 이득 부족정상 가능성 높음, 데이터 성격 확인
reject_alloc_failzpool 할당 실패풀 제한/메모리 압박/단편화 점검
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과의 중복 검토

두 문서를 함께 보면 중복되는 기초 설명이 일부 존재합니다. 학습 경로를 명확히 하기 위해 아래처럼 역할을 분리해서 읽는 것을 권장합니다.

운영 순서는 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 기준 사용
실전 흐름: 정책 진단과 zswap 미세 조정 분리 1) 시스템 정책 진단 swap-subsystem 기준선 확정 2) zswap 파라미터 조정 compressor/zpool/threshold 3) 통합 검증 PSI + reject/writeback + iowait 경계 규칙 - swappiness, memory.swap.max, PSI 임계치는 swap-subsystem에서 결정하고 이 페이지에는 재정의하지 않음 - 이 페이지는 zswap 내부 수단(압축/저장/회수/거부/쓰기 되돌리기)만 심화 - 장애 보고서는 "시스템 지표 + zswap 지표" 두 축으로 제출해 원인 혼동을 방지 - 문서 유지보수 시 중복 확장은 금지하고, 반대편 문서로 링크를 우선 사용 핵심 zswap 튜닝은 swap 정책을 대체하지 않습니다. 정책 위에서만 효과적으로 동작합니다.
문서 유지보수 팁: zswap 내부 함수/자료구조 변경 사항은 이 페이지에서만 상세 반영하고, swap-subsystem에는 "동작 영향 2~3줄 + 링크"만 추가하면 중복을 안정적으로 관리할 수 있습니다.

참고자료

커널 소스

공식 문서

관련 커밋과 패치

관련 발표 및 자료

다음 학습: