Swapping 서브시스템

Linux 커널 Swapping 메커니즘: swap 공간 설정, swap cache, swap out/in 경로, swappiness 튜닝, zswap/zram 압축 스왑, 성능 모니터링 종합 가이드.

관련 페이지: 기본 메모리 관리는 메모리 관리 (기초), 고급 메모리 관리는 메모리 관리 (심화) 페이지를 참고하세요. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 메모리 관리 기초CPU 캐시 문서를 먼저 읽으세요. 메모리 서브시스템은 가상 메모리와 물리 메모리 정책이 동시에 동작하므로, 주소 변환과 회수 정책을 같이 보는 관점이 필요합니다.
일상 비유: 이 주제는 창고 적재와 재배치 운영과 비슷합니다. 빈 공간을 잘 배치해야 출고가 빨라지듯이, 페이지 배치/회수 정책이 성능과 지연을 직접 좌우합니다.

핵심 요약

  • Swap 공간 — 물리 메모리 부족 시 익명 페이지를 임시 저장하는 디스크 영역입니다.
  • Swap Cache — swap out된 페이지를 RAM에 캐싱하여, 다시 접근 시 디스크 I/O 없이 빠르게 복원합니다.
  • kswapd — 백그라운드에서 메모리 부족을 미리 감지하고 페이지를 회수하는 커널 스레드입니다.
  • swappiness — 익명 페이지(swap)와 파일 페이지(page cache) 중 어느 쪽을 먼저 회수할지 비율을 조정하는 커널 파라미터입니다.
  • zswap / zram — 디스크 대신 압축된 RAM 영역을 swap으로 사용하여 성능을 크게 개선합니다.

단계별 이해

  1. Swap의 필요성 — 익명 페이지(힙, 스택, mmap anonymous)는 backing store가 없어 swap 없이는 회수 불가능합니다.

    파일 페이지는 원본 파일에서 다시 읽을 수 있지만, 익명 페이지는 swap이 유일한 저장소입니다.

  2. Swap Out/In 과정 — kswapd가 메모리 압력을 감지하면 LRU 리스트에서 오래 사용되지 않은 페이지를 선택하여 디스크에 기록하고, 물리 페이지를 회수합니다.

    나중에 해당 페이지에 접근하면 page fault가 발생하고, 커널이 swap 영역에서 다시 읽어옵니다(swap in).

  3. Swap Cache 최적화 — swap out된 페이지가 아직 RAM에 남아 있으면, 다시 접근 시 디스크 I/O 없이 즉시 복원됩니다.

    이를 통해 반복적으로 접근되는 페이지의 swap in 비용을 크게 줄입니다.

  4. zswap/zram 압축 스왑 — 디스크 대신 압축된 메모리를 사용하여 swap 성능을 10배 이상 개선할 수 있습니다.

    zswap은 기존 swap의 캐시 역할, zram은 독립적인 블록 디바이스로 동작하며 모바일/임베디드 환경에서 필수입니다.

Swapping 서브시스템

Swapping은 물리 메모리(RAM) 부족 시 익명 페이지(anonymous page)를 디스크의 스왑 영역으로 내보내고, 다시 접근할 때 복원하는 메커니즘입니다. 파일 기반 페이지는 원본 파일에서 다시 읽을 수 있지만, 힙·스택·mmap(MAP_ANONYMOUS) 등 backing store가 없는 익명 페이지는 스왑 영역이 유일한 저장소입니다.

ℹ️

Swap vs Page Cache 회수: 커널의 메모리 회수(reclaim)는 두 가지 경로로 동작합니다. 파일 페이지(page cache)는 clean이면 즉시 버리고 dirty면 원본 파일에 writeback 후 회수합니다. 익명 페이지는 스왑 영역에 기록(swap out)해야만 회수할 수 있습니다. vm.swappiness로 이 두 경로의 비율을 조정합니다.

Swap 공간 설정

스왑 영역은 전용 파티션 또는 스왑 파일로 구성할 수 있습니다. 여러 스왑 영역을 동시에 사용할 수 있으며, 우선순위(priority)로 사용 순서를 제어합니다.

# === 스왑 파티션 설정 ===
mkswap /dev/sda2                    # 파티션을 스왑으로 포맷
swapon /dev/sda2                    # 스왑 활성화
swapon -p 10 /dev/sda2              # 우선순위 10으로 활성화

# === 스왑 파일 설정 ===
fallocate -l 4G /swapfile           # 4GB 파일 생성
chmod 600 /swapfile                 # 권한 제한 (필수)
mkswap /swapfile                    # 스왑 포맷
swapon /swapfile                    # 활성화

# === /etc/fstab 영구 설정 ===
# /dev/sda2  none  swap  sw,pri=10       0 0
# /swapfile  none  swap  sw,pri=5        0 0

# === 스왑 상태 확인 ===
swapon --show                       # 활성 스왑 영역 목록
cat /proc/swaps                     # 동일 정보 (proc 인터페이스)
free -h                             # 스왑 사용량 요약

# === 스왑 비활성화 ===
swapoff /dev/sda2                   # 스왑 인 후 비활성화 (시간 소요)
swapoff -a                          # 모든 스왑 비활성화
⚠️

스왑 파일 주의사항: Btrfs에서 스왑 파일을 사용하려면 chattr +C로 COW를 비활성화하고 별도 서브볼륨에 생성해야 합니다. ext4에서 fallocate 대신 dd를 사용해야 하는 오래된 커널(< 5.0)도 있으므로 주의하십시오. 스왑 파일은 반드시 chmod 600으로 권한을 제한해야 합니다.

우선순위(Priority) 동작 방식: 동일한 우선순위를 가진 스왑 영역들은 라운드 로빈으로 사용되어 I/O가 분산됩니다(RAID-0과 유사). 우선순위가 다르면 높은 우선순위의 영역을 먼저 사용하고, 가득 차면 낮은 우선순위로 넘어갑니다.

