zswap (압축 스왑(Swap) 캐시(Cache))

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

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

핵심 요약

  • zswap -- 스왑 아웃 대상 페이지를 RAM 내에서 압축 저장하는 캐시 계층
  • frontswap -- 스왑 쓰기/읽기를 가로채어 zswap에 전달하는 커널 인터페이스
  • zpool -- 압축된 데이터를 저장하는 메모리 풀 추상화 (zbud, z3fold, zsmalloc)
  • writeback -- zswap 캐시가 가득 찰 때 LRU 순서로 배킹 스왑 디바이스에 기록
  • same-filled pages -- 동일 값으로 채워진 페이지를 압축 없이 값만 저장하는 최적화
  • accept_threshold -- zswap 풀이 최대 크기의 일정 비율에 도달하면 신규 저장을 거부하는 임계치
/* mm/zswap.c — zswap_store() 핵심 흐름 (간략화) */
bool zswap_store(struct folio *folio)
{
    /* 1. accept_threshold 검사 */
    if (zswap_check_limit())       /* 풀 사용량 > max * threshold → 거부 */
        return false;
    /* 2. same-filled 검사 (0x00, 0xFF 등 단일 값 페이지) */
    if (zswap_is_page_same_filled(page, &value))
        goto insert;              /* 압축 불필요, 값만 저장 */
    /* 3. per-CPU acomp_ctx로 페이지 압축 */
    crypto_acomp_compress(acomp_ctx->req);
    /* 4. zpool에 압축 데이터 저장 */
    zpool_malloc(pool, dst_len, &handle);
    /* 5. zswap_entry를 xarray에 삽입 */
insert:
    xa_store(&tree->xarray, offset, entry, GFP_KERNEL);
    return true;
}
# 현재 zswap 상태 확인 명령
$ cat /sys/module/zswap/parameters/enabled        # Y/N
$ cat /sys/module/zswap/parameters/compressor      # lz4, lzo, zstd 등
$ cat /sys/module/zswap/parameters/zpool           # zsmalloc, z3fold, zbud
$ cat /sys/module/zswap/parameters/max_pool_percent # 기본 20
$ grep -i zswap /proc/meminfo                      # Zswap, ZswapCompressed

단계별 이해

  1. 메모리 압박 발생
    kswapd 또는 직접 회수(try_to_free_pages()) 경로에서 익명 페이지를 스왑 아웃하려 합니다.

    확인: cat /proc/vmstat | grep pgscan

  2. frontswap 가로채기
    swap_writepage()__swap_writepage()zswap_store() 순서로 호출됩니다.

    확인: cat /sys/kernel/debug/zswap/stored_pages (저장 성공 수)

  3. 페이지 압축
    crypto_acomp_compress()로 4KB 페이지를 압축합니다. 압축 결과가 원본보다 크면 reject_compress_poor 카운터가 증가하며 거부합니다.

    확인: cat /sys/kernel/debug/zswap/reject_compress_poor

  4. zpool 저장
    zpool_malloc()로 압축된 데이터를 zpool 백엔드(zbud/z3fold/zsmalloc)에 저장합니다. 할당 실패 시 reject_alloc_fail이 증가합니다.

    확인: cat /sys/kernel/debug/zswap/pool_total_size (현재 풀 크기)

  5. 읽기 시 복원
    해당 페이지에 접근하면 zswap_load()crypto_acomp_decompress()가 호출되어 원본 페이지를 복원합니다.

    확인: grep Zswap /proc/meminfo

개요

zswap이란?

zswap은 리눅스 커널 3.11(2013년 9월)에 도입된 압축 스왑 캐시(compressed swap cache)입니다. 커널의 메모리 회수(Memory Reclaim) 경로에서 익명 페이지가 스왑 아웃될 때, 실제 디스크에 기록하기 전에 RAM 내에서 페이지를 압축하여 보관합니다. 이를 통해 다음과 같은 이점을 제공합니다:

zswap의 위치: 메모리 회수 경로

zswap은 frontswap 인터페이스를 통해 커널의 스왑 경로에 투명하게 삽입됩니다. 유저스페이스 프로세스(Process)나 스왑 디바이스 설정을 변경할 필요 없이, 커널 파라미터 하나로 활성화할 수 있습니다. 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행 스왑 엔트리에서 스왑 타입(어느 스왑 영역(Swap Area)인지)과 오프셋(해당 영역 내 위치)을 추출합니다.
  • 7행 등록된 모든 frontswap 백엔드를 순회하며 저장을 시도합니다. zswap이 등록되어 있으면 zswap_frontswap_store()가 호출됩니다.
  • 8-9행 백엔드가 0을 반환하면 저장 성공입니다. 이 경우 실제 디스크에 쓰기를 하지 않습니다.
  • 11행 모든 백엔드가 실패하면 -1을 반환하여, 커널이 원래의 스왑 디바이스에 기록하도록 합니다.
참고: Linux 6.5부터 frontswap 인터페이스가 제거되고, zswap이 스왑 서브시스템에 직접 통합되었습니다. swap_writepage() 내부에서 zswap_store()를 직접 호출하는 구조로 변경되어 간접 호출 오버헤드(Overhead)가 제거되었습니다.

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
# 현재 시스템에서 어떤 스왑 메커니즘이 활성인지 확인
# 1. zswap 활성 여부
$ cat /sys/module/zswap/parameters/enabled
Y

# 2. zram 디바이스 존재 여부
$ ls /dev/zram*
/dev/zram0

# 3. 전통적 스왑 장치 목록
$ swapon --show
NAME      TYPE      SIZE USED PRIO
/dev/sda2 partition  8G  1.2G   -2

# ⚠️ zswap과 zram을 동시에 사용하면 이중 압축이 발생합니다.
# zswap 사용 시 zram은 비활성화하세요.
$ swapoff /dev/zram0   # zram 스왑 비활성화

zswap 내부 구현

zswap의 핵심 자료구조를 이해하면 전체 동작을 파악할 수 있습니다. 주요 구조체(Struct)는 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을 변경하면 새 풀이 생성되고, 기존 풀은 참조 카운트(Reference Count)가 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에서 핸들을 매핑(Mapping)하여 압축 데이터에 접근하고, crypto API로 압축을 해제합니다.
  • 24-26행 로드 후에는 entry를 RB-tree에서 제거하고 zpool 메모리를 해제합니다. 페이지가 다시 활성화되었으므로 zswap 캐시에 남아있을 필요가 없습니다.

zpool 백엔드

zpool은 가변 크기의 압축 객체를 저장하는 메모리 할당자(Memory Allocator)의 추상화 계층입니다. 세 가지 백엔드가 존재하며, 각각 메모리 효율성과 성능 특성이 다릅니다.

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 비용)
  • 실시간(Real-time)/지연 민감: lz4 (해제 속도 최우선)
  • 레거시 호환: lzo-rle (오래된 커널 기본값)

압축 알고리즘 성능 시각화

아래 차트는 4KB 익명 페이지 기준으로 각 알고리즘의 상대적 성능을 나타냅니다. 실제 수치는 워크로드(Workload)와 하드웨어에 따라 달라지지만, 알고리즘 간 상대적 차이를 이해하는 데 도움이 됩니다.

