Linux에서 zswap과 zram의 구조적 차이, 성능 특성, 실패 모드, 그리고 각각을 언제 사용해야 하는지를 설명합니다.
tl;dr:
확신이 서지 않는다면 zswap을 우선 사용하세요. 아주 구체적인 이유가 있을 때만 zram을 사용하세요.
아키텍처 측면에서 보면:
제 조언은 다음과 같습니다:
최근 한 독자로부터 Linux의 압축 스왑 기술에 대한 질문을 받았습니다:
Linux의 메모리 관리(스왑)에 관한 당신의 글을 읽었습니다. "32GB 있으니 꺼라" 같은 인터넷 전문가들 말 대신, 드디어 전문가의 말을 보게 되네요 :)
zswap이나 zram에 대한 후속 글이 있으면 좋겠습니다 … :) 이게 좋은지(32GB RAM 이상인 데스크톱에서), 둘 중 뭐가 더 나은지, 왜 그런지 말이죠 … 설명만 보면 작동 방식은 이해하겠는데, 실제 환경에서 측정하고 비교하기는 어렵더라고요. 이번에도 많은 "인터넷 전문가"들은 그냥 "SSD 수명 안 닳으니까 zram"이라고만 하던데요 …
우선, 제가 이 글을 쓰고 있다는 사실을 보면, 분명 아첨은 꽤 효과가 있습니다 ;-)
인터넷에는 zswap과 zram을 언제 써야 하는지, 그리고 둘 사이의 절충점이 무엇인지에 대해 혼란과 잘못된 정보가 정말 많습니다. 저는 거의 10년 동안 커널 메모리 관리와 스왑 코드 작업을 해 왔고, 이 기술들이 발전하는 과정과 그 주변에서 생겨나는 흔한 오해들을 직접 보아 왔습니다.
대부분의 사람에게 짧은 답은 이렇습니다. zswap을 쓰세요. 자신이 다루는 워크로드에 어떤 위험을 만들 수 있는지 충분히 이해하지 못했다면 zram은 쓰지 마세요. 하지만 왜 그런지 이해하려면, 또 실제로 zram이 올바른 선택이 되는 경우가 언제인지 이해하려면, 이 두 기술이 커널 내부에서 어떻게 동작하는지를 들여다봐야 합니다.
대부분의 사람들은 zswap과 zram을 단지 같은 것의 두 가지 변형, 즉 압축 스왑이라고 생각합니다. 표면적으로는 맞습니다. 둘 다 원래 디스크로 가야 할 페이지를 압축합니다. 하지만 커널이 메모리 압력을 어떻게 처리해야 하는지에 대해 두 기술은 근본적으로 다른 선택을 하고 있으며, 상황에 맞지 않는 쪽을 고르면 스왑이 전혀 없는 것보다도 상황이 더 나빠질 수 있습니다.
둘 사이의 가장 중요한 차이는 커널의 저장 계층 어디에 위치하느냐, 그리고 그에 따라 커널의 나머지 부분에 무엇을 신호로 전달할 수 있느냐에 있습니다:
다르게 말하면, zram은 딱딱한 용량 한계를 제공하는 반면, zswap은 더 빠른 스왑 계층(즉, 압축 RAM)과 더 느린 스왑 계층(즉, 디스크) 사이의 자동 계층화를 제공하며 메모리 압력이 증가할수록 자연스럽게 성능이 저하됩니다. 대부분의 사람에게는 이런 자연스러운 저하가 바라는 특성입니다. 이제 이것이 실제로 어떻게 나타나는지 봅시다.
zram은 독립형 스왑으로 동작하는 압축 블록 장치를 만듭니다. zram 위에 스왑을 설정하는 일반적인 절차는 다음과 같습니다:
modprobe zram으로 모듈을 로드해 zram 장치를 생성합니다./sys/block/zram0/comp_algorithm(사용할 압축 알고리즘)과 /sys/block/zram0/disksize(가상 장치 용량)를 설정합니다./dev/zram0에 mkswap을 수행합니다./dev/zram0에 swapon을 수행합니다.이 과정이 파티션에 스왑을 설정할 때와 거의 완전히 같아 보인다는 점을 눈치채셨을 겁니다. 이것은 우연이 아닙니다. 커널은 이를 블록 계층을 통한 또 하나의 저장 장치로 보기 때문입니다(zram_submit_bio() 참고).
이 때문에 zram은 임베디드 시스템에 자연스럽게 잘 맞습니다. 완전히 자기완결적이고, 애초에 이런 시스템에는 없을 가능성이 큰 디스크 저장소에 의존하지 않기 때문입니다. 임베디드 컨트롤러나 SD 카드가 달린 Raspberry Pi 같은 환경에서는 zram이 외부 의존성 없이 일정량의 메모리 오프로딩을 제공합니다. 여기까지는 꽤 합리적으로 보입니다.
하지만 디스크 저장소를 사용할 수 있는 경우(예를 들어 SSD가 있는 경우), zram의 블록 장치 아키텍처는 몇 가지 중요한 제약을 만들어냅니다. 커널은 본질적으로 zram을 느린 디스크 위의 일반적인 블록 장치와 다르게 인식하지 못하며, 그래서 여기에 일반적인 디스크 지향 기본값을 적용합니다. 한 가지 예만 들어도, 커널에는 vm.page-cluster라는 튜너블이 있는데, 이는 단일 스왑 페이지를 fault-in할 때 몇 개의 페이지를 미리 읽을지 결정합니다. 기본값은 3이며, 이는 커널이 한 번에 2^3 = 8페이지를 읽는다는 뜻입니다. 순차 접근일 때 저렴한 디스크 작업을 상쇄하기 위한 것입니다. 이는 하드 디스크에서 더 중요하지만, 현대 NAND에서도 랜덤 읽기와 순차 읽기 사이에는 여전히 의미 있는 성능 차이가 있습니다.
우리가 Quest에서 zram 사용 작업을 시작했을 때(Android 기반이므로 zram을 사용함), 이런 readahead 동작은 가장 먼저 부딪힌 문제 중 하나였습니다. 디스크에서는 readahead 가정이 성립합니다. 디스크상에서 서로 가까운 페이지는 시간적으로도 비슷한 시점에 필요해질 가능성이 높기 때문에 한 번에 읽는 것이 좋습니다. 하지만 zram에서는 압축 페이지에 지역성이 없으므로 가정이 반대로 뒤집힙니다. 이제는 1페이지가 필요할 때마다 필요하지도 않은 7페이지로 스왑 캐시를 오염시키게 되어, 상당히 불리하게 작용합니다.
중요한 점은 이것이 Quest에만 특화된 것도 아니고 vm.page-cluster에만 국한된 것도 아니라는 것입니다. 더 큰 문제는 커널이 zram을 다른 블록 장치와 동일하게 취급한다는 데서 비롯됩니다. vm.page-cluster는 적어도 조정할 수 있지만, 커널에는 sysctl로조차 노출되지 않은 다른 가정들도 박혀 있습니다. 많은 경우 커널은 여러분에게 유리하게 작동하지 않으며, 이를 제대로 맞추려면 상당한 노력과 지식이 필요합니다.
이에 비해 zswap은 블록 장치를 전혀 만들지 않습니다. 대신 커널의 메모리 관리 서브시스템에 직접 통합됩니다. 이 차이는 들리는 것보다 훨씬 중요합니다. zswap은 reclaim 경로 자체에 엮여 있기 때문에, 커널은 실제로 zswap 풀에 있는 페이지 중 무엇이 뜨겁고 무엇이 차가운지 알고 있습니다. zram은 그저 또 하나의 블록 장치일 뿐이므로 이런 가시성이 없습니다.
이런 인식이 자동 계층화의 기반이며, 메모리 압력이 있을 때 zswap이 zram보다 훨씬 더 잘 버티는 주된 이유입니다.
그렇다면 zswap의 계층화는 실제로 어떻게 작동할까요? 커널이 페이지를 스왑 아웃해야 할 때 swap_writeout()을 호출하고, 여기서 zswap이 먼저 가로챌 기회를 가집니다:
int swap_writeout(struct folio *folio, struct swap_iocb **swap_plug)
{
/* ... */
if (zswap_store(folio)) { /* zswap has called bagsy on the page */
count_mthp_stat(folio_order(folio), MTHP_STAT_ZSWPOUT);
goto out_unlock;
}
if (!mem_cgroup_zswap_writeback_enabled(folio_memcg(folio))) {
folio_mark_dirty(folio);
return AOP_WRITEPAGE_ACTIVATE;
}
__swap_writepage(folio, swap_plug);
return 0;
out_unlock:
folio_unlock(folio);
return ret;
}
zswap_store()가 true를 반환하면 페이지는 압축된 RAM에 저장되고 디스크에는 전혀 닿지 않습니다. zswap이 이를 거부하거나(zswap이 비활성화된 경우 포함) 그럴 때만 커널은 백엔드 스왑 장치에 쓰는 경로로 폴백합니다.
다음은 zswap_store()가 내부적으로 하는 일입니다:
bool zswap_store(struct folio *folio)
{
/* ... */
/* Check if we've hit pool size limits */
if (zswap_check_limits())
goto put_objcg;
/* Get the current compression pool */
pool = zswap_pool_current_get();
if (!pool)
goto put_objcg;
/* Try to compress and store each page in the folio */
for (index = 0; index < nr_pages; ++index) {
struct page *page = folio_page(folio, index);
if (!zswap_store_page(page, objcg, pool))
goto put_pool;
}
ret = true; /* Success! */
put_pool:
zswap_pool_put(pool);
put_objcg:
obj_cgroup_put(objcg);
/* If we failed because pool was full, queue work to shrink it */
if (!ret && zswap_pool_reached_full)
queue_work(shrink_wq, &zswap_shrink_work);
check_old:
return ret;
}
여기서 queue_work(shrink_wq, &zswap_shrink_work)라는 부분이 보일 것입니다. 이것은 zswap이 가득 찼다는 사실을 방금 알았기 때문에, 차가운 페이지 일부를 자동으로 디스크로 퇴출시키는 작업자를 큐에 넣는 역할을 합니다. 결국 shrink_worker()를 호출하게 되며, 이 함수가 해당 reclamation을 처리합니다.
메모리 관리 서브시스템의 나머지 부분과의 긴밀한 통합, 그리고 다른 mm 코드가 이 저장소의 성격을 이해하고 있다는 점이 zswap을 zram과 크게 구분 짓습니다. zswap은 SSD 스왑 앞단의 투명한 압축 계층으로 동작하며, zram처럼 별도의 저장 계층이 아닙니다. 풀이 가득 차면 shrinker를 자동으로 트리거해 차가운 페이지를 디스크로 퇴출합니다.
하지만 잠깐, Chris, 저는 제 zram 스왑 장치에 우선순위를 설정했습니다. 그럼 이게 zswap의 이런 "계층형" 아키텍처와 같은 것 아닌가요?
안타깝게도, 이 논리는 사람들이 zram 모양의 자가 함정에 걸려드는 가장 흔한 방식 중 하나입니다. 이야기는 대략 이렇게 전개됩니다:
swap_writeout()으로 들어갑니다.종이 위에서는 모두 괜찮아 보이죠? 물론입니다. 그래서 사람들이 계속 이 함정에 빠집니다. 그럼 문제는 뭘까요?
문제는 커널이 여러 장치에 걸쳐 스왑 공간을 할당하는 방식에 있습니다. 커널에는 swap_alloc_slow()라는 함수가 있는데, 이 함수가 적절한 장치와 클러스터를 찾아 쓰는 일을 담당합니다:
/* Rotate the device and switch to a new cluster */
static void swap_alloc_slow(swp_entry_t *entry, int order)
{
unsigned long offset;
struct swap_info_struct *si, *next;
spin_lock(&swap_avail_lock);
start_over:
plist_for_each_entry_safe(si, next, &swap_avail_head, avail_list) {
/* Rotate the device and switch to a new cluster */
plist_requeue(&si->avail_list, &swap_avail_head);
spin_unlock(&swap_avail_lock);
if (get_swap_device_info(si)) {
offset = cluster_alloc_swap_entry(si, order, SWAP_HAS_CACHE);
put_swap_device(si);
if (offset) {
*entry = swp_entry(si->type, offset);
return;
}
if (order)
return;
}
spin_lock(&swap_avail_lock);
/* ... continue to next device if this one is full ... */
}
}
이 코드가 기본적으로 말하는 것은 우선순위가 높은 장치를 먼저 쓴다는 것입니다. 괜찮아 보이죠? 물론 그렇게 보입니다. 그래서 사용자들이 늘 이런 식으로 설정합니다. 하지만 이런 사용자들은 업타임이 길어질수록 점점 더 가능성이 커지는 함정을 무심코 만든 것입니다.
그 함정은 이렇습니다. zram 장치의 스왑이 가장 높은 우선순위를 갖기 때문에, 커널은 모든 할당에서 zram을 선호합니다. zram이 가득 차면 그 뒤의 모든 미래 할당은 디스크 기반 스왑으로 전환됩니다.
이 말은 개입이 없다면 여러분의 소중한 zram은 그저 우연히 가장 먼저 스왑 아웃된 페이지들로 채워진다는 뜻입니다. 그리고 그것은 보통 지금 당장 필요한 페이지와는 거의 정반대로 상관됩니다.
일반적인 데스크톱 세션에서는 이런 페이지들이 대개 차가운 초기화 시점 데이터입니다. 예를 들어 방금 연 브라우저를 위한 공간을 만들기 위해 초기에 밀려난 데이터들입니다. 이런 차가운 페이지가 빠른 zram을 영구적으로 점유합니다. 한편 세션이 계속되고 메모리 압력이 지속되면, 더 새로운, 즉 잠재적으로 더 "뜨거운" 페이지들(예를 들어 지금 활발히 전환 중인 최근 브라우저 탭)은 더 낮은 우선순위 장치, 즉 느린 기계식 디스크나 SSD로 밀려나게 됩니다.
이것이 바로 _LRU inversion_이라고 부르는 현상입니다. 가장 빠른 저장 계층이 가장 차가운 데이터로 막혀 있고, 이를 퇴출할 방법이 없으며, 그 결과 작업 집합이 가장 느린 저장소로 강제로 밀려납니다. 이 경우 zram은 도움이 되지 못할 뿐 아니라, 오히려 압축 스왑이 전혀 없는 것보다도 더 나쁜 상황을 만듭니다. 더 나쁜 것은 시스템이 오래 실행될수록 상황이 더 망가진다는 점입니다. 따뜻한 페이지는 디스크로 흘러가고, 차가운 페이지는 zram 안에서 굳어지며, zram이 보유한 것과 실제로 필요한 것 사이의 격차는 계속 벌어집니다. 정말 훌륭하군요!
zram (priority) + disk swap
Fast — compressed RAM
Slow — disk swap
zswap + disk swap
Fast — compressed RAM
Slow — disk swap
Cold page (swapped out first, unlikely to be needed)
Warm page (recently swapped, more likely to be needed)
System starts. Pages are swapped out over time as memory fills. Page 1 is the first (oldest) page evicted; page 6 will be the most recently evicted.
Step 1 / 6
배치 문제를 넘어, 양쪽 모두에서 실제 오버헤드도 지불하고 있습니다. zram으로 들어가는 모든 페이지는 압축에 CPU 사이클이 듭니다. zram에 남아 있는 페이지를 접근할 때마다 이를 사용하기 전에 minor fault와 주 메모리로의 압축 해제가 필요합니다. 여러분은 실제로는 적극적으로 사용하지도 않는 데이터에 대해 압축과 압축 해제 오버헤드를 지불하고 있고, 정작 필요한 데이터는 느린 디스크 I/O를 통과하며 질질 끌리게 됩니다.
자, 몇 년 전에 이 글을 썼다면 여기서 논의가 끝났을 것입니다. :-) 하지만 커널 4.14부터 zram은 writeback을 지원하며, 이는 이 문제를 해결하려는 시도입니다.
writeback이 설정된 경우, zram은 idle 상태이거나 잘 압축되지 않는 페이지를 다시 써낼 수 있습니다. 즉, 현대의 zram 구성은 이론적으로는 계층화를 구현할 수도 있습니다. 하지만:
어쩌면 그렇게 어렵게 들리지 않을 수도 있습니다. 하지만 그렇지 않다는 점을 설득해 보겠습니다.
압축이 안 되거나 idle 상태인 페이지를 디스크로 이동시키는 zswap과 비슷한 동작을 얻으려면, 여러분은 직접 해결책을 만들어야 합니다. 한 가지 문제는 zram writeback을 스왑 파일이나 기존 스왑 파티션에 바로 연결할 수 없다는 점입니다. 대신 writeback 인터페이스는 전용의, 포맷되지 않은 블록 장치를 요구합니다. zram-generator를 사용하면 대략 이런 식입니다:
[zram0]
zram-size = ram / 2
writeback-device = /dev/sda4
또 다른 문제는 zram 백엔드용 전용 파티션을 만들기 위해 디스크를 다시 파티셔닝하거나, 루프백 장치를 관리해야 한다는 점입니다(이는 추가 오버헤드를 만듭니다). 또한 이 공간을 시스템의 하이버네이션 스왑이나 다른 데이터와 쉽게 공유할 수도 없습니다.
더 큰 장애물은 실제로 flush를 수행하는 일입니다. 장치를 연결해 두더라도 zram은 여기에 페이지를 자동으로 써내지 않으며, 어떤 데이터를 zram에서 백엔드 장치로 옮길지 결정하는 내부 휴리스틱도 커널에 없습니다. zram은 mm 서브시스템에 충분히 통합되어 있지 않기 때문에 이를 효과적으로 수행할 수 없습니다.
대신 systemd timer나 cron job을 만들어 이 flush를 수동으로 트리거해야 하는데, 이것조차 그리 쉽지 않습니다.
예를 들어 zram이 압축하지 못한 페이지를 flush하는 것은 다음처럼 간단합니다:
echo huge > /sys/block/zram0/writeback
…하지만 idle 또는 차가운 데이터를 flush하는 일은 훨씬 더 복잡합니다. zram은 그 위에 스왑이 올라가 있는지, 아니면 다른 종류의 데이터가 있는지에 무관심하기 때문에, 단순한 퇴출을 가능하게 하는 방식으로 LRU age를 본질적으로 추적하지 않습니다. 먼저 커널에게 페이지를 idle로 표시하라고 말한 다음, 그 뒤에 zram에게 이를 써내라고 말해야 합니다.
echo 3600 > /sys/block/zram0/idle # 1h
echo idle > /sys/block/zram0/writeback
이 글을 검토하던 Sam의 표현을 빌리자면, 이것은 "메모리 관리계의 IKEA"입니다. dombås wardrobe를 조립할 때는 괜찮을 수 있지만, 이게 운영 환경에서 여러분을 물어뜯기 시작하면 dombås 같은 기분이 드는 쪽은 여러분일 것입니다.
복잡성 외에도, zram은 구조적으로 zswap의 네이티브 LRU 계층화에 비해 상당히 불리합니다.
예를 들어 zram에서는 이 age 체크가 일회성 이벤트입니다. 스크립트나 타이머를 실행할 때 현재 상태의 스냅샷을 취합니다. 어떤 페이지가 5분 후에 차가워진다면, 스크립트를 다시 실행할 때까지 그 페이지는 RAM에 남아 있습니다. reclaim 프로세스나 shrinker와의 연결이 없고, 메모리 압력이 갑자기 치솟으면 스크립트를 실행하기엔 이미 늦을 수 있습니다.
반면 zswap에서는 LRU 리스트가 정상적인 메모리 reclamation 수명 주기의 일부로 평가됩니다. 메모리 압력이 올라가자마자 커널은 reclaim의 일부로 현재 페이지 목록을 보고 가장 오래된 것들을 즉시 퇴출합니다. 이를 구체적으로 말하면, zram에서는 스크립트 실행 사이에 메모리 스파이크가 발생하면 애플리케이션이 최악의 순간에 디스크 스왑으로 가게 됩니다. 반면 zswap에서는 압력이 발생하는 즉시 커널이 자연스럽게 대응합니다.
세밀도 문제도 있습니다. zram에서는 시간 기준(예: 24시간)에 따라 퇴출을 수행할 마법 같은 숫자를 추정해야 합니다. 너무 크게 잡으면 RAM을 낭비합니다. 너무 작게 잡으면 사실 보존하고 싶었던 데이터를 flush하게 됩니다. 시스템은 결국 여러분이 시키는 일만 하며, 장기간의 광범위한 프로파일링 없이는 무엇을 지시해야 효과적인지 알기 어렵습니다.
반면 zswap에는 그런 마법의 숫자가 없고, 압력에 따라 LRU를 동적으로 균형 맞춥니다. RAM이 충분하고 압력이 없으면 데이터를 무기한 유지합니다. RAM이 부족하면 데이터가 24시간 되었든 24분 되었든 가장 오래된 데이터를 공격적으로 퇴출합니다. 시스템의 나머지와 상호작용하고 협상할 수 있는 내부 관찰 능력이 있기 때문에, 더 나은 결정을 내릴 수 있습니다.
궁극적으로 zram writeback은 해결책이 아니라 우회책입니다. 어떤 학술적인 의미에서 작동하도록 만들 수 없다는 것이 아닙니다. 물론 만들 수 있습니다. 문제는 모든 지저분한 엣지 케이스가 정확히 가장 안 좋은 순간, 즉 메모리 압력이 가장 높고 정성껏 추정한 임계값이 가장 틀릴 가능성이 높은 순간에 나타난다는 것입니다. 저는 여러분이 이런 방식으로 메모리를 관리하지 않기를 강하게 권합니다.
위에서 설명했듯이, zswap의 긴밀한 mm 통합은 이런 것들을 모두 공짜로 제공해 주며, 이는 어딘가 수상할 정도로 공짜 점심 같은 느낌이 납니다. 대부분의 워크로드에서는 실제로 어느 정도 공짜 점심에 가깝지만, 알고 있어야 할 몇 가지 함정이 있습니다.
zswap의 계층화 메커니즘은 서로 혼동하기 쉬운 두 가지 별개의 shrinker 메커니즘을 통해 동작하므로, 둘 다 미리 이해할 가치가 있습니다.
첫 번째는 zswap_shrinker_count()(그리고 짝인 zswap_shrinker_scan())이며, 동적 shrinker의 일부로 존재합니다. 이는 메모리 reclaimers(예: kswapd, direct reclaimers, 그리고 Senpai 같은 proactive reclaimers)에 의해 독립적으로 트리거되며, 풀 한계 때문에 호출되는 것이 아닙니다. 그 역할은 메모리 접근 패턴, 압축 가능성, 메모리 압력을 바탕으로 zswap 풀 크기를 동적으로 조절하는 것입니다. 이상적으로는 정적 풀 한계에 아예 도달하지 않게 하는 것이 목표입니다. Meta의 운영 환경에서는 이 동적 shrinker가 미리 상황을 제어하기 때문에 정적 풀 한계에 부딪히는 경우가 드뭅니다. 랩톱처럼 메모리가 더 제한된 시스템에서는 좀 더 자주 보일 수 있습니다.
두 번째 shrinker인 shrink_worker()는 실제로 풀 한계에 도달했을 때만 작동하는 한계 기반 폴백입니다. 성능 절벽은 바로 여기 있고, 이에 대해서는 아래에서 더 설명하겠습니다.
까다로운 부분은 몇 개의 페이지를 퇴출할지 결정하는 것입니다. 너무 적게 퇴출하면 풀이 계속 차오르고, 너무 많이 퇴출하면 곧바로 다시 디스크에서 페이지를 불러오며 thrashing이 일어납니다. zswap_shrinker_count()는 이를 다음과 같이 처리합니다:
static unsigned long zswap_shrinker_count(
struct shrinker *shrinker,
struct shrink_control *sc)
{
/* zswap shrinker_count basically answers the question of
* how many pages we should evict from zswap to the
* backing swap device. */
struct lruvec *lruvec =
mem_cgroup_lruvec(sc->memcg, NODE_DATA(sc->nid));
/* This is how often we had to fetch data from slow disk
* recently. We track this to avoid thrashing. */
atomic_long_t *nr_disk_swapins =
&lruvec->zswap_lruvec_state.nr_disk_swapins;
/* ... */
/* Subtract from the lru size the number of pages that
* were recently swapped in from disk. The idea is that
* had we protected this many more pages in the zswap
* LRU from eviction, those disk swapins would not have
* happened. */
nr_disk_swapins_cur = atomic_long_read(nr_disk_swapins);
do {
if (nr_freeable >= nr_disk_swapins_cur)
nr_remain = 0;
else
nr_remain = nr_disk_swapins_cur - nr_freeable;
} while (!atomic_long_try_cmpxchg(
nr_disk_swapins, &nr_disk_swapins_cur, nr_remain));
nr_freeable -= nr_disk_swapins_cur - nr_remain;
if (!nr_freeable)
return 0;
/* Scale eviction by compression ratio. If compression is
* good (stored is small), we evict fewer pages to avoid
* wasting I/O for small gains. */
return mult_frac(nr_freeable, nr_backing, nr_stored);
}
즉, 정적 임계값이나 주기적인 폴링에 의존하는 대신 zswap은 reclaim 경로의 살아 있는 피드백에 따라 퇴출을 수행하며, 실제 디스크 swap-in 비율과 압축 비율을 추적합니다. 차가운 페이지는 압력이 생기는 순간 SSD로 빠져나갑니다. 메모리가 정말로 부족할 때 압축 풀은 몇 시간 전에 손대기를 멈춘 데이터가 아니라 활성 작업 집합을 들고 있게 되고, 가장 중요한 페이지 fault는 디스크가 아니라 빠른 압축 RAM에서 처리됩니다.
하지만 늘 그렇듯 함정은 있습니다. zswap이 max_pool_percent 한계에 도달하면 zswap_check_limits()는 zswap_store()가 페이지를 거부하고 false를 반환하게 만듭니다. 그러면 shrink_worker()가 깨어나 차가운 페이지를 디스크로 퇴출하지만, 이 작업은 비동기적으로 수행됩니다. 즉, 현재 페이지는 zswap에 저장되지 않으며 지금 당장 다른 곳으로 가야 합니다.
그 다음에 무슨 일이 일어나는지는 cgroup의 writeback 모드에 달려 있습니다:
__swap_writepage()로 흘러가 zswap 캐시를 완전히 우회하고 바로 디스크로 갑니다.이는 writeback이 활성화된 상태에서 메모리 압력이 심해지면, zswap이 경고 없이 페이지를 바로 디스크로 보내기 시작할 수 있음을 의미합니다. 그 결과 시스템은 RAM 접근에 가까울 것이라 기대한 스왑 성능에서, 갑자기 디스크 접근에 가까운 성능으로 떨어지는 성능 절벽을 맞게 됩니다. 이는 zram의 동작보다 더 나쁜 것은 아니지만, 염두에 둘 필요는 있습니다.
두 기술 모두 CPU 사이클을 I/O 감소와 맞바꿉니다. 정상 동작 시 오버헤드 프로파일은 대체로 비슷합니다. 실제로 중요한 차이는 실패 모드에 있고, 그 차이는 큽니다.
압축 스왑은 데이터가 잘 압축될 때 분명 유용합니다. 덜 분명한 것은 그렇지 않을 때 무슨 일이 일어나는지이며, 이 점에서 두 기술은 정반대의 선택을 합니다.
zswap은 압축 중 압축되지 않는 페이지를 감지하고 이를 거부하여 곧바로 디스크로 보낼 수 있습니다. 이는 RAM도 절약하고(잘 압축되지 않는 데이터를 저장하지 않으므로), CPU 사이클도 절약합니다(압축되지 않는 데이터를 반복해서 압축하려 하지 않으므로). zswap이 얼마나 자주 이렇게 했는지는 /sys/kernel/debug/zswap/의 reject_compress_poor 카운터에서 볼 수 있습니다.
반면 zram은 기본적으로 압축 비율과 상관없이 모든 것을 압축합니다. zram은 이런 저압축 페이지를 huge_pages 통계로 추적하지만, 4KB 페이지가 3.9KB로밖에 줄지 않아도 기꺼이 저장하며 메모리와 CPU를 모두 낭비합니다.
즉, 압축되지 않는 데이터가 많은 워크로드에서는 zswap이 보통 더 나은 최악의 경우 동작을 보입니다. 다만 일반적인 혼합 워크로드에서는 실제 차이가 미미한 경우가 많습니다.
압축되지 않는 페이지를 zswap이 거부했을 때의 동작도 최근 커널 버전에서 발전했습니다. 기본적으로는, 앞서 swap_writeout()에서 보셨듯이, 거부된 페이지는 __swap_writepage()로 흘러가 디스크로 갑니다. 그러나 어떤 스왑 I/O도 원치 않는 워크로드를 위해, 커널은 이제 cgroup별 writeback 비활성화 모드(kernel 6.8+)를 지원합니다. cgroup에서 이를 비활성화하면, zswap이 압축 불가, 풀 한계, 또는 그 밖의 어떤 이유로든 거부한 페이지는 디스크로 가지 않고 active list로 다시 순환합니다. 이는 따뜻하지만 압축되지 않는 페이지가 훨씬 더 차갑지만 압축 가능한 페이지보다 먼저 디스크로 가는 형태의 LRU inversion을 막아 줍니다. 활성화 방법은 다음과 같습니다:
echo 0 > /sys/fs/cgroup/<cgroup>/memory.zswap.writeback
하지만 writeback 비활성화 모드에는 단점도 있습니다. 메모리 압력이 높고 어떤 cgroup이 압축되지 않는 데이터를 많이 만들어내면, reclaimer가 병적인 루프에 빠질 수 있습니다. 즉, 동일한 압축 불가 페이지를 반복해서 압축하려 시도하고, 실패하고, active list로 되돌리고, 다시 시도하는 일이 반복됩니다. 디스크 폴백이 없기 때문에 앞으로 나아갈 방법이 없고, 이는 운영 환경에서 심각한 문제를 유발할 수 있습니다. 우리는 이런 페이지를 다시 순환시키는 대신 zswap 풀 안에 있는 그대로 보관하고, cgroup별 LRU로 정리해 차가워졌을 때 shrinker가 디스크로 퇴출할 수 있도록 하는 접근 방식을 작업 중입니다.
어떤 사람들은 zswap보다 zram을 선호해야 한다고 말하는 또 다른 이유로 SSD 마모를 줄인다는 점을 듭니다. 즉, 디스크 I/O를 줄인다고 믿는 것입니다.
하지만 이것은 어리석은 생각입니다. RAM은 유한합니다. RAM을 어떤 데이터로 채우고 있다면, 결국 RAM을 모두 사용했을 때 그 데이터는 어디론가 가야 합니다.
서버와 데스크톱 모두에서 메모리는 대부분 두 종류의 페이지가 지배합니다. 하나는 익명 페이지로, 프로그램의 힙과 스택 데이터 같은 것입니다. 다른 하나는 파일 페이지, 즉 디스크 캐시입니다. 물리적인 백엔드 장치 없이 zram을 쓰면, 사실상 모든 익명 데이터를 RAM 안에 가둬 두는 셈이 됩니다. 메모리 압력이 오면 커널은 공간을 만들기 위해 파일 캐시를 공격적으로 퇴출하는 것 말고는 선택지가 없습니다.
퇴출되는 파일 페이지가 "dirty"하다면(즉, 수정된 데이터를 포함한다면), 커널은 공간을 확보하고 앞으로 나아가기 위해 이를 SSD에 써야 합니다. "clean"하다면(즉, 수정되지 않았다면), 해당 페이지는 그냥 버려지고, 다음에 필요할 때 SSD에서 다시 읽어 와야 합니다. zswap이나 스왑 파티션을 통해 차갑고 사용하지 않는 익명 데이터를 물리 디스크로 내보내는 것을 거부하면, 페이지 캐시를 질식시키게 됩니다. 그러면 시스템은 활성 파일을 끊임없이 flush하고 다시 읽게 됩니다.
zram만 사용하면 디스크 스왑 I/O는 사라지지만, 대신 압력을 페이지 캐시로 옮겨 놓을 뿐입니다. 메모리 압력 하에서는 더 많은 파일 캐시가 버려지거나(다시 읽기 유발), dirty하다면 writeback될 수 있습니다. 디스크 백엔드 스왑(또는 zswap)을 사용하면 시스템은 대신 차가운 익명 페이지를 퇴출할 수 있고, 이는 캐시 churn을 줄여 결과적으로 I/O를 줄일 수 있습니다. 즉, zram은 적절히 관리되지 않으면 실제로 총 디스크 I/O를 증가시킬 수 있습니다.
진짜 목표는 활성 작업 집합을 RAM에 유지하는 것이며, 적절하게 사용된 디스크 스왑은 차가운 익명 페이지가 갈 곳을 제공함으로써 차갑고 뜨거운 데이터가 같은 풀을 놓고 경쟁하지 않게 해 줍니다.
실제 사례를 보여 주는 구체적인 숫자도 있습니다. 메모리 바운드 성격이 강한 Django 기반 서비스인 Instagram에서, 기존 구성(스왑 완전 비활성화)에서 디스크 스왑과 zswap 계층화가 있는 구성으로 이동하는 테스트를 했습니다. Django 워커는 생애 동안 상당한 차가운 힙 상태를 축적합니다. 예를 들어 중복 메모리를 가진 fork된 프로세스, 커지는 요청 캐시, Python 객체 오버헤드 등이 있습니다. 결과는 두 가지였습니다:
짐작하시겠지만, 이 테스트 결과로 Instagram은 그 후 수년 동안 zswap을 사용해 오고 있습니다.
이 지점에서 어떤 분들은 스왑을 추가하는 것이 어떻게 디스크 I/O를 줄일 수 있는지 의아해하실 수도 있습니다. 도대체 어떻게 디스크 기반 메모리 오프로딩을 더 추가하는 것이 I/O 감소로 이어질 수 있을까요?
RAM을 모두 쓰지 않을 것이라고 생각할 수도 있습니다. 하지만 Linux에서는 실제로 RAM을 비워 두지 않습니다. 커널은 사용되지 않는 RAM은 낭비된 RAM이라는 철학을 따르며, 남는 공간은 페이지 캐시와 그 밖의 유용한 것들, 예를 들어 파일, 라이브러리, 디스크 데이터의 복사본 같은 것으로 자동 채워 미래 접근을 빠르게 합니다.
이 과정의 일부로 커널 데몬 kswapd는 자유 공간이 특정 워터마크 아래로 떨어지면 메모리 reclamation을 위해 미리 깨어나 메모리 사용의 균형을 맞춥니다. 이상적인 경우에는 reclaim 때문에 잠들 필요 없이 즉시 할당할 수 있는 페이지를 항상 확보하고 싶기 때문에, 정상 동작 상태에서도 압력을 관리해 즉시 할당용 버퍼가 항상 있도록 합니다.
그 reclaim은 어디론가 가야 하는데, zram만 있다면 갈 수 있는 곳은 파일 캐시뿐입니다. 디스크 백엔드 스왑(또는 그 앞단의 zswap)이 있으면 커널은 선택권을 가집니다. 최근성과 접근 패턴에 따라, 익명 페이지든 파일 캐시든 더 차가운 쪽을 reclaim할 수 있습니다. 이때 발생하는 I/O는 절박해서가 아니라 의도적으로 수행된 것입니다.
물론 Instagram의 워크로드는 zswap에 특히 유리한 편이니, 정확한 숫자는 그대로 일반화하지는 마세요. 그래도 방향성 자체는 거의 모든 사용 사례에 적용됩니다. 워크로드는 일반적으로 시간이 지나며 차가운 익명 페이지를 축적하고, 그런 페이지는 대체로 잘 압축됩니다.
그뿐 아니라 zswap은 write-reduction 필터로 동작해 SSD 마모를 크게 줄이기도 합니다. 높은 빈도의 page-out/page-in 일시적 변동을 RAM에서 흡수하고, 데이터가 디스크에 닿기 전에 압축합니다. 캐시를 끝까지 살아남은 진짜 차가운 데이터만 디스크에 기록됩니다. 앞서 언급한 Instagram 사례에서 디스크 쓰기가 25% 감소한 이유는, 메모리 내 캐시가 원래 SSD까지 도달했을 뜨거운 write churn을 흡수했기 때문입니다.
현대 SSD는 보통 수백 테라바이트의 쓰기를 감당할 수 있습니다. 소비자용 SSD는 일반적으로 150-600 TB TBW를 제공합니다. 2026년의 시점에서, 아주 저렴한 eMMC 저장소를 쓰는 경우가 아니라면 이 논의는 대체로 큰 의미가 없습니다. 설령 그렇다 해도 zram이 최선의 선택은 아닐 수 있습니다.
그렇다고 해도, 특정 워크로드에서 스왑 I/O가 정말로 절대 허용되지 않는 요구 사항이라면, zswap의 cgroup별 writeback 비활성화 모드(위의 압축 불가 데이터 섹션 참고)를 사용하면 메모리 관리 서브시스템과의 zswap 통합을 포기하지 않고도 특정 cgroup에 대해 디스크 스왑 I/O를 완전히 막을 수 있습니다. 심지어 혼합 구성도 가능합니다. 지연 시간에 민감한 서비스는 writeback 비활성화된 zswap을 쓰고, 다른 서비스는 전체 zswap-후-디스크 계층화를 쓸 수 있습니다. 이는 획일적인 zram 접근보다 훨씬 유연합니다.
정상 동작 시 zswap과 zram은 비슷한 오버헤드를 가집니다. 둘이 크게 갈라지는 지점은 메모리 압력 하에서의 실패 모드이며, 어떤 것을 써야 하는지 이해하는 핵심도 여기에 있습니다.
zswap에서는 압력이 지속적이고 선제적으로 처리됩니다. 풀이 차오르면 동적 shrinker(zswap_shrinker_count)가 깨어나 미리 차가운 페이지를 디스크로 퇴출하고, 디스크 swap-in 비율과 압축 비율을 추적해 thrashing을 피합니다. 실제로는 풀 한계에 거의 도달하지 않는다는 뜻입니다. Meta의 운영 서버에서는 이 한계가 거의 발동하지 않습니다. 동적 shrinker가 그보다 훨씬 전에 상황을 제어합니다. 한계에 실제로 도달했을 때는 페이지가 캐시를 우회하고 바로 디스크로 가기 시작하는 성능 절벽이 있습니다. 좋지는 않지만, 이것은 점진적인 성능 저하입니다. 시스템은 절벽 아래로 떨어지는 대신 점점 느려집니다.
zram에는 이에 해당하는 과정이 없습니다. 장치가 차오르는 것을 지켜보며 조치하는 무언가가 없습니다. 용량에 도달하면 단순히 페이지 수용을 멈춥니다. 더 낮은 우선순위의 디스크 스왑 장치가 있다면, 커널은 거기로 흘려보내고, 앞서 설명한 LRU inversion 문제가 모두 나타납니다. 다른 장치가 없다면, 커널이 무엇이든 reclaim할 수 있는 것을 필사적으로 찾으려 하면서 시스템이 멈추거나, OOM killer가 발동합니다. 어느 경우든 시스템은 우아하게 저하되지 않습니다.
상황은 실제로 더 나쁠 수도 있습니다. 경우에 따라 OOM killer가 아예 발동하지 않을 수도 있습니다. 2026년 3월, Cloudflare의 Matt Fleming은 운영 머신에서 OOM killer가 한 번도 트리거되지 않은 채 20분에서 30분에 이르는 brownout을 보고했습니다. 원인은 zram의 블록 장치 아키텍처에서 직접 비롯됩니다. should_reclaim_retry()는 reclaim 가능한 메모리를 추정할 때 남은 스왑 슬롯 수를 확인합니다. 디스크 백엔드 스왑에서는 남은 슬롯이 있다는 것은 페이지를 추가 RAM 소비 없이 거기에 둘 수 있다는 뜻입니다. 하지만 zram은 thin-provisioned이므로, 실제로는 그 슬롯을 뒷받침할 물리 RAM이 고갈되었더라도 설정된 전체 크기를 사용 가능한 용량으로 보고합니다. 377 GiB 장치가 10% 사용 중이면 약 340 GiB의 빈 슬롯이 있다고 보고하지만, 거기에 쓰려면 시스템에 더 이상 없는 RAM이 필요합니다. should_reclaim_retry()는 계속 true를 반환하고, 커널은 direct reclaim에서 무한정 회전합니다. 심지어 OOM killer가 결국 발동하더라도, 많은 사람이 기대하는 깔끔한 탈출구는 아닙니다.
아마 이렇게 생각할 수도 있습니다. 시스템이 이미 디스크로 심하게 스왑 중이라면 반응성은 이미 망가진 것 아닌가? 천천히 thrash하며 사용자를 괴롭히느니 차라리 OOM killer가 프로세스 하나를 죽이는 편이 낫지 않나? 하지만 여기에는 자주 간과되는 위험한 미묘함이 있습니다. 커널 OOM killer는 전혀 즉각적이지 않습니다.
제가 SREcon 발표에서 설명했듯이, 반응성을 구하기 위해 커널 내장 OOM killer에 의존하는 것은 대체로 지는 싸움입니다. 커널은 사실 어떤 직접적인 의미에서든 자신이 메모리가 부족한 시점을 알지 못합니다. "메모리 부족"이라는 것은 단지 메모리가 가득 찼다는 뜻이 아니라 reclaim할 것이 아무것도 남아 있지 않다는 뜻이며, 이를 확인하는 유일한 방법은 전체 reclaim 사이클을 시도해 보고 실패하는 것입니다.
OOM killer가 호출되기 전에 커널은 공격적인 reclaim 사이클에 들어갑니다:
우리는 단순한 운영 워크로드에서도 이 과정이 몇 초, 심지어 몇 분이 걸리는 것을 자주 보아 왔습니다. 이 동안 애플리케이션은 중단되고, 시스템은 멈춘 것처럼 보입니다. OOM killer가 실제로 발동할 때쯤이면 사용자는 이미 상당한 비반응성을 경험했을 가능성이 높고, 시스템은 사용자가 거의 아무것도 할 수 없을 정도로 잠겨 있을 수도 있습니다.
커널 OOM killer는 또한 매우 부정확합니다. 누구를 죽일지 결정하기 위해 휴리스틱 기반의 "score"를 사용하는데, "score"라는 단어가 애매하게 들린다면 실제로 그렇기 때문입니다. 이는 커널도 누가 올바른 희생자인지 모르며, oom_score_adj로 그 빈틈을 여러분이 메우길 바란다는 고백에 가깝습니다. 실질적인 결과는 실제로 메모리를 누수하는 프로세스가 아니라 가장 큰 프로세스를 죽이는 경우가 많다는 것입니다. 예를 들어 Chrome이 RAM의 80%를 차지하고 있고 백그라운드 데몬 하나가 메모리 누수를 시작했다고 해 보겠습니다. OOM killer는 Chrome을 표적으로 삼고, Chrome을 죽이면 시스템은 안정화되며, 데몬은 결코 식별되지 않습니다. 다음에 누수가 생기면 Chrome이 또 죽습니다. 데몬은 계속 누수합니다.
왜 Fedora 같은 배포판은 빠른 SSD가 있는 데스크톱에서도 zram-only 구성을 기본으로 할까요? 그리고 왜 그냥 zswap을 쓰지 않을까요?
답은 zswap이 애초에 선택지에 없었다는 것입니다. Fedora의 목표는 꽤 오래전부터 디스크 스왑을 완전히 제거하는 것이었고, zswap은 구조적으로 디스크 스왑 앞단의 캐시이므로 후보가 될 수 없습니다.
디스크 스왑을 제거하려는 이유는 순전히 메모리 관리 때문만은 아니며, 다른 시스템 특성도 크게 작용합니다. 예를 들어 스왑이 페이지를 디스크로 내보내면 개인 키, 비밀번호, 세션 토큰, 브라우저 상태가 영구 파티션에 남게 됩니다. zram은 이를 완전히 피합니다. RAM 안에만 존재하고 재부팅하면 사라지므로 어떤 것도 디스크로 갈 위험이 없습니다. 스왑 암호화도 여기에는 도움이 되지만, 설정 복잡성을 더하고 여전히 키 관리 체계를 신뢰해야 하며, 결국 Fedora의 목표는 완화책을 덧씌우는 것이 아니라 공격 면 자체를 제거하는 것입니다.
Fedora는 zram을 systemd-oomd와 함께 사용합니다. systemd-oomd는 PSI를 모니터링해 정책에 따라 미리 프로세스를 종료합니다. 또 스왑 장치를 하나만(zram 위에만) 두어 LRU inversion도 피합니다. 따라서 디스크 스왑이 전혀 없으니 뒤집힐 대상 자체가 없습니다.
이 구성은 그들이 처한 제약과 systemd-oomd 기반 완화책을 고려하면 어느 정도는 말이 됩니다. 그들의 구성에서는 데스크톱 사용자가 심한 메모리 압력 하에 있다면 이미 반응성이 낮아졌을 가능성이 높고, userspace OOM 데몬이 문제가 되는 프로세스를 깔끔하게 종료하는 편이 디스크 스왑을 몇 분 동안 thrash하도록 내버려두는 것보다 나은 경우가 많습니다.
하지만, 그리고 이것이 중요합니다, 이것은 userspace OOM 데몬이 실행 중이고 제대로 설정되어 있으며, 디스크 스왑 장치가 전혀 없을 때만 작동합니다. systemd-oomd가 없으면 깔끔한 종료 없는 하드 한계만 남고, 시스템은 똑같이 혹은 더 심하게 멈춥니다.
따라서 이것이 순수하게 메모리 관리 측면에서 최적의 구성이라고 보기는 어렵습니다. zswap의 더 긴밀한 mm 통합과 LRU 계층화는 zram이 제공하지 못하는 실제 장점을 제공합니다. 하지만 Fedora가 최적화한 것은 메모리 효율만이 아니었고, 그들의 제약 중 상당수는 메모리 관리와 무관했습니다. 그런 제약 아래에서 이 결정은 일관성이 있습니다. 최적성은 언제나 무엇을 달성하려 하는지에 상대적입니다(그리고 이 점은 독자인 여러분에게도 그대로 적용됩니다. 여러분이 무엇을 하려는지 가장 잘 아는 사람은 저보다 여러분입니다).
그렇다고 해도, 앞으로 몇 년 안에 zswap이 곧 추가될 디스크 없는 모드를 얻으면 Fedora 쪽도 zswap 쪽으로 어느 정도 이동하지 않을까 생각합니다. 특히 커널 개발자들이 점점 zram 지원에서 멀어지고 있다는 점을 고려하면 더욱 그렇습니다(이에 대해서는 아래에서 더 설명합니다).
단순한 경우, zram에서는 장치 크기를 미리 추정해야 합니다. 너무 작게 잡으면 잠재력을 낭비합니다. 너무 크게 잡으면 OOM killing 위험이 있거나 불필요한 minor fault를 유발할 수 있습니다.
그렇다면 Fedora는 이를 어떻게 정할까요? 음, RAM의 100%까지 잡습니다. 깔끔하죠.
…좋습니다, 아마 조금 더 설명은 필요하겠네요. :-)
Fedora는 여기서 공격적인 접근을 취합니다. zram 장치 크기를 물리 RAM의 100%로, 다만 최대 8GB로 제한합니다. 이것이 도대체 어떻게 말이 되는지 의아하실 수 있습니다. 어떻게 RAM 전체 크기와 같은 스왑 장치를 가질 수 있죠? 그리고 zram에서 페이지를 읽으려면 메인 RAM으로 압축을 풀어야 하는데, zram이 가득 찼다면 압축 해제한 페이지는 어디에 두나요? 이게 무슨 광기인가요?!
Fedora는 여기서 어느 정도 계산된 도박을 합니다. 먼저, zram은 thin provisioned입니다. 페이지가 실제로 fault되기 전까지는 메모리를 쓰지 않습니다. 따라서 크기가 100%로 설정된 zram 장치라도 그 안에 아무것도 없으면 bookkeeping에 쓰이는 공간 외에는 아무 것도 차지하지 않습니다.
거기에 더해, 데이터가 잘 압축될 것이라고 가정합니다. 예를 들어 3:1이라고 해 보죠. 그러면 크기가 100%인 zram 장치는 물리적으로는 RAM의 3분의 1만 차지하고, 나머지 66%는 OS와 압축 해제 버퍼를 위해 남습니다.
그다음 systemd-oomd로 메모리 압력을 감시합니다. zram이 물리적으로 RAM을 채우기 시작하는 것이 보이면, 압축 해제할 공간이 부족해지는 교착 상태 벽에 닿기 전에 정책에 따라 무언가를 죽입니다.
확신이 없다면 저는 강력하게 zswap과 디스크 백엔드 스왑을 권합니다. zram보다 메모리 관리 서브시스템의 나머지와 훨씬 긴밀하게 통합되어 있고, reclaim과 퇴출에 대한 휴리스틱도 훨씬 낫고, 압축되지 않는 데이터도 훨씬 잘 처리하며, 압력 아래에서 자연스럽게 성능이 저하됩니다. 또한 하이버네이션도 투명하게 처리합니다. 디스크 기반 스왑은 RAM이 많더라도 많은 경우 여전히 중요한 장점이 있습니다.
zram이 말이 되는 경우는 더 미묘합니다. 임베디드 시스템에서 zram은 극도로 단순하고 자기완결적입니다. 디스크가 전혀 없을 때는 당연한 선택이며, 그런 환경에서는 하드 한계의 예측 가능성이 버그가 아니라 기능인 경우가 많습니다. 또 다른 경우는 Fedora처럼 설계상 의도적으로 완전히 디스크 없는 환경을 지향할 때입니다.
Android는 디스크 없는 접근의 가장 대표적인 예입니다. 수십억 대의 장치가 디스크 스왑 없이 zram만 사용하며, userspace kill daemon인 lmkd와 함께 동작합니다. 이 조합은 디스크 스왑 계층 자체가 없으므로 LRU inversion을 완전히 피합니다. 하지만 Android의 zram이 작동하는 이유는 전화기 하드웨어와 전화기 워크로드에 맞춰 광범위하게 튜닝되어 있기 때문입니다. 앞서 설명했듯이, readahead 기본값 같은 아주 기본적인 것들조차 기본 상태에서는 오히려 불리하게 작동하며, 그런 것들은 눈에 보이는 조절 가능한 값에 불과합니다. 그런 가정은 다른 환경으로 옮겨가지 않고, Android의 튜닝도 함께 따라오지 않습니다.
서버는 zram이 특히 더 설득력이 떨어지는 또 다른 장소입니다. zram이 (자연스럽게) 저하되지 않는다는 점 외에도, zram의 메모리 사용량은 사실상 커널에 불투명하며 어떤 cgroup에도 과금되지 않습니다. 커널은 특정 cgroup을 대신해 zram이 얼마나 많은 메모리를 쓰고 있는지 볼 수 없기 때문에, 서비스 간 자원 격리와 압력 신호가 깨질 수 있습니다. 이 간극만으로도 컨테이너화되었거나 격리된 워크로드를 운영하는 여러 조직에서 zram 도입을 막는 강력한 장애물이 되어 왔습니다.
심지어 임베디드와 디스크 없는 사례조차 점점 줄어들고 있습니다. 이 분야에서 일하는 우리 중 많은 사람들은 앞으로의 방향에 대해 비슷한 견해를 공유합니다. 블록 계층의 핵심 기여자 중 한 명인 Christoph는 아주 직설적으로 말했습니다:
No way. Stop adding hacks to the block layer just because you're abusing a block driver for compressed swap. Please everyone direct their energy to pluggable zswap backends and backing-store-less zswap now instead of making the zram mess even worse.
메모리 관리 유지보수자 중 한 명인 Johannes도 동의했습니다:
Compression is a memory consumer. A big one. And with swap it sits in the reclaim path. So now you have to solve intricate MM problems with the block layer in between. […] We should try to make zswap the single compressed swap implementation. It would simplify things dramatically for kernel developers working on MM and the swap subsystem. It would make things better for users too.
Christoph가 언급한 "pluggable zswap backends and backing-store-less zswap"은 zswap이 디스크 스왑 장치 없이도 동작할 수 있게 하는 현재 진행 중인 작업을 뜻합니다. 이는 디스크 없는 환경에서조차 zram의 남은 사용 사례를 닫아 버리게 됩니다. Nhat Pham이 현재 virtual swap spaces라는 이름으로 이 작업을 이끌고 있습니다. 방향성은 꽤 분명합니다.
실제로, 우리가 zswap을 대규모로 배포한 서비스 전반에서 zswap은 일관되게 OOM을 줄이고, 디스크 쓰기 압력을 낮추며, 어떤 수동 개입도 없이 그 결과를 보여 주었습니다. zram은 메모리 관리 서브시스템의 완전히 수동적인 부분입니다. 여러분이 이를 올바르게 관리할 책임을 떠안거나, 아니면 그 결과를 감수해야 합니다. 반면 zswap은 커널 자체가 관리하며, 이에 수반되는 실시간 피드백, reclaim 통합, 자동 계층화를 모두 누릴 수 있습니다. 대부분의 Linux 시스템에서 여러분이 원하는 것은 바로 커널이 zswap으로 이 일을 하도록 맡기는 것입니다.
이 글에 피드백을 준 Nhat, Javier, Sam, Johannes, 그리고 Andreas에게 감사드립니다.