# 우선순위 기반 스왑 계층 구성 예시
swapon -p 100 /dev/zram0            # 1순위: zram (압축 메모리, 가장 빠름)
swapon -p 10  /dev/nvme0n1p2        # 2순위: NVMe SSD
swapon -p 1   /swapfile             # 3순위: HDD 스왑 파일 (가장 느림)

# 결과 확인
swapon --show
# NAME            TYPE       SIZE  USED PRIO
# /dev/zram0      partition  2G    0B   100
# /dev/nvme0n1p2  partition  8G    0B   10
# /swapfile       file       4G    0B   1

Swap 핵심 자료구조

스왑 서브시스템은 세 가지 핵심 자료구조로 구성됩니다.

/* include/linux/swap.h — 스왑 영역 정보 */
struct swap_info_struct {
    unsigned long        flags;          /* SWP_USED | SWP_WRITEOK 등 */
    signed short         prio;           /* 스왑 우선순위 */
    struct plist_node    list;           /* 우선순위 정렬 리스트 */
    signed char          type;           /* 스왑 영역 인덱스 (0~MAX_SWAPFILES-1) */
    unsigned int         max;            /* 최대 스왑 슬롯 수 */
    unsigned char       *swap_map;       /* 슬롯별 참조 카운트 배열 */
    struct swap_cluster_info *cluster_info; /* 클러스터별 정보 */
    struct swap_cluster_list free_clusters; /* 빈 클러스터 리스트 */
    unsigned int         lowest_bit;     /* 빈 슬롯 탐색 힌트 (시작) */
    unsigned int         highest_bit;    /* 빈 슬롯 탐색 힌트 (끝) */
    unsigned int         pages;          /* 사용 가능 총 페이지 수 */
    unsigned int         inuse_pages;    /* 사용 중인 페이지 수 */
    unsigned int         cluster_next;   /* 다음 할당 위치 힌트 */
    unsigned int         cluster_nr;     /* 현재 클러스터 내 위치 */
    struct percpu_cluster __percpu *percpu_cluster; /* Per-CPU 할당 */
    struct block_device *bdev;           /* 스왑 블록 디바이스 */
    struct file         *swap_file;      /* 스왑 파일 (파일 기반 시) */
    unsigned int         old_block_size;  /* 이전 블록 크기 */
};
/* include/linux/swapops.h — 스왑 엔트리 인코딩 */

/*
 * swp_entry_t: PTE에 저장되는 스왑 위치 정보
 * 페이지가 스왑 아웃되면, PTE의 present 비트가 0이 되고
 * 나머지 비트에 스왑 영역 인덱스(type)와 오프셋(offset)이 인코딩됩니다.
 *
 * x86_64 레이아웃 (64비트 PTE):
 * ┌──────────────────────────────────────────────────────┐
 * │ bit 63..58 │ bit 57..5        │ bit 4..1  │ bit 0   │
 * │ (unused)   │ offset (53 bits) │ type (4b) │ P=0     │
 * └──────────────────────────────────────────────────────┘
 * P=0이므로 MMU는 page fault 발생 → do_swap_page() 호출
 */
typedef struct {
    unsigned long val;
} swp_entry_t;

/* swp_entry_t 조작 매크로/함수 */
swp_type(entry)        /* 스왑 영역 인덱스 추출 (0~MAX_SWAPFILES-1) */
swp_offset(entry)      /* 스왑 영역 내 슬롯 오프셋 추출 */
swp_entry(type, off)   /* type + offset → swp_entry_t 생성 */

/* PTE ↔ swp_entry_t 변환 */
pte_to_swp_entry(pte)  /* non-present PTE → swp_entry_t */
swp_entry_to_pte(ent)  /* swp_entry_t → non-present PTE */
/* mm/swap_state.c — swap_map: 슬롯별 참조 카운트 */

/*
 * swap_map[offset] 값의 의미:
 *   0              : 빈 슬롯 (할당 가능)
 *   1~SWAP_MAP_MAX : 참조 카운트 (해당 슬롯을 참조하는 PTE 수)
 *   SWAP_MAP_BAD   : 불량 슬롯 (사용 불가)
 *   SWAP_HAS_CACHE : 스왑 캐시에 존재 (비트 OR)
 *
 * 참조 카운트가 여러 개인 경우:
 *   fork() 시 CoW로 공유된 익명 페이지가 스왑 아웃되면
 *   부모와 자식 프로세스의 PTE가 동일한 swap entry를 가리킴
 */
#define SWAP_HAS_CACHE  0x40     /* 스왑 캐시에 페이지 존재 */
#define SWAP_MAP_MAX    0x3e     /* 최대 참조 카운트 (62) */
#define SWAP_MAP_BAD    0x3f     /* 불량 슬롯 표시 */
#define SWAP_MAP_SHMEM  0x20     /* shmem/tmpfs 전용 참조 */

Swap Cache

Swap Cache는 스왑 영역과 메모리 사이의 중간 캐시 계층입니다. 페이지가 스왑 아웃/인될 때 일시적으로 스왑 캐시에 존재하며, 동일 페이지에 대한 중복 I/O를 방지하고 fork된 프로세스 간 일관성을 보장합니다.

프로세스 PTE swp_entry_t (P=0) Swap Cache address_space (swapper_spaces) XArray: offset → struct page PageSwapCache 플래그 설정됨 Swap 영역 파티션 or 파일 slot[0] slot[1] slot[2] ... 물리 페이지 struct page (RAM) lookup page swap out swap in Swap Cache 역할 1. 중복 swap I/O 방지 2. fork CoW 일관성 보장 3. swap readahead 저장소 4. swap in 완료 전 재접근 처리 SwapCached in /proc/meminfo → 이 캐시의 크기를 나타냄
/* mm/swap_state.c — Swap Cache 핵심 함수 */

/*
 * swapper_spaces[]: 스왑 영역별 address_space 배열
 * 각 address_space의 XArray에 swap offset → struct page 매핑 저장
 * 일반 파일의 page cache와 동일한 인터페이스(find_get_page 등) 사용
 */
struct address_space *swapper_spaces[MAX_SWAPFILES];