압축 알고리즘 성능 비교 (4KB 페이지 기준) 상대 성능 (높을수록 좋음) 100% 75% 50% 25% lz4 (현재 기본) 95% 100% 50% lzo-rle (이전 기본) 90% 90% 55% zstd (고압축) 60% 70% 90% deflate (극한 압축) 40% 50% 95% 압축 속도 해제 속도 압축률
실측 참고치 (x86_64, 4KB 페이지):
  • lz4 압축: ~800 MB/s, 해제: ~4,000 MB/s, 비율: ~2.1:1
  • lzo-rle 압축: ~700 MB/s, 해제: ~3,500 MB/s, 비율: ~2.2:1
  • zstd 압축: ~350 MB/s, 해제: ~1,200 MB/s, 비율: ~2.8:1
  • deflate 압축: ~150 MB/s, 해제: ~500 MB/s, 비율: ~3.2:1
실제 값은 데이터 패턴에 크게 좌우됩니다. 텍스트/0 채움 데이터는 압축률이 높고, 이미 압축된(Compressed)/암호화된(Encrypted) 데이터는 압축률이 매우 낮습니다.

per-CPU 압축 컨텍스트(acomp_ctx) 구조

zswap은 CPU별 독립적인 압축/해제 컨텍스트를 유지하여 잠금 없는 병렬 처리(Lock-free Parallelism)를 실현합니다. 이 설계는 멀티코어(Multi-core) 시스템에서 zswap의 확장성(Scalability)을 보장하는 핵심 요소입니다.

per-CPU 압축 컨텍스트 (acomp_ctx) 구조 zswap_pool acomp_ctx __percpu * CPU 0 mutex (재진입 방지) req: acomp_request* buffer: 4KB 작업 버퍼 kswapd 압축 중... lz4_compress() 독립적 처리 잠금 경합 없음 CPU 1 mutex (재진입 방지) req: acomp_request* buffer: 4KB 작업 버퍼 direct reclaim 중... lz4_compress() 독립적 처리 잠금 경합 없음 CPU 2 mutex (재진입 방지) req: acomp_request* buffer: 4KB 작업 버퍼 zswap_load 중... lz4_decompress() 독립적 처리 잠금 경합 없음 CPU N ... N개 CPU 병렬
/* per-CPU 압축 컨텍스트 구조체 */
struct acomp_ctx {
    struct mutex mutex;           /* 동일 CPU 재진입 방지 */
    struct crypto_acomp *acomp;   /* 압축 알고리즘 인스턴스 */
    struct acomp_req *req;        /* 압축/해제 요청 구조체 */
    u8 *buffer;                    /* PAGE_SIZE 작업 버퍼 */
};

/* 풀 생성 시 per-CPU 컨텍스트 할당 */
pool->acomp_ctx = alloc_percpu(struct acomp_ctx);
for_each_possible_cpu(cpu) {
    struct acomp_ctx *ctx = per_cpu_ptr(pool->acomp_ctx, cpu);
    mutex_init(&ctx->mutex);
    ctx->acomp = crypto_alloc_acomp(pool->tfm_name, 0, 0);
    ctx->req = acomp_request_alloc(ctx->acomp);
    ctx->buffer = kmalloc(PAGE_SIZE, GFP_KERNEL);
}
코드 설명
  • mutex per-CPU이므로 다른 CPU와의 경합은 없지만, 동일 CPU에서 kswapd와 direct reclaim이 동시에 실행될 수 있으므로 mutex로 보호합니다. 스핀락(Spinlock) 대신 mutex를 사용하는 이유는 압축/해제 중 슬립(Sleep)이 가능한 컨텍스트이기 때문입니다.
  • acomp / req 각 CPU마다 독립적인 crypto 알고리즘 인스턴스와 요청 구조체를 가집니다. 이렇게 하면 scatterlist, 작업 버퍼 등을 공유할 필요 없이 완전히 독립적으로 압축/해제를 수행할 수 있습니다.
  • buffer PAGE_SIZE(4KB)의 작업 버퍼입니다. 페이지 데이터를 이 버퍼에 복사한 후 압축하고, 결과도 이 버퍼에 저장합니다. CPU 로컬 버퍼이므로 캐시 라인(Cache Line) 경합이 발생하지 않습니다.

Writeback 메커니즘

zswap의 RAM 풀이 가득 차거나 메모리 압박이 심해지면, LRU 순서로 오래된 압축 페이지를 배킹 스왑 디바이스로 기록(writeback)하여 RAM 공간을 확보합니다. 이 과정은 shrink 콜백(Callback)을 통해 수행됩니다.

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행 페이지를 커널 주소 공간(Address Space)에 매핑하고 첫 번째 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%

Exclusive Loads 모드

Linux 6.5에서 도입된 exclusive_loads 파라미터는 zswap_load() 호출 후 해당 엔트리를 즉시 무효화(Invalidate)할지 여부를 제어합니다. 이 파라미터는 zswap의 메모리 사용 패턴과 성능 특성에 큰 영향을 미칩니다.

exclusive_loads 모드 비교 exclusive_loads = Y (6.5+ 기본) zswap_load() 즉시 삭제 동작: 1. RB-tree에서 entry 검색 2. 압축 해제하여 folio에 복원 3. RB-tree에서 entry 즉시 제거 4. zpool 메모리 해제 장점 즉시 RAM 반환 → 풀 공간 확보 빠름 메모리 회계(accounting) 정확 단점 재스왑 시 다시 압축 필요 (CPU 비용) 잠깐 쓰고 바로 스왑 아웃 → 이중 작업 exclusive_loads = N zswap_load() 캐시 유지 동작: 1. RB-tree에서 entry 검색 2. 압축 해제하여 folio에 복원 3. entry를 RB-tree에 그대로 유지 4. 재스왑 시 기존 entry 재사용 장점 반복 스왑 아웃 시 재압축 불필요 CPU 비용 절감 (변경 안 된 페이지) 단점 RAM이 더 오래 점유됨 (풀 크기 증가) stale 데이터 유지 가능성
# exclusive_loads 확인 및 설정
$ cat /sys/module/zswap/parameters/exclusive_loads
Y

# 비활성화 (캐시 모드)
$ echo N > /sys/module/zswap/parameters/exclusive_loads

# 권장: 대부분의 환경에서 Y(기본값)가 적합
# N은 동일 페이지를 반복적으로 swap in/out하는 특수 워크로드에서만 유리
선택 가이드:
  • exclusive_loads=Y (기본): 대부분의 워크로드에 적합. RAM을 빠르게 반환하여 zswap 풀 효율 극대화
  • exclusive_loads=N: 동일 익명 페이지가 수정 없이 반복적으로 swap in/out되는 워크로드에서 CPU 절약. 예: 대규모 배치(Batch) 처리에서 메모리를 잠깐 참조하고 다시 스왑하는 패턴

커널 설정

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 최적화
컨테이너(Container) 호스트zstd + zsmalloc + 30%다수 컨테이너 메모리 절약

zswap 디버깅(Debugging)

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 카운터는 단순한 실패 집계가 아니라, 병목(Bottleneck) 위치를 빠르게 좁히는 신호입니다. 카운터별 해석을 고정하면 운영 대응 속도가 크게 빨라집니다.