/* 페이지를 Swap Cache에 추가 (swap out 시) */
int add_to_swap_cache(struct page *page, swp_entry_t entry,
                     gfp_t gfp, void **shadowp)
{
    struct address_space *address_space = swap_address_space(entry);
    pgoff_t idx = swp_offset(entry);

    SetPageSwapCache(page);          /* PG_swapcache 플래그 설정 */
    set_page_private(page, entry.val); /* page->private에 swap entry 저장 */

    /* XArray에 page 삽입 (page cache와 동일한 방식) */
    xa_store(&address_space->i_pages, idx, page, gfp);
    ...
}

/* Swap Cache에서 페이지 검색 (swap in 시) */
struct page *lookup_swap_cache(swp_entry_t entry,
                              struct vm_area_struct *vma,
                              unsigned long addr)
{
    struct page *page;
    page = find_get_page(swap_address_space(entry),
                         swp_offset(entry));
    if (page) {
        /* Swap Cache 히트 — 디스크 I/O 없이 즉시 반환 */
        mark_page_accessed(page);
    }
    return page;
}
💡

Swap Cache vs Page Cache: Swap Cache는 사실상 Page Cache의 특수한 형태입니다. 일반 파일 페이지의 page->mapping이 파일의 address_space를 가리키듯, 스왑 캐시 페이지의 page->mappingswapper_spaces[]address_space를 가리킵니다. /proc/meminfoSwapCached 항목이 현재 스왑 캐시 크기를 나타냅니다.

페이지 Swap Out 경로

메모리 회수(reclaim) 과정에서 익명 페이지를 스왑 영역에 기록하는 전체 흐름입니다.

/*
 * Swap Out 전체 경로 (간략화):
 *
 * kswapd / direct_reclaim
 *   → shrink_node()
 *     → shrink_lruvec()
 *       → shrink_list()           ← inactive anon LRU 리스트 순회
 *         → shrink_folio_list()
 *           → add_to_swap()       ← 1. 스왑 슬롯 할당 + 스왑 캐시 등록
 *           → pageout()
 *             → swap_writepage()  ← 2. 디스크에 기록
 *           → try_to_unmap()      ← 3. 모든 PTE에서 매핑 제거
 *             → rmap walk
 *               → try_to_unmap_one() ← PTE를 swap entry로 교체
 *           → free the page       ← 4. 페이지 프레임 해제
 */

/* mm/vmscan.c — add_to_swap(): 스왑 슬롯 할당 핵심 */
bool add_to_swap(struct folio *folio)
{
    swp_entry_t entry;

    /* 1. 빈 스왑 슬롯 할당 (우선순위 기반) */
    entry = folio_alloc_swap(folio);
    if (!entry.val)
        return false;  /* 스왑 공간 부족 */

    /* 2. Swap Cache에 등록 */
    if (add_to_swap_cache(folio, entry, ...))
        return true;

    /* 실패 시 슬롯 반환 */
    put_swap_folio(folio, entry);
    return false;
}

/* mm/page_io.c — swap_writepage(): 실제 디스크 기록 */
int swap_writepage(struct page *page, struct writeback_control *wbc)
{
    /* zswap이 활성화되어 있으면 압축 저장 시도 */
    if (zswap_store(folio)) {
        count_vm_event(ZSWPOUT);
        return 0;  /* zswap에 저장 성공 → 디스크 I/O 회피 */
    }
    /* 블록 디바이스에 비동기 기록 */
    __swap_writepage(page, wbc);
    ...
}

/* mm/rmap.c — try_to_unmap_one(): PTE를 swap entry로 교체 */
static bool try_to_unmap_one(struct folio *folio,
    struct vm_area_struct *vma, unsigned long address, ...)
{
    pte_t pteval;
    swp_entry_t entry;

    /* 현재 PTE 값 읽기 및 unmap */
    pteval = ptep_clear_flush(vma, address, pvmw.pte);

    /* page->private에서 swap entry 추출 */
    entry = make_readable_migration_entry(page_to_pfn(page));
    if (PageSwapCache(page)) {
        entry.val = page_private(page);  /* swap entry */
    }

    /* PTE를 swap entry로 교체 (present=0) */
    set_pte_at(mm, address, pvmw.pte,
              swp_entry_to_pte(entry));
    ...
}

페이지 Swap In 경로

프로세스가 스왑 아웃된 페이지에 접근하면 page fault가 발생하고, do_swap_page()가 호출되어 페이지를 복원합니다.

/*
 * Swap In 전체 경로:
 *
 * CPU가 PTE 접근 → present=0 → page fault
 *   → handle_pte_fault()
 *     → 비어있지 않은 non-present PTE → do_swap_page()
 *
 * do_swap_page() 내부:
 *   1. PTE에서 swp_entry_t 추출
 *   2. Swap Cache에서 페이지 검색 (hit이면 I/O 불필요)
 *   3. Cache miss → swap_readpage()로 디스크에서 읽기
 *   4. 읽은 페이지를 Swap Cache에 등록
 *   5. PTE를 유효한 매핑으로 복원 (present=1)
 *   6. swap_map 참조 카운트 감소
 *   7. 참조 카운트가 0이면 → Swap Cache에서 제거 + 슬롯 해제
 */

/* mm/memory.c — do_swap_page() 핵심 로직 (간략화) */
vm_fault_t do_swap_page(struct vm_fault *vmf)
{
    swp_entry_t entry;
    struct page *page;
    pte_t pte;

    /* 1. PTE에서 swap entry 추출 */
    entry = pte_to_swp_entry(vmf->orig_pte);

    /* 2. Swap Cache 검색 */
    page = lookup_swap_cache(entry, vma, vmf->address);
    if (!page) {
        /* 3. Cache miss → 디스크에서 읽기 */
        page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE,
                                vmf);
        if (!page)
            return VM_FAULT_OOM;
    }

    /* 4. 페이지 잠금 및 유효성 검증 */
    lock_page(page);

    /* 5. PTE를 유효한 매핑으로 복원 */
    pte = mk_pte(page, vma->vm_page_prot);
    if (vmf->flags & FAULT_FLAG_WRITE)
        pte = maybe_mkwrite(pte_mkdirty(pte), vma);
    set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);

    /* 6. swap_map 참조 카운트 감소 */
    swap_free(entry);
    /* 참조 카운트가 0이면 Swap Cache에서도 제거됨 */

    return 0;
}

/* mm/page_io.c — swap_readpage(): 디스크에서 읽기 */
int swap_readpage(struct page *page, bool synchronous,
                 struct swap_iocb **plug)
{
    /* zswap에서 먼저 검색 (압축 저장된 경우) */
    if (zswap_load(folio)) {
        count_vm_event(ZSWPIN);
        SetPageUptodate(page);
        return 0;  /* zswap에서 복원 성공 */
    }
    /* 블록 디바이스에서 비동기/동기 읽기 */
    submit_bio(bio);
    ...
}

Swap Readahead

스왑 인 시 인접 페이지를 미리 읽어 성능을 개선합니다. 커널은 두 가지 readahead 전략을 사용합니다.

전략방식적합한 상황설정
클러스터 readahead 스왑 영역에서 물리적으로 인접한 슬롯들을 함께 읽기 순차 접근 패턴, HDD /proc/sys/vm/page-cluster (2^n 페이지, 기본=3 → 8페이지)
VMA readahead 가상 주소 공간에서 인접한 스왑 엔트리들을 함께 읽기 연속적 가상 메모리 접근, SSD 자동 (swap in 패턴 분석)
# Swap readahead 크기 설정
# page-cluster: 2^N 페이지를 한 번에 readahead
cat /proc/sys/vm/page-cluster     # 기본값: 3 (2^3 = 8 페이지 = 32KB)

# SSD에서는 줄이는 것이 유리 (랜덤 읽기 비용이 낮음)
echo 0 > /proc/sys/vm/page-cluster  # readahead 비활성화
echo 1 > /proc/sys/vm/page-cluster  # 2페이지만 readahead

# HDD에서는 높은 값이 유리 (순차 읽기가 빠름)
echo 4 > /proc/sys/vm/page-cluster  # 16페이지 readahead

Swappiness 튜닝

vm.swappiness는 커널의 메모리 회수 시 익명 페이지(swap out)파일 페이지(page cache 회수)의 상대적 비율을 조절합니다.

동작적합한 워크로드
0 가능한 한 스왑 안 함 (파일 캐시를 우선 회수, 메모리 극히 부족할 때만 스왑) 데이터베이스, 실시간 시스템
10 스왑을 최소화하되 필요 시 약간 허용 데스크톱, 일반 서버
60 기본값 — 파일 캐시와 익명 페이지를 균형 있게 회수 범용 서버
100 익명 페이지와 파일 페이지를 동일 비율로 회수 대용량 파일 캐시 유지가 중요한 경우
200 익명 페이지를 적극적으로 스왑 (cgroup v2 전용, 6.1+) 메모리 오버커밋, 컨테이너 환경
# 전역 swappiness 설정
sysctl vm.swappiness=10             # 스왑 최소화
sysctl -w vm.swappiness=60          # 기본값으로 복원

# 영구 설정 (/etc/sysctl.conf)
# vm.swappiness = 10

# cgroup v2: 그룹별 독립 swappiness 설정
echo 0 > /sys/fs/cgroup/mydb/memory.swap.max   # 해당 cgroup 스왑 금지
echo 10 > /sys/fs/cgroup/myapp/memory.swappiness # 그룹별 swappiness (커널/배포판별 지원 범위 확인) 
/* mm/vmscan.c — swappiness가 reclaim 비율에 미치는 영향 (간략화) */
static void get_scan_count(struct lruvec *lruvec,
                          struct scan_control *sc,
                          unsigned long *nr)
{
    unsigned long ap, fp;  /* anon pressure, file pressure */
    unsigned long swappiness = mem_cgroup_swappiness(memcg);

    /* swappiness가 0이면 → 메모리 극히 부족할 때만 anon 회수 */
    if (!swappiness) {
        /* 파일 캐시를 우선 회수, free가 극히 낮으면 anon도 회수 */
        fraction[0] = 0;  /* anon scan = 0 */
        fraction[1] = 1;  /* file scan = 전체 */
        return;
    }

    /*
     * anon과 file의 스캔 비율 결정:
     * ap = swappiness * (최근 anon 참조 빈도의 역수)
     * fp = (200 - swappiness) * (최근 file 참조 빈도의 역수)
     * → swappiness가 높을수록 anon 스캔 비율 증가
     */
    ap = swappiness * (total_cost + 1);
    ap /= anon_cost + 1;

    fp = (200 - swappiness) * (total_cost + 1);
    fp /= file_cost + 1;

    fraction[0] = ap;  /* anon LRU 스캔 비율 */
    fraction[1] = fp;  /* file LRU 스캔 비율 */
}
⚠️

swappiness=0은 스왑 완전 비활성이 아닙니다. vm.swappiness=0으로 설정해도, 시스템 전체 free 메모리가 zone의 high watermark + file cache보다 낮아지면 커널은 여전히 익명 페이지를 스왑 아웃합니다. 스왑을 완전히 금지하려면 swapoff -a로 스왑 영역을 비활성화하거나, cgroup v2에서 memory.swap.max=0으로 설정해야 합니다.

Multi-Gen LRU (MGLRU)

커널 6.1에 도입된 MGLRU는 기존의 active/inactive 2-리스트 LRU를 다중 세대(generation)로 확장하여, 페이지의 접근 빈도를 더 정밀하게 추적합니다. 이를 통해 스왑 아웃/페이지 회수 결정의 정확도가 크게 향상되어, 특히 메모리 부족 시 성능 저하가 줄어듭니다.