reject 카운터 기반 진단 분기 reject 카운터 증가 감지 reject_compress_poor 압축 이득 없음 (이미 압축된/랜덤 데이터) reject_alloc_fail zpool 메모리 할당 실패 (풀 부족/단편화) reclaim/kmemcache 회수 실패 / 메타 할당 실패 (시스템 메모리 위기) 조치 • 대부분 정상 (데이터 특성) • 비율 높으면 lz4로 전환 • 암호화 데이터 비율 확인 • zswap 비활성화 검토 조치 • max_pool_percent 증가 • zpool → zsmalloc 전환 • writeback 활성화 확인 • 시스템 메모리 증설 조치 • 배킹 스왑 장치 점검 • swapon --show 확인 • OOM 근접 여부 점검 • dmesg 에러 로그 확인
카운터주요 의미우선 조치
reject_compress_poor압축 이득 부족정상 가능성 높음, 데이터 성격 확인
reject_alloc_failzpool 할당 실패풀 제한/메모리 압박/단편화(Fragmentation) 점검
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줄 + 링크"만 추가하면 중복을 안정적으로 관리할 수 있습니다.

zswap_store() 호출 체인 분석

메모리 회수 경로에서 zswap에 페이지가 저장되기까지의 전체 호출 체인(Call Chain)을 추적합니다. 커널 6.5 이후 frontswap이 제거되면서 호출 경로가 단순화되었지만, 핵심 흐름은 동일합니다.

zswap_store() 호출 체인 (Linux 6.x) 메모리 회수 계층 kswapd() direct_reclaim() memory.reclaim (cgroup) shrink_folio_list() pageout() → swap_writepage() v6.5 이전 (frontswap 경유) swap_writepage() → frontswap_store() → zswap_frontswap_store() v6.5 이후 (직접 호출) swap_writepage() → __swap_writepage() → zswap_store() zswap_store() 내부 1. zswap_can_accept() — threshold 확인 2. zswap_is_page_same_filled() — 동일값 페이지 최적화 3. crypto_acomp_compress() — 페이지 압축 4. zpool_malloc() + memcpy() — 압축 데이터 저장 5. zswap_tree_insert() + LRU — 인덱스 등록 6. obj_cgroup_charge() — cgroup 메모리 과금

호출 체인 요약 (Linux 6.8 기준):

  1. shrink_folio_list() — LRU 스캔에서 익명 folio 선택
    1. add_to_swap() — swap entry 할당
    2. folio_mark_dirty()
    3. pageout() — 실제 기록 시작
      1. swap_writepage()
        1. zswap_store() — zswap 진입점
          1. zswap_can_accept()
          2. zswap_compress()
          3. zpool_malloc()
          4. xa_store() — xarray에 entry 삽입

ftrace로 호출 체인 실시간 확인:

# echo zswap_store > /sys/kernel/tracing/set_ftrace_filter
# echo function_graph > /sys/kernel/tracing/current_tracer
# cat /sys/kernel/tracing/trace_pipe
 0)               |  zswap_store() {
 0)   0.234 us    |    zswap_can_accept();
 0)               |    crypto_acomp_compress() {
 0)   1.567 us    |      lz4_compress_crypto();
 0)   2.345 us    |    }
 0)   0.456 us    |    zpool_malloc();
 0)   0.123 us    |    xa_store();
 0)   5.678 us    |  }
v6.5 변경 요약: frontswap 추상화 계층이 제거되면서 zswap_store()가 swap 경로에서 직접 호출됩니다. 이로 인해 호출 스택(Call Stack)이 한 단계 줄어들고, 불필요한 간접 호출(Indirect Call) 오버헤드가 제거되었습니다. zswap_frontswap_store()zswap_store()로 이름이 변경되었습니다.

struct zswap_entry 필드별 해설

struct zswap_entry는 zswap에 저장된 개별 압축 페이지를 나타내는 핵심 자료구조입니다. 각 필드의 역할, 생명 주기(Lifecycle), 그리고 상호 관계를 상세히 분석합니다.

/* mm/zswap.c - struct zswap_entry 상세 (Linux 6.8 기준) */
struct zswap_entry {
    struct rb_node rbnode;         /* RB-tree 노드 — swap offset 기준 정렬 */
    swp_entry_t swpentry;           /* swap 엔트리 (type + offset 인코딩) */
    int length;                      /* 압축 후 바이트 크기 (0이면 same-filled) */
    struct zswap_pool *pool;        /* 이 엔트리를 관리하는 압축 풀 */
    union {
        unsigned long handle;        /* zpool 할당 핸들 (압축 데이터 위치) */
        unsigned long value;         /* same-filled 페이지의 반복 값 */
    };
    struct obj_cgroup *objcg;       /* 메모리 cgroup 소속 추적 */
    struct list_head lru;           /* LRU 이중 연결 리스트 노드 */
    refcount_t refcount;             /* 참조 카운트 (동시 접근 보호) */
};
코드 설명
  • rbnode struct rb_node로 RB-tree에 삽입됩니다. 키는 swpentry에 인코딩된 swap offset이며, zswap_tree_search()에서 O(log n) 검색에 사용됩니다. 동일 offset에 대한 중복 삽입 시 기존 엔트리를 교체(invalidate)합니다.
  • swpentry swp_entry_t는 swap type(상위 비트)과 offset(하위 비트)을 하나의 unsigned long에 인코딩한 값입니다. swp_type()swp_offset() 매크로로 분리합니다. 이 값이 RB-tree의 검색 키 역할을 합니다.
  • length 압축 후 데이터의 바이트 크기입니다. 0이면 same-filled 페이지를 의미하며, 이 경우 handle 대신 value 필드가 유효합니다. 최대 값은 PAGE_SIZE이며, 압축 후 크기가 원본 이상이면 저장을 거부합니다.
  • pool 이 엔트리를 생성한 zswap_pool에 대한 포인터입니다. 런타임에 압축기(Compressor)나 zpool 백엔드를 변경하면 새 풀이 생성되므로, 각 엔트리는 자신이 속한 풀을 명시적으로 추적해야 합니다. 로드/해제 시 올바른 압축 해제기와 zpool을 사용하기 위해 필수적입니다.
  • handle / value (union) 두 필드가 union으로 묶여 메모리를 절약합니다. 일반 압축 페이지는 handle에 zpool 할당 핸들을 저장하고, same-filled 페이지는 value에 반복되는 unsigned long 값을 저장합니다. length == 0일 때 value가 유효합니다.
  • objcg obj_cgroup 포인터로, 이 압축 페이지의 메모리 사용량을 원래 프로세스의 cgroup에 과금(Charge)합니다. cgroup v2 환경에서 zswap이 특정 cgroup의 메모리 한도를 초과하지 않도록 보장합니다. 엔트리 해제 시 obj_cgroup_uncharge()로 과금을 반환합니다.
  • lru LRU(Least Recently Used) 이중 연결 리스트의 노드입니다. writeback이 필요할 때 가장 오래된 엔트리부터 배킹 스왑에 기록합니다. list_add_tail()로 삽입하여 FIFO 순서를 유지하고, 로드 시에는 리스트에서 제거합니다.
  • refcount 동시 접근을 보호하는 참조 카운트입니다. store 경로에서 1로 초기화되고, load나 writeback이 진행 중일 때 증가합니다. 참조 카운트가 0이 되면 zpool 메모리를 해제하고 엔트리를 kfree합니다.
필드타입설정 시점해제 시점역할
rbnodestruct rb_nodetree inserttree deleteRB-tree 인덱스 노드
swpentryswp_entry_tstore 진입entry freeswap 위치 식별자 (검색 키)
lengthint압축 완료entry free압축 크기 (0 = same-filled)
poolzswap_pool *storeentry free압축기 + zpool 조합 참조
handleunsigned longzpool_malloczpool_free압축 데이터 위치
valueunsigned longsame-filled 감지entry free동일값 페이지 값
objcgobj_cgroup *cgroup chargecgroup uncharge메모리 cgroup 과금
lrustruct list_headstore 완료load/writebackLRU 순서 (writeback 대상 선정)
refcountrefcount_tstore (=1)0 도달 시동시 접근 보호

struct zswap_pool 필드별 해설

struct zswap_pool은 압축 알고리즘과 메모리 할당 백엔드(zpool)의 조합을 나타냅니다. 런타임에 압축기나 zpool 백엔드를 변경하면 새 풀이 생성되고, 기존 풀은 참조 카운트가 0이 될 때까지 유지됩니다.

/* mm/zswap.c - struct zswap_pool 상세 (Linux 6.8 기준) */
struct zswap_pool {
    struct zpool *zpool;              /* zpool 백엔드 인스턴스 */
    struct crypto_acomp *acomp;       /* 비동기 압축 알고리즘 핸들 */
    struct acomp_ctx __percpu *acomp_ctx; /* per-CPU 압축 컨텍스트 */
    struct kref kref;                 /* 참조 카운트 (안전한 해제) */
    struct list_head list;            /* 전역 zswap_pools 리스트 노드 */
    struct list_head lru_list;        /* 이 풀의 엔트리 LRU 헤드 */
    spinlock_t lru_lock;               /* lru_list 보호 스핀락 */
    struct hlist_node node;           /* shrinker 해시 테이블 노드 */
    atomic_t nr_stored;                /* 현재 저장된 엔트리 수 */
    char tfm_name[CRYPTO_MAX_ALG_NAME]; /* 압축 알고리즘 이름 */
};
코드 설명
  • zpool 실제 메모리 할당을 담당하는 zpool 백엔드(zbud, z3fold, zsmalloc) 인스턴스입니다. zpool_malloc()/zpool_free()로 압축 데이터 저장 공간을 할당/해제합니다. 풀 생성 시 zpool_create_pool()로 초기화됩니다.
  • acomp / acomp_ctx crypto_acomp는 커널 crypto API의 비동기 압축 알고리즘 핸들입니다. acomp_ctx는 per-CPU 구조체로, 각 CPU가 독립적으로 압축/해제를 수행할 수 있도록 합니다. per-CPU 설계로 동기화 오버헤드 없이 병렬 처리가 가능합니다.
  • kref 풀의 참조 카운트입니다. zswap_pool_current_get()으로 참조를 획득하고, zswap_pool_put()으로 반환합니다. 런타임에 압축기가 변경되어 새 풀이 생성되더라도, 기존 풀은 모든 엔트리가 해제될 때까지 안전하게 유지됩니다.
  • list 전역 zswap_pools 리스트에 연결됩니다. 헤드에 가장 최근에 생성된 풀이 위치하며, zswap_pool_current()는 항상 리스트의 첫 번째 풀을 반환합니다.
  • lru_list / lru_lock 이 풀에 속한 모든 zswap_entry의 LRU 순서를 유지합니다. writeback 시 lru_list의 앞쪽(가장 오래된) 엔트리부터 배킹 스왑으로 기록합니다. lru_lock은 리스트 조작 시 동기화를 보장하는 스핀락(Spinlock)입니다.
  • node 메모리 shrinker 해시 테이블에 연결되는 노드입니다. 메모리 압박 시 shrinker 콜백이 호출되면, 이 노드를 통해 풀을 찾아 writeback을 수행합니다.
  • nr_stored 현재 이 풀에 저장된 엔트리 수를 원자적(Atomic)으로 추적합니다. 디버깅과 모니터링에 사용되며, /sys/kernel/debug/zswap의 통계에 반영됩니다.
  • tfm_name 압축 알고리즘의 이름 문자열입니다(예: "lz4", "zstd", "lzo-rle"). 풀 생성 시 설정되며, sysfs를 통한 런타임 변경 시 새 풀 생성의 기준이 됩니다.

zswap_store() 함수 구현 분석

zswap_store() 함수의 실제 구현을 단계별로 분석합니다. 아래 코드는 Linux 6.8 기준으로 핵심 로직만 추출한 것이며, 에러 처리와 통계 업데이트 등 부수적 코드는 생략했습니다.