# MGLRU 활성화 상태 확인 (CONFIG_LRU_GEN 필요)
cat /sys/kernel/mm/lru_gen/enabled
# 0x0007 = 모든 기능 활성화 (Y+Y+Y)
# 비트 0: lru_gen 코어 활성화
# 비트 1: lru_gen에 의한 reclaim 활성화
# 비트 2: mm_walk(페이지 테이블 스캔)으로 세대 결정 활성화

# MGLRU 활성화/비활성화
echo 7 > /sys/kernel/mm/lru_gen/enabled   # 전체 활성화
echo 0 > /sys/kernel/mm/lru_gen/enabled   # 비활성화 (기존 LRU로 복귀)

# 세대별 페이지 분포 확인
cat /sys/kernel/mm/lru_gen/memcg_path
# memcg  nid  gen  anon_pages  file_pages  birth_time
ℹ️

MGLRU 성능 효과: Google의 벤치마크에서 MGLRU는 기존 LRU 대비 메모리 부족 워크로드에서 최대 40%의 성능 향상을 보였습니다. 특히 대규모 서버, Android, ChromeOS 등에서 메모리 압박 시 OOM 발생률이 감소하고, swap thrashing으로 인한 성능 저하가 크게 줄어듭니다.

Swap 모니터링과 디버깅

# === /proc/meminfo — 스왑 관련 항목 ===
cat /proc/meminfo | grep -i swap
# SwapCached:        10240 kB   ← Swap Cache 크기 (RAM에 캐시된 스왑 페이지)
# SwapTotal:       8388604 kB   ← 전체 스왑 공간
# SwapFree:        7340032 kB   ← 미사용 스왑 공간

# === /proc/vmstat — 스왑 I/O 통계 ===
cat /proc/vmstat | grep -E "pswp|swap"
# pswpin  123456     ← 스왑 인 된 총 페이지 수 (누적)
# pswpout 234567     ← 스왑 아웃 된 총 페이지 수 (누적)

# === vmstat 명령으로 실시간 스왑 활동 모니터링 ===
vmstat 1
#  procs ---memory--- ---swap-- -----io---- ...
#   r  b   swpd  free   si   so    bi    bo ...
#   1  0  10240 65432    0    0    12     8 ...
# swpd: 사용 중인 스왑 (KB)
# si: 초당 스왑 인 (KB/s)  — 높으면 스왑 thrashing 의심
# so: 초당 스왑 아웃 (KB/s)

# === 프로세스별 스왑 사용량 ===
cat /proc/<pid>/status | grep -i swap
# VmSwap:     1024 kB           ← 해당 프로세스의 스왑 사용량

# 스왑 사용량이 큰 프로세스 상위 10개
for f in /proc/[0-9]*/status; do
    awk '/^(Name|VmSwap)/{printf "%s ", $2}' "$f" 2>/dev/null
    echo
done | sort -k2 -n -r | head -10

# === /proc/<pid>/smaps — 상세 VMA별 스왑 정보 ===
cat /proc/<pid>/smaps | grep -A 20 "heap" | grep Swap
# Swap:                1024 kB  ← 해당 VMA의 스왑 사용량
# SwapPss:              512 kB  ← PSS 비례 스왑 (공유 시 분할)

# === ftrace로 스왑 이벤트 추적 ===
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_lru_shrink_inactive/enable
echo 1 > /sys/kernel/debug/tracing/events/swap/swap_readpage/enable
echo 1 > /sys/kernel/debug/tracing/events/swap/swap_writepage/enable
cat /sys/kernel/debug/tracing/trace_pipe
⚠️

Swap Thrashing 감지: vmstatsi/so 값이 지속적으로 높으면(수 MB/s 이상) 스왑 thrashing 상태입니다. 이는 물리 메모리가 워크로드에 비해 크게 부족하다는 신호이며, 시스템 전체 성능이 급격히 저하됩니다. 해결 방법: 메모리 증설, 불필요한 프로세스 종료, vm.swappiness 조정, zswap/zram 도입, 또는 cgroup memory.high로 throttling 적용.

Swap 관련 커널 설정 요약

설정기본값설명
CONFIG_SWAP y 스왑 서브시스템 활성화. 비활성 시 익명 페이지 회수 불가
CONFIG_SWAP_STATS y 스왑 통계 수집 (/proc/vmstat의 pswpin/pswpout)
CONFIG_ZSWAP y zswap 압축 스왑 캐시 활성화
CONFIG_ZRAM m zram 압축 블록 디바이스 (보통 모듈)
CONFIG_LRU_GEN y (6.1+) MGLRU 활성화 — 페이지 회수 정확도 향상
vm.swappiness 60 anon vs file 회수 비율 (0~200, cgroup v2)
vm.page-cluster 3 swap readahead 크기: 2^N 페이지
vm.min_free_kbytes 자동 MIN watermark 기준 — 스왑/회수 트리거에 간접 영향
vm.watermark_boost_factor 15000 단편화 방지 워터마크 부스트 팩터
MAX_SWAPFILES 32 동시에 활성화 가능한 최대 스왑 영역 수

zswap과 zram

zswap과 zram은 압축을 활용하여 스왑 성능을 크게 개선하는 메커니즘입니다. 디스크 I/O를 줄이고 스왑 영역의 실질적 용량을 확장합니다.

zswap — 압축 스왑 캐시

zswap은 스왑 아웃될 페이지를 압축하여 RAM의 동적 풀에 캐시하는 커널 기능입니다. 실제 디스크/SSD 스왑 I/O가 발생하기 전에 RAM에서 압축 저장을 시도하므로, 디스크 I/O를 극적으로 줄입니다. zswap은 기존 스왑 영역 위에서 동작하는 write-back 캐시이며, 풀이 가득 차면 가장 오래된 페이지를 실제 스왑 영역으로 writeback합니다.

메모리 회수 swap_writepage() zswap (RAM) 압축 엔진: lz4/lzo/zstd 메모리 풀: z3fold/zsmalloc max_pool_percent로 크기 제한 풀 가득 차면 → writeback same-filled page 최적화 스왑 영역 디스크/SSD swap in zswap 먼저 miss→디스크 압축 writeback
# === zswap 설정 ===
# 런타임 활성화
echo Y > /sys/module/zswap/parameters/enabled

# 압축 알고리즘 선택
echo lz4 > /sys/module/zswap/parameters/compressor
# 선택지: lzo (기본, 균형), lz4 (빠름), zstd (높은 압축률)

# 메모리 풀 할당자 선택
echo z3fold > /sys/module/zswap/parameters/zpool
# zbud:     2:1 압축 비율, 간단하고 예측 가능
# z3fold:   3:1 압축 비율, zbud보다 효율적 (권장)
# zsmalloc: 최고 압축 효율, 약간의 CPU 오버헤드

# 최대 풀 크기 (전체 RAM 대비 퍼센트)
echo 20 > /sys/module/zswap/parameters/max_pool_percent

# same-filled page 최적화 (0으로 채워진 페이지 특수 처리)
echo Y > /sys/module/zswap/parameters/same_filled_pages_enabled

# 커널 부트 파라미터 (권장 설정)
# zswap.enabled=1 zswap.compressor=lz4 zswap.zpool=z3fold zswap.max_pool_percent=25

# === zswap 상태 모니터링 ===
grep -r . /sys/kernel/debug/zswap/ 2>/dev/null
# pool_total_size:     압축 데이터가 차지하는 메모리 (바이트)
# stored_pages:        현재 저장된 페이지 수
# pool_limit_hit:      풀 크기 제한에 도달한 횟수
# reject_reclaim_fail: writeback 실패로 거절된 횟수
# reject_compress_poor: 압축 효율 낮아 거절된 횟수
# written_back_pages:  디스크로 writeback된 페이지 수
# same_filled_pages:   same-filled로 최적화된 페이지 수

# 압축 비율 계산
# 원본 크기 = stored_pages × 4096
# 압축 크기 = pool_total_size
# 압축 비율 = 원본 / 압축
💡

zswap 압축 알고리즘 선택 가이드: lz4는 압축/해제 속도가 가장 빨라 CPU 오버헤드가 적으며 대부분의 환경에서 권장됩니다. zstd는 압축률이 높아 메모리 절약이 최우선인 서버에 적합하지만 CPU 사용량이 증가합니다. lzo는 기본값으로 lz4와 유사한 성능을 보입니다. Android에서는 lz4가 표준입니다.

zram — 압축 RAM 블록 디바이스

zram은 RAM의 일부를 압축 블록 디바이스로 만들어 스왑 영역으로 사용하는 모듈입니다. zswap과 달리 독립적인 스왑 디바이스로 동작하며, 실제 디스크 스왑 영역이 없어도 사용할 수 있습니다. 디스크 없는 임베디드 시스템이나 SSD 수명을 보호하려는 환경에서 유용합니다.

# === zram 설정 ===
# 모듈 로드 (디바이스 수 지정)
modprobe zram num_devices=2

# 압축 알고리즘 설정 (디바이스 생성 전에 설정)
echo lz4 > /sys/block/zram0/comp_algorithm
# 지원 알고리즘 확인:
cat /sys/block/zram0/comp_algorithm
# lzo lzo-rle lz4 [lz4hc] zstd  (대괄호=현재 선택)

# 디스크 크기 설정 (압축 전 논리적 크기)
echo 4G > /sys/block/zram0/disksize

# 메모리 사용량 제한 (선택사항)
echo 1G > /sys/block/zram0/mem_limit  # 실제 RAM 사용 상한

# 스왑으로 활성화
mkswap /dev/zram0
swapon -p 100 /dev/zram0  # 높은 우선순위 (디스크보다 먼저 사용)

# === zram 상태 모니터링 ===
cat /sys/block/zram0/mm_stat
# orig_data_size  compr_data_size  mem_used_total  mem_limit  ...
# 4096000000      1024000000       1073741824      1073741824 ...
# ↑ 원본 크기     ↑ 압축 크기      ↑ 실제 메모리   ↑ 메모리 제한

zramctl                   # zram 디바이스 상태 요약
# NAME       ALGORITHM DISKSIZE  DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 lz4           4G  1.2G  320M  340M       4 [SWAP]

# === zram 비활성화 및 리셋 ===
swapoff /dev/zram0
echo 1 > /sys/block/zram0/reset  # 디바이스 초기화

zswap vs zram 비교

특성zswapzram
동작 방식 기존 스왑 영역의 write-back 캐시 독립적인 스왑 블록 디바이스
디스크 스왑 필요 필수 (디스크 스왑 위에서 동작) 불필요 (RAM만으로 동작 가능)
풀 가득 참 시 디스크 스왑으로 writeback 스왑 공간 부족 처리 (할당 실패)
메모리 풀 zbud/z3fold/zsmalloc (동적) 자체 메모리 할당자 (zsmalloc)
투명성 완전 투명 (기존 스왑 앞단에 삽입) 별도 스왑 디바이스로 명시적 설정
주요 용도 디스크 스왑 I/O 감소, 서버 디스크 없는 시스템, SSD 보호, Android
병용 가능 가능하지만 비권장 — 이중 압축 오버헤드 발생. 보통 하나만 선택
ℹ️

권장 구성: SSD 기반 서버에서는 zswap + SSD 스왑 조합이 효과적입니다 (디스크 I/O 감소 + writeback 안전망). 디스크 없는 임베디드/IoT에서는 zram 단독이 유일한 선택입니다. 데스크톱/Android에서는 zram이 일반적입니다. 대규모 서버 환경에서 메모리 절약이 최우선이면 zswap + zstd 조합을 고려하십시오.

압축 알고리즘 비교

알고리즘 압축률 속도 CPU 사용 권장 환경
lz4 2.0× 매우 빠름 낮음 일반 서버/데스크톱 — CPU 비용 최소화
zstd 2.5× 빠름 중간 메모리 절약 우선, 여유 CPU 있을 때
lzo 1.8× 빠름 낮음 레거시 커널 (lz4 없을 때)
lz4hc 2.3× 느림 높음 압축률 우선, CPU 여유 충분한 경우
lzo-rle 1.9× 빠름 낮음 희소(sparse) 페이지 많은 환경
💡