/* mm/zswap.c - zswap_store() 상세 구현 분석 (Linux 6.8, 간략화) */
bool zswap_store(struct folio *folio)
{
    struct zswap_entry *entry, *old;
    struct obj_cgroup *objcg = NULL;
    struct zswap_pool *pool;
    struct zpool *zpool;
    unsigned int dlen = PAGE_SIZE;
    unsigned long handle, value;
    u8 *buf;

    /* (1) 전역 활성화 및 풀 용량 확인 */
    if (!zswap_enabled || !zswap_can_accept())
        return false;

    /* (2) cgroup 메모리 과금 시도 */
    objcg = get_obj_cgroup_from_folio(folio);
    if (objcg && !obj_cgroup_may_zswap(objcg))
        goto reject;

    /* (3) zswap_entry 할당 */
    entry = zswap_entry_cache_alloc(GFP_KERNEL, folio_nid(folio));
    if (!entry)
        goto reject;

    /* (4) same-filled page 최적화 — 압축 없이 값만 저장 */
    if (zswap_is_page_same_filled(folio, &value)) {
        entry->length = 0;
        entry->value = value;
        atomic_inc(&zswap_same_filled_pages);
        goto insert;
    }

    /* (5) 현재 활성 풀 획득 (참조 카운트 증가) */
    pool = zswap_pool_current_get();
    if (!pool)
        goto freepage;

    /* (6) per-CPU 압축 컨텍스트에서 압축 수행 */
    struct acomp_ctx *acomp_ctx = per_cpu_ptr(pool->acomp_ctx, raw_smp_processor_id());
    mutex_lock(&acomp_ctx->mutex);
    memcpy(acomp_ctx->buffer, kmap_local_folio(folio, 0), PAGE_SIZE);
    sg_init_one(&input, acomp_ctx->buffer, PAGE_SIZE);
    sg_init_one(&output, acomp_ctx->buffer, PAGE_SIZE);
    acomp_request_set_params(acomp_ctx->req, &input, &output, PAGE_SIZE, dlen);
    crypto_acomp_compress(acomp_ctx->req);
    dlen = acomp_ctx->req->dlen;

    /* (7) 압축 효율 검증 — 원본 이상이면 거부 */
    if (dlen >= PAGE_SIZE) {
        zswap_reject_compress_poor++;
        goto unlock;
    }

    /* (8) zpool 메모리 할당 */
    zpool = pool->zpool;
    if (zpool_malloc(zpool, dlen, &handle)) {
        zswap_reject_alloc_fail++;
        goto unlock;
    }

    /* (9) 압축 데이터를 zpool에 복사 */
    buf = zpool_map_handle(zpool, handle, ZPOOL_MM_WO);
    memcpy(buf, acomp_ctx->buffer, dlen);
    zpool_unmap_handle(zpool, handle);
    mutex_unlock(&acomp_ctx->mutex);

    /* (10) entry 필드 설정 */
    entry->handle = handle;
    entry->length = dlen;
    entry->pool = pool;

insert:
    /* (11) RB-tree 삽입 — 기존 entry가 있으면 교체 */
    old = zswap_tree_insert(&tree->rbroot, entry);
    if (old)
        zswap_entry_free(old);

    /* (12) LRU 리스트 꼬리에 추가 + 통계 업데이트 */
    spin_lock(&pool->lru_lock);
    list_add_tail(&entry->lru, &pool->lru_list);
    spin_unlock(&pool->lru_lock);
    atomic_inc(&pool->nr_stored);
    atomic_inc(&zswap_stored_pages);

    return true;
}
코드 설명
  • (1) 전역 확인 zswap_can_accept()는 현재 zswap 풀 크기가 max_pool_percent * accept_threshold_percent / 100를 초과하는지 확인합니다. 초과하면 새로운 저장을 거부하여 메모리 폭주를 방지합니다.
  • (2) cgroup 과금 cgroup v2 환경에서 obj_cgroup_may_zswap()는 해당 cgroup의 zswap 사용이 허용되는지 확인합니다. memory.zswap.max 제한을 초과하면 저장을 거부합니다.
  • (3) entry 할당 전용 slab 캐시(zswap_entry_cache)에서 zswap_entry를 할당합니다. NUMA 노드(folio_nid)를 고려하여 가능한 한 가까운 노드에서 할당합니다.
  • (4) same-filled 최적화 페이지 전체가 동일한 unsigned long 값으로 채워져 있는지 검사합니다. 0으로 채워진 zero page가 대표적입니다. 이 경우 압축도 zpool 할당도 필요 없이 값만 저장하므로, 메모리와 CPU 시간을 크게 절약합니다.
  • (5) 풀 획득 zswap_pool_current_get()는 전역 리스트의 첫 번째 풀을 반환하면서 kref 참조를 증가시킵니다. 이렇게 해야 store 도중에 풀이 해제되는 것을 방지할 수 있습니다.
  • (6) per-CPU 압축 per-CPU 압축 컨텍스트를 사용하여 CPU간 잠금(Lock) 없이 병렬 압축이 가능합니다. mutex는 동일 CPU에서의 재진입을 방지합니다. kmap_local_folio()로 folio를 임시 매핑하여 압축 버퍼에 복사합니다.
  • (7) 압축 효율 검증 압축 결과가 원본 PAGE_SIZE 이상이면 압축 효과가 없으므로 거부합니다. 이 경우 reject_compress_poor 카운터가 증가하며, 해당 페이지는 일반 swap 경로로 폴백(Fallback)됩니다.
  • (8-9) zpool 할당 및 복사 zpool_malloc()은 압축 크기(dlen)만큼의 메모리를 할당하고 핸들을 반환합니다. zpool_map_handle()로 핸들을 커널 가상 주소로 매핑한 뒤, 압축 데이터를 복사합니다. ZPOOL_MM_WO(Write Only)는 쓰기 전용 매핑입니다.
  • (11) RB-tree 삽입 동일한 swap offset에 대한 기존 엔트리가 있으면 교체합니다. 이는 페이지가 수정되어 다시 스왑 아웃되는 경우에 발생합니다. 기존 엔트리의 zpool 메모리도 함께 해제됩니다.
  • (12) LRU 및 통계 LRU 리스트의 꼬리에 추가하여 FIFO 순서를 유지합니다. lru_lock 스핀락으로 동시 접근을 보호하며, nr_stored와 전역 zswap_stored_pages 카운터를 원자적으로 증가시킵니다.

zswap_load() 경로 분석

스왑 인(Swap In) 시 zswap_load()가 호출되어 압축 데이터를 복원합니다. 아래에서 전체 해제 경로와 각 단계의 커널 구현을 분석합니다.

zswap_load() 해제 경로 (Linux 6.x) do_swap_page() — 페이지 폴트 zswap_load(folio) zswap_tree_search() — O(log n) miss → 디스크 읽기 entry->length == 0 ? same-filled 경로 zswap_fill_page(dst, entry->value) 압축 해제 경로 1. zpool_map_handle(ZPOOL_MM_RO) 2. crypto_acomp_decompress() 3. zpool_unmap_handle() 엔트리 정리 zswap_tree_delete() → zswap_entry_free() → atomic_dec(stored_pages) 진입 분기 정리
/* mm/zswap.c - zswap_load() 상세 구현 (Linux 6.8, 간략화) */
bool zswap_load(struct folio *folio)
{
    swp_entry_t swp = folio->swap;
    pgoff_t offset = swp_offset(swp);
    struct zswap_tree *tree = swap_zswap_tree(swp);
    struct zswap_entry *entry;
    u8 *dst;

    /* (1) RB-tree에서 swap offset으로 entry 검색 */
    spin_lock(&tree->lock);
    entry = zswap_tree_search(&tree->rbroot, offset);
    if (!entry) {
        spin_unlock(&tree->lock);
        return false;    /* miss → 일반 swap 읽기로 폴백 */
    }
    zswap_entry_get(entry);  /* 참조 카운트 증가 */
    spin_unlock(&tree->lock);

    /* (2) folio를 커널 가상 주소로 매핑 */
    dst = kmap_local_folio(folio, 0);

    /* (3) same-filled page — 압축 해제 없이 값으로 채움 */
    if (entry->length == 0) {
        zswap_fill_page(dst, entry->value);
        goto stats;
    }

    /* (4) zpool에서 압축 데이터 매핑 (읽기 전용) */
    struct zswap_pool *pool = entry->pool;
    struct acomp_ctx *acomp_ctx = per_cpu_ptr(pool->acomp_ctx, raw_smp_processor_id());
    mutex_lock(&acomp_ctx->mutex);
    u8 *src = zpool_map_handle(pool->zpool, entry->handle, ZPOOL_MM_RO);
    memcpy(acomp_ctx->buffer, src, entry->length);
    zpool_unmap_handle(pool->zpool, entry->handle);

    /* (5) 압축 해제 — 원본 PAGE_SIZE로 복원 */
    sg_init_one(&input, acomp_ctx->buffer, entry->length);
    sg_init_one(&output, dst, PAGE_SIZE);
    acomp_request_set_params(acomp_ctx->req, &input, &output, entry->length, PAGE_SIZE);
    crypto_acomp_decompress(acomp_ctx->req);
    mutex_unlock(&acomp_ctx->mutex);

stats:
    kunmap_local(dst);

    /* (6) entry 정리 — RB-tree 제거, zpool 해제, 통계 감소 */
    spin_lock(&tree->lock);
    zswap_tree_delete(&tree->rbroot, entry);
    spin_unlock(&tree->lock);

    zswap_entry_put(entry);  /* refcount→0이면 zpool_free + kfree */
    atomic_dec(&zswap_stored_pages);

    return true;
}
코드 설명
  • (1) RB-tree 검색 zswap_tree_search()는 swap offset을 키로 RB-tree를 탐색합니다. 검색 전 tree->lock 스핀락을 획득하고, entry를 찾으면 zswap_entry_get()으로 참조 카운트를 증가시킨 뒤 잠금을 해제합니다. 검색 실패(miss)는 해당 페이지가 이미 writeback으로 배킹 스왑에 기록되었거나, zswap에 저장된 적 없는 경우입니다.
  • (2) folio 매핑 kmap_local_folio()는 folio를 현재 CPU의 로컬 가상 주소에 임시 매핑합니다. HIGHMEM 환경에서도 안전하게 동작하며, 사용 후 kunmap_local()로 해제합니다.
  • (3) same-filled 복원 length == 0이면 same-filled 페이지입니다. zswap_fill_page()memset_l()을 사용하여 페이지 전체를 entry->value 값으로 채웁니다. zpool 접근이 불필요하므로 해제 속도가 매우 빠릅니다.
  • (4) 압축 데이터 접근 ZPOOL_MM_RO(Read Only)로 핸들을 매핑하여 압축 데이터에 접근합니다. per-CPU 버퍼에 복사한 뒤 즉시 unmap하여, zpool 내부 잠금 보유 시간을 최소화합니다.
  • (5) 압축 해제 crypto API의 crypto_acomp_decompress()로 압축을 해제합니다. 입력은 entry->length 바이트의 압축 데이터이고, 출력은 PAGE_SIZE 바이트의 원본 페이지입니다. per-CPU mutex로 동일 CPU에서의 동시 실행을 방지합니다.
  • (6) entry 정리 RB-tree에서 entry를 삭제하고, zswap_entry_put()으로 참조 카운트를 감소시킵니다. 참조 카운트가 0이 되면 zpool_free()로 압축 메모리를 해제하고 entry를 kfree합니다. 로드 후 entry를 제거하는 이유는, 해당 페이지가 활성 메모리로 복귀했으므로 zswap 캐시에 유지할 필요가 없기 때문입니다.