권장: 대부분의 경우 lz4가 최선입니다. 메모리 절약이 최우선이고 CPU 여유가 있다면 zstd를 고려하세요. ARM/저전력 장치에서는 lzo-rle도 좋은 선택입니다.

성능 벤치마크

시나리오 일반 스왑 (SSD) Zswap (lz4) Zram (lz4)
읽기 지연 (µs) 500 50 30
쓰기 지연 (µs) 1,000 80 60
처리량 (MB/s) 500 3,000 5,000
메모리 절약 없음 15~20% (RAM의 20~25%로 제한) 40~60% (전체 RAM 활용 가능)
CPU 오버헤드 없음 낮음 (~1% 단일 코어) 낮음 (~1% 단일 코어)

최적 설정 예시

# === 8GB RAM 서버 — Zswap + SSD 스왑 ===
# /etc/default/grub
GRUB_CMDLINE_LINUX="zswap.enabled=1 zswap.compressor=lz4 zswap.max_pool_percent=25 zswap.zpool=z3fold"

# swappiness (높여도 OK — RAM 내 압축이 먼저)
echo 80 > /proc/sys/vm/swappiness

# === 4GB RAM 임베디드/데스크톱 — Zram 단독 ===
modprobe zram
echo lz4 > /sys/block/zram0/comp_algorithm
echo 2G > /sys/block/zram0/disksize  # RAM의 50%
mkswap /dev/zram0
swapon /dev/zram0 -p 10
# swappiness 높여서 Zram 적극 활용
echo 100 > /proc/sys/vm/swappiness

# === systemd-zram-setup (Fedora/Ubuntu 22.04+) ===
# /etc/systemd/zram-generator.conf
# [zram0]
# zram-size = ram / 2
# compression-algorithm = lz4

모니터링

# === Zswap 모니터링 ===
grep -r "" /sys/kernel/debug/zswap/
# pool_total_size   524288000   # 풀 사용 (Bytes)
# stored_pages      128000      # 압축 저장된 페이지 수
# written_back_pages 1024       # 디스크로 writeback된 페이지
# reject_compress_poor 512      # 압축률 불량으로 거부된 페이지
# duplicate_entry   0           # 중복 엔트리

# === Zram 모니터링 ===
zramctl
# NAME       ALGORITHM DISKSIZE  DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 lz4           4G  1.2G  400M  450M       4 [SWAP]

cat /sys/block/zram0/mm_stat
# orig_data_size  compr_data_size  mem_used_total  ...
# 1073741824      357564928        367001600
# 압축률 = orig / compr = 3.0x

# /proc/meminfo 관련 항목
grep -E "Swap|Zswap" /proc/meminfo
# SwapTotal:  4194304 kB
# SwapFree:   3145728 kB
# SwapCached:    8192 kB

알려진 문제

⚠️
  • CPU 오버헤드: 압축/해제는 CPU 사이클을 소모합니다. CPU가 이미 포화 상태라면 지연이 더 커질 수 있습니다. lz4 선택 및 CPU 사용률 모니터링으로 완화 가능합니다.
  • 비압축성 데이터: 이미 압축된 데이터(JPEG·MP4·암호화 파일 등)는 압축률이 1.0배 미만으로, reject_compress_poor 카운터가 증가하며 곧장 디스크 스왑으로 넘어갑니다.
  • Zswap 풀 고갈: max_pool_percent에 도달하면 기존 항목을 디스크 스왑으로 writeback합니다. SSD가 느리다면 일시적으로 성능이 떨어질 수 있습니다.
  • Zram + Zswap 병용: 이중 압축 오버헤드가 발생하므로 비권장입니다. 하나만 선택하십시오.
  • 메모리 부족 시 Zram OOM: Zram만 사용 중이고 압축 풀이 가득 찬 경우 백업 스왑이 없어 OOM Killer가 동작할 수 있습니다. 중요 프로세스에는 oom_score_adj = -1000을 설정하십시오.

Swap 튜닝 플레이북

Swap 성능 문제는 저장장치 속도, 압축 정책, 워크로드 working set 크기가 동시에 영향을 줍니다. 단순 swappiness 변경보다 먼저 병목 위치를 분리하세요.

상황우선 점검권장 조치
응답 지연 급증vmstat si/so, iowaitzswap/zram 적용, hot set 보호
CPU 과부하압축 알고리즘 비용lz4/zstd 압축 정책 재검토
지속적 swap-out메모리 과할당메모리 상한 조정, workload 분리
# swap 상태 핵심 지표
free -h
vmstat 1 10
grep -E "Swap|Zswap" /proc/meminfo
cat /sys/module/zswap/parameters/enabled 2>/dev/null || true

Swap 클러스터 할당 심화

실제 swap I/O 성능은 빈 슬롯을 "어떻게" 찾고 묶어서 쓰는지에 크게 좌우됩니다. 최신 커널은 단일 비트 탐색보다 클러스터 단위 할당과 Per-CPU 힌트를 활용해 락 경합과 탐색 비용을 줄입니다.

/* mm/swapfile.c - 클러스터 기반 할당 개념 (간략화) */
#define SWAPFILE_CLUSTER 256   /* 256 페이지 단위 (아키텍처/설정에 따라 다를 수 있음) */

struct swap_cluster_info {
    unsigned int flags;
    unsigned int count;      /* 사용 중 슬롯 수 */
};

struct percpu_cluster {
    unsigned int index;      /* 현재 CPU가 소비 중인 클러스터 */
    unsigned int next;       /* 다음 슬롯 힌트 */
};