성능 특성: same-filled 페이지의 로드는 ~0.1μs 수준으로, 일반 압축 해제(~2-5μs)보다 수십 배 빠릅니다. 디스크 swap 읽기(~1-10ms)와 비교하면 수천 배 이상의 차이입니다. zswap_same_filled_pages 카운터가 높을수록 zswap의 실질 효과가 커집니다.

RB-tree에서 xarray로의 전환 (Linux 6.9+)

Linux 6.9에서 zswap의 인덱스 구조가 RB-tree에서 xarray로 전환되었습니다. 이 변경은 zswap의 동시성(Concurrency), 메모리 효율, 코드 복잡도에 큰 영향을 미칩니다.

zswap 인덱스 구조 전환: RB-tree → xarray 이전: RB-tree (v3.11 ~ v6.8) 256 128 512 64 192 특성: - 검색: O(log n), 균형 이진 트리 - 동기화: spinlock (전체 트리 잠금) - 노드 오버헤드: rb_node (24바이트) - 삽입/삭제: 리밸런싱 필요 문제점 spinlock 경합 → 다중 CPU에서 병목 zswap_entry에 rb_node 포함 필수 → entry 크기 증가, 메모리 낭비 현재: xarray (v6.9+) xa_head slot[0] slot[1] slot[4] slot[8] entry* entry* 특성: - 검색: O(1) ~ O(log n), 기수 트리 - 동기화: xa_lock (세밀한 잠금) - 노드: xarray 내부 관리 (entry 경량화) - 삽입/삭제: 리밸런싱 불필요 개선점 RCU 읽기 → load 경로에서 잠금 제거 가능 entry에서 rb_node 필드 제거 → entry 크기 24바이트 감소, 메모리 절약 v6.9
/* v6.8 이전: RB-tree 기반 인덱스 */
struct zswap_entry {
    struct rb_node rbnode;    /* 24바이트 오버헤드 */
    swp_entry_t swpentry;
    /* ... */
};

struct zswap_tree {
    struct rb_root rbroot;
    spinlock_t lock;          /* 전체 트리 잠금 — 경합 심각 */
};

/* v6.9 이후: xarray 기반 인덱스 */
struct zswap_entry {
    /* rb_node 제거됨 — 24바이트 절약! */
    swp_entry_t swpentry;
    /* ... */
};

/* per-swap-type xarray */
static struct xarray *swap_zswap_tree(swp_entry_t swp)
{
    return &swap_info[swp_type(swp)]->zswap_tree;
}

/* xarray 삽입 */
xa_store(tree, offset, entry, GFP_KERNEL);

/* xarray 검색 — RCU 보호로 잠금 없이 읽기 가능 */
rcu_read_lock();
entry = xa_load(tree, offset);
rcu_read_unlock();

/* xarray 삭제 */
xa_erase(tree, offset);
코드 설명
  • rb_node 제거 xarray는 내부적으로 노드를 관리하므로, zswap_entryrb_node를 포함할 필요가 없습니다. 이는 entry당 24바이트를 절약합니다. 100만 개의 압축 페이지가 있다면 약 23MB의 메모리 절약 효과가 있습니다.
  • xa_store / xa_load / xa_erase xarray API는 RB-tree의 rb_insert_color()/rb_erase()보다 사용이 간단합니다. 키(swap offset)를 직접 인덱스로 사용하므로 비교 함수도 불필요합니다.
  • RCU 읽기 xa_load()는 RCU(Read-Copy-Update) 보호 하에 잠금 없이 수행할 수 있습니다. zswap_load() 경로에서 spinlock 획득을 제거하여, 다중 CPU에서의 동시 읽기 성능이 크게 향상됩니다.
특성RB-tree (v6.8 이전)xarray (v6.9+)
검색 복잡도O(log n)O(log₆₄ n) ≈ O(1)
읽기 동기화spinlock 필수RCU (잠금 불필요)
쓰기 동기화spinlockxa_lock
entry 오버헤드+24바이트 (rb_node)없음
리밸런싱삽입/삭제 시 필요불필요
캐시 친화성포인터 추적 → 캐시 미스배열 기반 → 캐시 적중률 높음
코드 복잡도비교함수 + 트리 조작단순 API 호출
성능 영향: 특히 다중 CPU에서 메모리 압박이 심한 환경에서 효과가 큽니다. 여러 CPU가 동시에 zswap_load()를 호출할 때, RB-tree 방식은 spinlock 경합이 심해지지만 xarray의 RCU 읽기는 잠금 없이 수행되어 확장성이 크게 개선됩니다. Facebook/Meta의 내부 벤치마크(Benchmark)에서 이 변경으로 zswap 관련 락 경합이 80% 이상 감소한 것으로 보고되었습니다.

zswap 풀 생명주기

zswap 풀의 생성, 전환, 소멸 과정을 이해하면 런타임에 압축 알고리즘이나 zpool 백엔드를 변경할 때 발생하는 내부 동작을 파악할 수 있습니다. 풀은 참조 카운트(Reference Count) 기반으로 관리되어, 기존 엔트리가 모두 해제될 때까지 안전하게 유지됩니다.

zswap 풀 생명주기 (런타임 전환) 시간 t₀ Pool A (lzo + zbud) 상태: 활성 (current) kref: 1 + N (N = 저장된 entry 수) 모든 새 store → Pool A t₁ echo lz4 > /sys/module/zswap/parameters/compressor t₂ Pool A (lzo + zbud) 상태: 비활성 (draining) kref: N (기존 entry만 참조) 새 store 불가, load/writeback만 Pool B (lz4 + zbud) 상태: 활성 (current) kref: 1 + M (M = 신규 entry 수) 모든 새 store → Pool B t₃ Pool A kref: 감소 중... (load/writeback 시 해제) Pool B (활성) kref: 증가 중... 새 entry 계속 추가 t₄ Pool A: kref → 0 zswap_pool_destroy() Pool B (유일한 활성 풀) 모든 entry가 Pool B에 속함 핵심: 풀 전환은 무중단(Zero-downtime)으로 진행됩니다. 기존 entry는 원래 풀의 압축기로 해제되고, 새 entry는 새 풀의 압축기로 압축됩니다. 사용자/프로세스는 전환을 인지하지 못합니다.
/* mm/zswap.c - 풀 전환 핵심 로직 */

/* 1. 새 풀 생성 */
static struct zswap_pool *zswap_pool_create(char *type, char *compressor)
{
    struct zswap_pool *pool;
    pool = kzalloc(sizeof(*pool), GFP_KERNEL);
    pool->zpool = zpool_create_pool(type, ...);
    kref_init(&pool->kref);     /* kref = 1 */
    /* per-CPU 압축 컨텍스트 할당 */
    cpuhp_state_add_instance(...);
    return pool;
}

/* 2. 현재 활성 풀 교체 */
static int __zswap_param_set(...)
{
    struct zswap_pool *pool, *old;

    pool = zswap_pool_create(type, compressor);
    spin_lock(&zswap_pools_lock);
    old = zswap_pool_current();
    list_add_rcu(&pool->list, &zswap_pools);  /* 새 풀을 리스트 헤드에 */
    spin_unlock(&zswap_pools_lock);

    /* 이전 풀의 기본 참조 반환 */
    if (old)
        zswap_pool_put(old);  /* kref-- */
}

/* 3. 풀 참조 반환 → kref가 0이 되면 소멸 */
static void zswap_pool_put(struct zswap_pool *pool)
{
    kref_put(&pool->kref, zswap_pool_release);
}

static void zswap_pool_release(struct kref *kref)
{
    /* 모든 entry가 해제되었으므로 풀을 안전하게 파괴 */
    zpool_destroy_pool(pool->zpool);
    free_percpu(pool->acomp_ctx);
    kfree(pool);
}
주의: 풀 전환 중에는 두 개 이상의 풀이 동시에 존재할 수 있습니다. 이 기간 동안 메모리 사용량이 일시적으로 증가할 수 있으며, 빈번한 압축기 변경은 피해야 합니다. 또한 이전 풀의 entry가 모두 소진될 때까지 이전 압축 알고리즘 모듈을 언로드(Unload)하면 커널 패닉(Kernel Panic)이 발생할 수 있습니다.

메모리 압박 단계별 zswap 동작

시스템 메모리 압박 수준에 따라 zswap의 동작이 어떻게 변하는지 이해하면, 파라미터 튜닝과 장애 대응에 큰 도움이 됩니다. 아래 다이어그램은 워터마크(Watermark) 기반의 메모리 상태와 zswap의 반응을 단계별로 보여줍니다.

메모리 압박 단계별 zswap 동작 충분 low min 위기 OOM high low min 단계 1: 메모리 충분 (high 워터마크 이상) - 스왑 발생 없음 → zswap 활동 없음 - zswap 풀: 유휴 (이전에 저장된 entry만 유지) zswap 영향: 없음 단계 2: kswapd 동작 (low ~ high 사이) - kswapd가 백그라운드에서 페이지 회수 시작 - 익명 페이지 → zswap_store() 호출 → RAM 내 압축 저장 - zswap 풀 크기 점진적 증가, accept_threshold 미만이면 정상 저장 zswap 영향: 활발한 저장 단계 3: Direct Reclaim (min ~ low 사이) - 할당 경로에서 직접 회수 → zswap_store() 빈도 급증 - zswap 풀 포화 → accept_threshold 도달 → reject 증가 - writeback 발생: LRU 순서로 배킹 스왑에 기록하여 RAM 확보 zswap 영향: 풀 포화 + writeback 단계 4: 메모리 위기 (min 이하) - zswap 풀 최대 → 대부분의 store 거부 (reject_alloc_fail 급증) - shrinker가 공격적으로 writeback 수행 - zswap 자체가 메모리 압박 완화에 한계 → 직접 디스크 스왑 증가 zswap 영향: 효과 감소 단계 5: OOM zswap을 포함한 모든 회수 실패 → OOM Killer 호출 zswap 영향: OOM 지연 효과만
# 현재 메모리 압박 단계를 빠르게 판별하는 명령

# 1. 워터마크 대비 free 확인 (단계 판별의 핵심)
$ grep -A4 "Normal" /proc/zoneinfo | grep -E "free|min|low|high"
  pages free     120575
        min      16384
        low      20480
        high     24576

# 2. PSI 메트릭으로 메모리 압박 정도 확인
$ cat /proc/pressure/memory
some avg10=0.00 avg60=0.00 avg300=0.12 total=345678
full avg10=0.00 avg60=0.00 avg300=0.00 total=12345

# 3. zswap 활동 수준으로 단계 추정
$ cat /sys/kernel/debug/zswap/stored_pages     # 증가 중이면 단계 2+
$ cat /sys/kernel/debug/zswap/written_back_pages # 증가 중이면 단계 3+

# 4. kswapd 활성 여부
$ grep kswapd /proc/vmstat | head
pgscan_kswapd 4567890    # 증가 중이면 단계 2+
pgscan_direct 12345      # 증가 중이면 단계 3+
튜닝 시사점:
  • 단계 2에서 오래 머무르도록 max_pool_percent를 충분히 크게 설정하면, 디스크 I/O 없이 메모리 회수가 진행됩니다
  • 단계 3 진입 빈도가 높다면 vm.swappiness를 높여 kswapd 단계에서 더 적극적으로 스왑하는 것이 유리합니다
  • 단계 4 진입은 zswap 튜닝만으로는 해결 불가 — 근본적인 메모리 확장이나 워크로드 조정이 필요합니다
  • PSI(Pressure Stall Information) 메트릭의 memory somememory full을 함께 모니터링하면 현재 어느 단계에 있는지 파악할 수 있습니다

cgroup v2 통합

Linux 6.1부터 zswap의 cgroup v2 통합이 크게 개선되어, 컨테이너/서비스 단위로 zswap 사용량을 제어하고 모니터링할 수 있습니다. 이 섹션에서는 memory.zswap.* 인터페이스의 상세 동작을 다룹니다.

cgroup v2 zswap 제어 인터페이스 (6.1+) / (root cgroup) zswap 전역 풀 관리 /system.slice/web.service memory.zswap.current: 128 MB memory.zswap.max: 512 MB memory.zswap.writeback: 1 (활성) → 128/512 = 25% 사용 /system.slice/db.service memory.zswap.current: 480 MB memory.zswap.max: 1024 MB memory.zswap.writeback: 0 (비활성) → writeback 비활성 = 풀 초과 시 reject 제어 인터페이스 memory.zswap.max — cgroup별 최대 zswap 사용 memory.zswap.writeback — writeback 허용 여부 memory.zswap.current — 현재 사용 중인 크기 memory.stat: zswap 관련 통계 동작 메커니즘 obj_cgroup_charge_zswap() — 저장 시 과금 obj_cgroup_uncharge_zswap() — 해제 시 반환 writeback=0 → store 거부 시 디스크 직접 기록 max 초과 → 해당 cgroup의 새 store 거부
# cgroup v2 zswap 인터페이스 사용 예시