/* 핵심 흐름: 클러스터 우선, 실패 시 글로벌 탐색 */
swp_entry_t get_swap_page(struct folio *folio)
{
    swp_entry_t entry;

    /* 1) 현재 CPU 클러스터에서 빠른 할당 시도 */
    entry = scan_swap_map_try_ssd_cluster(si, cpu);
    if (entry.val)
        return entry;

    /* 2) free_clusters 리스트에서 새 클러스터 획득 */
    entry = alloc_swap_scan_cluster(si);
    if (entry.val)
        return entry;

    /* 3) 최후: lowest_bit~highest_bit 선형 탐색 */
    return scan_swap_map_slots(si);
}
클러스터 기반 swap 슬롯 할당 흐름 CPU N reclaim 경로 get_swap_page() Per-CPU cluster 힌트 사용 연속 슬롯 우선 할당 성공? swap_map 갱신 + entry 반환 I/O locality 개선 실패 free_clusters 글로벌 획득 경합 시 비용 증가 최후 경로: lowest_bit~highest_bit 선형 탐색 단편화/고사용률일수록 CPU 비용 증가 pool_total_size가 아니라 swap_map 밀도도 같이 봐야 함 운영 포인트 여러 swap 장치 + 우선순위 I/O 분산 효과
swap 할당은 "빈 공간 존재 여부"보다 "연속성/경합"이 성능을 더 크게 좌우합니다.

cgroup v2 Swap 제어 심화

현대 운영 환경에서 swap 정책은 전역 `vm.swappiness`보다 cgroup 단위 제어가 더 중요합니다. 컨테이너별 상한과 보호 정책으로 swap thrashing 전파를 차단할 수 있습니다.

파일의미실전 활용
memory.swap.max해당 cgroup swap 사용 상한0이면 swap 금지, 장애 전파 차단
memory.high소프트 상한 (초과 시 reclaim/스로틀)OOM 전에 완충 구간 형성
memory.max하드 상한초과 시 강제 회수/킬
memory.eventshigh/max/oom 이벤트 카운터자동 스케일링/알람 트리거
memory.pressurePSI 지표stall 기반 조기 경보
# cgroup v2 기준 예시
CG=/sys/fs/cgroup/workloads/api
mkdir -p $CG

# 메모리 8GB, swap 2GB 상한
echo $((8*1024*1024*1024)) > $CG/memory.max
echo $((2*1024*1024*1024)) > $CG/memory.swap.max

# high를 먼저 설정해 스로틀+완충
echo $((7*1024*1024*1024)) > $CG/memory.high

# 이벤트/압력 모니터링
watch -n 1 "cat $CG/memory.events; echo; cat $CG/memory.pressure"
운영 원칙: latency 민감 워크로드는 memory.swap.max=0, 배치/비동기 워크로드는 제한된 swap 허용이 안정적입니다. 같은 호스트에서 두 유형을 분리하지 않으면 swap 지연이 서비스 전반으로 전파됩니다.

Swap 지연시간 분해

swap fault 한 번의 비용은 단일 값이 아닙니다. lock 대기, 압축 해제, 블록 I/O, 페이지 테이블 복원 비용이 합산됩니다. 병목 구간을 분리 측정해야 튜닝이 정확해집니다.

Swap Fault 지연시간 분해 (예시) PTE walk Swap Cache lookup zswap hit zswap miss 시 디스크 read I/O PTE restore zswap hit 경로: 보통 수십~수백 us CPU 비용 중심 (압축 해제 + 매핑 복원) 디스크 경로: 수 ms ~ 수십 ms 스토리지 큐/혼잡도 영향 큼 계측 체크리스트 1) pswpin/pswpout 증가율과 memory.pressure(PSI) 동시 확인 2) zswap hit/miss 비율과 written_back_pages로 디스크 경로 비중 파악 3) iowait가 높고 si가 지속되면 swap 정책보다 저장장치 병목이 우선 원인
튜닝 우선순위는 항상 "디스크 경로 비중"을 먼저 파악한 뒤 결정해야 합니다.

swap-subsystem과 zswap 페이지 중복 검토

두 문서를 비교하면 아래 주제가 중복됩니다. 중복 자체는 탐색 편의에는 유리하지만, 심화 관점에서는 역할 분리가 더 명확해야 합니다.

실무에서는 먼저 이 페이지에서 시스템 정책을 고정하고, zswap 세부 튜닝은 zswap 심화 페이지에서 별도로 최적화하는 순서가 가장 안정적입니다.

주제이 페이지 (swap-subsystem)zswap 페이지중복 처리 원칙
회수 정책주관: LRU/kswapd/direct reclaim, swappiness, PSI참조만 유지정책 결정은 이 페이지에서 1차 고정
zswap 내부개요만 유지주관: zpool, reject, writeback, shrinker내부 구조 설명은 zswap으로 단일화
튜닝 순서주관: 시스템 한계선과 보호 규칙주관: 파라미터 미세 조정정책 이후 파라미터 조정 순서 고정
모니터링 지표PSI, vmstat, cgroup 이벤트zswap debugfs, reject/writeback 지표대시보드 계층 분리(시스템/기능)
장애 대응주관: 시스템 스로틀/격리주관: zswap 병목 원인 분기공통 증상이라도 진단 관문을 분리
운영 문서 분업: 정책 계층과 기능 계층 분리 swap-subsystem (정책 계층) - reclaim 경로, swappiness, memcg, PSI - 서비스 유형별 swap 허용/차단 규칙 - 장애 시 전체 호스트 안정성 우선 - 측정: vmstat, memory.pressure, iowait - 산출물: 운영 기준선(baseline) zswap (기능 계층) - zpool/압축 알고리즘/accept_threshold - reject 카운터 해석과 writeback 제어 - 디버깅: zswap debugfs/ftrace - 측정: stored/written_back/reject_* - 산출물: 파라미터 세트 정책 확정 후 파라미터 조정 읽기 순서: swap-subsystem 정책 확정 → zswap 파라미터 최적화 → 다시 PSI/iowait로 전체 검증
중복 최소화 규칙: 문서를 갱신할 때 동일 주제를 두 페이지에 모두 확장하지 말고, 한 페이지를 원문으로 지정한 뒤 다른 페이지는 3~5줄 요약과 링크로 유지하십시오.

Swapping 서브시스템과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.