# 현재 cgroup의 zswap 사용량 확인
$ cat /sys/fs/cgroup/system.slice/web.service/memory.zswap.current
134217728    # 128 MB (바이트 단위)

# cgroup별 최대 zswap 크기 설정
$ echo $((512 * 1024 * 1024)) > \
    /sys/fs/cgroup/system.slice/web.service/memory.zswap.max
# 512 MB까지 zswap 사용 허용

# writeback 비활성화 (풀 초과 시 직접 스왑)
$ echo 0 > /sys/fs/cgroup/system.slice/db.service/memory.zswap.writeback
# DB 서비스의 zswap writeback 차단 → 디스크 I/O 예측 가능

# memory.stat에서 zswap 통계 확인
$ grep zswap /sys/fs/cgroup/system.slice/web.service/memory.stat
zswap                   134217728
zswapped                402653184
# zswap: 실제 사용 메모리, zswapped: 압축 전 원본 크기
# 압축률 = zswapped / zswap = 402653184 / 134217728 ≈ 3.0:1
인터페이스타입기본값도입 버전설명
memory.zswap.current읽기 전용-6.1현재 cgroup의 zswap 메모리 사용량 (바이트)
memory.zswap.max읽기/쓰기max (무제한)6.1cgroup별 zswap 최대 사용량 제한
memory.zswap.writeback읽기/쓰기1 (활성)6.50: store 거부 시 즉시 디스크, 1: writeback 허용
memory.stat:zswap읽기 전용-6.1실제 zswap 메모리 사용량
memory.stat:zswapped읽기 전용-6.1압축 전 원본 페이지 크기 합계
cgroup 활용 시나리오:
  • DB 서비스: memory.zswap.writeback=0 — writeback 비활성화로 I/O 지연을 예측 가능하게 유지. zswap 캐시 미스 시 즉시 디스크 읽기를 수행하여, writeback에 의한 예기치 않은 I/O 폭풍을 방지
  • 웹 서비스: memory.zswap.maxmemory.max의 10~20%로 설정 — 적절한 압축 캐시를 보장하되, 과도한 RAM 소비를 방지
  • 배치(Batch) 작업: 높은 memory.zswap.max + writeback=1 — 최대한 RAM 내에서 해결하고, 배킹 스왑은 최후 수단으로만 사용

흔한 실수와 모범 사례

실수증상모범 사례
zswap + zram 동시 활성화 이중 압축으로 CPU 낭비, zswap writeback이 zram으로 가면 RAM만 더 소비 둘 중 하나만 선택. 서버에서는 zswap(배킹 디스크 있음), 임베디드에서는 zram(디스크 없음)
max_pool_percent를 너무 높게 설정 zswap이 RAM의 절반 이상을 차지하여 파일 캐시(Page Cache) 부족 → I/O 성능 저하 20-35% 범위로 시작하여 워크로드에 맞게 조정. /proc/meminfoCached 값 모니터링
압축 불가 데이터에 zstd 사용 reject_compress_poor 급증, CPU 낭비 암호화/이미 압축된 데이터 비율이 높으면 lz4로 빠르게 실패 처리
배킹 스왑 없이 zswap만 활성화 zswap 풀이 차면 writeback 불가 → 모든 신규 store 거부 반드시 스왑 파티션이나 스왑 파일을 설정. swapon --show로 확인
런타임에 빈번하게 압축기 변경 다수의 이전 풀이 동시에 존재하여 메모리 낭비 부팅 파라미터로 한 번 설정하고, 변경은 유지보수 창(Maintenance Window)에서만
vm.swappiness=0으로 설정 스왑 자체가 억제되어 zswap이 거의 활용되지 않음 zswap 활용을 위해 최소 vm.swappiness=30 이상 유지
debugfs 미마운트 상태에서 모니터링 zswap 통계를 확인할 수 없어 블라인드(Blind) 운영 mount -t debugfs debugfs /sys/kernel/debug 확인

zswap 건강 상태 체크 스크립트

#!/bin/bash
# zswap 건강 상태 종합 진단 스크립트

PARAM="/sys/module/zswap/parameters"
DEBUG="/sys/kernel/debug/zswap"

# 1. 기본 상태 확인
echo "=== zswap 기본 상태 ==="
if [ "$(cat $PARAM/enabled)" != "Y" ]; then
    echo "[WARN] zswap이 비활성화 상태입니다"
    exit 1
fi
echo "활성: Y | 압축기: $(cat $PARAM/compressor) | zpool: $(cat $PARAM/zpool)"
echo "풀 최대: $(cat $PARAM/max_pool_percent)% | threshold: $(cat $PARAM/accept_threshold_percent)%"

# 2. 배킹 스왑 확인
if [ $(swapon --show --noheadings | wc -l) -eq 0 ]; then
    echo "[CRITICAL] 배킹 스왑이 설정되지 않았습니다!"
fi

# 3. 성능 지표 계산
if [ -d "$DEBUG" ]; then
    stored=$(cat $DEBUG/stored_pages)
    pool_sz=$(cat $DEBUG/pool_total_size)
    same=$(cat $DEBUG/same_filled_pages)
    wb=$(cat $DEBUG/written_back_pages)
    r_poor=$(cat $DEBUG/reject_compress_poor)
    r_alloc=$(cat $DEBUG/reject_alloc_fail)
    r_recl=$(cat $DEBUG/reject_reclaim_fail)

    echo ""
    echo "=== 성능 지표 ==="
    if [ "$pool_sz" -gt 0 ]; then
        ratio=$((stored * 4096 * 100 / pool_sz))
        echo "압축률: ${ratio}% ($(( stored * 4 / 1024 ))MB 원본 → $(( pool_sz / 1024 / 1024 ))MB 풀)"
    fi

    total_reject=$((r_poor + r_alloc + r_recl))
    if [ $((stored + total_reject)) -gt 0 ]; then
        success_rate=$((stored * 100 / (stored + total_reject)))
        echo "저장 성공률: ${success_rate}%"
    fi

    if [ "$stored" -gt 0 ]; then
        same_pct=$((same * 100 / stored))
        echo "same-filled 비율: ${same_pct}%"
    fi

    echo ""
    echo "=== 건강 진단 ==="
    if [ "$r_alloc" -gt 1000 ]; then
        echo "[WARN] reject_alloc_fail=$r_alloc — 풀 크기 증가 또는 writeback 확인 필요"
    fi
    if [ "$r_recl" -gt 100 ]; then
        echo "[WARN] reject_reclaim_fail=$r_recl — 배킹 스왑 디바이스 성능 확인 필요"
    fi
    if [ "$wb" -gt "$stored" ] 2>/dev/null; then
        echo "[INFO] writeback > stored — writeback 부담이 높음, 풀 크기 검토"
    fi
else
    echo "[WARN] debugfs 미마운트. mount -t debugfs debugfs /sys/kernel/debug"
fi

참고자료

커널 소스

공식 문서

맨 페이지(Man Page)

Bootlin 소스 참조

다음 학습: