F2FS와 BtrFS, 압축 가능한 파일시스템 관점에서 비교

F2FS와 Btrfs는 둘 다 Linux 커널에 포함된 파일시스템이면서 투명 압축(transparent compression)을 지원한다는 공통점이 있다. 하지만 압축을 구현한 방식과 실제로 얻을 수 있는 효과는 두 파일시스템의 근본적인 설계 철학 차이 — F2FS는 NAND flash를 위한 로그 구조(log-structured) 파일시스템이고, Btrfs는 범용 copy-on-write(CoW) B-tree 파일시스템이다 — 때문에 상당히 다르다. 이 글에서는 두 파일시스템의 압축 구현을 비교하고, 같은 테스트 데이터로 직접 압축 효과를 측정한 결과까지 다룬다.

압축 지원 개요

F2FSBtrfs
도입 시점Linux 5.6 (2020)Linux 2.6.32 (2009)부터
압축 단위cluster (기본 4블록 = 16KB)extent (최대 128KB)
지원 알고리즘lzo, lz4, zstd, lzo-rlezlib, lzo, zstd
활성화 단위파일/디렉터리별 opt-in (기본 꺼짐)마운트 옵션으로 전체 기본 적용, 파일별 override 가능
비압축 데이터 처리클러스터 단위로 이득 없으면 원본 저장휴리스틱으로 비압축성 데이터 감지 시 스킵
커널 요구사항CONFIG_F2FS_FS_COMPRESSION 빌드 필요기본 Btrfs 모듈에 포함

F2FS 압축: cluster 단위, 명시적 opt-in

F2FS 압축은 기본적으로 꺼져 있고, 파일 단위로 명시적으로 켜야 한다. 방법은 두 가지다. 하나는 빈 파일이나 디렉터리에 chattr +m으로 압축 inode 플래그를 직접 붙이는 것이고(디렉터리에 붙이면 그 안에 새로 생성되는 파일에 상속된다), 다른 하나는 마운트 시 compress_extension=ext1,ext2 옵션으로 특정 확장자를 가진 파일에 자동으로 플래그가 붙게 하는 것이다. mkfs 시점에는 -O extra_attr,compression으로 압축 기능 자체를 파일시스템에 켜둬야 하고, 마운트 시 compress_algorithm=zstd 같은 옵션으로 알고리즘을 지정한다.

압축은 4블록(16KB)짜리 cluster 단위로 이뤄진다. 한 cluster를 압축했을 때 최소 1블록 이상 줄어들지 않으면(즉 이득이 그 클러스터 하나만큼도 안 되면) 압축을 포기하고 원본 그대로 저장한다. 이 cluster 단위 처리는 F2FS의 로그 구조 설계와 맞물려 있다 — GC가 세그먼트를 회수할 때도 압축된 cluster를 통째로 하나의 단위로 다루므로, 압축 여부와 무관하게 기존 append-only 쓰기/회수 모델을 그대로 재사용할 수 있다. 커널 5.16부터는 compress_mode=fs|user 옵션도 추가됐는데, 기본값인 fs는 쓰기 시점에 자동으로 압축하고, user는 데이터를 일단 그대로 쓴 뒤 애플리케이션이 ioctl로 압축을 명시적으로 트리거하게 만들어 압축 타이밍을 제어할 수 있게 한다.

여기서 실제로 검증하다가 걸린 부분을 짚고 넘어갈 필요가 있다. 이 글의 벤치마크는 WSL2 환경에서 진행했는데, mkfs.f2fs -O extra_attr,compressionchattr +m까지 정확히 설정했는데도 실제로는 압축이 전혀 일어나지 않았다. 원인은 dmesg에 그대로 찍혀 있었다.

$ dmesg | grep f2fs
F2FS-fs: compression options not supported

이 WSL2 커널은 CONFIG_F2FS_FS_COMPRESSION 자체가 빌드에서 빠져 있었다.

$ zcat /proc/config.gz | grep F2FS_FS_COMPRESSION
# CONFIG_F2FS_FS_COMPRESSION is not set

mkfs와 mount 명령 자체는 에러 없이 성공하고 압축 플래그도 lsattr로 정상적으로 확인되기 때문에, 이 로그를 직접 확인하지 않았다면 압축이 조용히 무시되고 있다는 사실을 눈치채기 어려웠을 것이다. F2FS 압축을 실제로 쓰려는 환경(특히 WSL2나 커스텀 커널을 쓰는 임베디드 보드)에서는 배포판 커널이 이 옵션을 빌드에 포함했는지 위 두 명령으로 먼저 확인하는 것이 좋다. 이 문제 때문에 이번 벤치마크에서는 F2FS의 실측 압축률을 직접 얻지 못했고, 이어지는 실측 비교는 Btrfs 결과만 다룬다.

Btrfs 압축: extent 단위, 휴리스틱 기반 자동 적용

Btrfs는 마운트 옵션 하나(compress=zstd)로 파일시스템 전체에 압축을 기본 적용한다. 파일이나 디렉터리 단위로 btrfs property set <path> compression zstd처럼 개별 설정도 가능하다. 압축은 최대 128KB 크기의 extent 단위로 이뤄지며, 쓰기 전에 앞부분 일부를 샘플링해 압축 가능성을 추정하는 휴리스틱이 있어 이미 압축된 파일(mp4, jpg 등)에 불필요하게 CPU를 쓰는 것을 어느 정도 피한다. 압축 이득이 없다고 판단되면 해당 extent는 원본 그대로 저장한다. 강제로 모든 데이터에 압축을 시도하게 하려면 compress-force=zstd를 쓰지만, 이미 압축된 데이터에는 CPU만 낭비하는 경우가 많아 권장되지 않는다.

Btrfs는 애초에 모든 쓰기가 CoW로 새 위치에 이뤄지는 구조라, 압축된 extent를 쓰는 것과 압축되지 않은 extent를 쓰는 것 사이에 구조적인 차이가 거의 없다. F2FS처럼 별도의 cluster 개념이나 GC 이동 단위를 새로 정의할 필요 없이, 그냥 “약간 작아진 extent를 쓴다”는 정도로 기존 CoW 파이프라인에 자연스럽게 편입된다.

실측: 같은 데이터셋으로 압축 효과 측정

2GB짜리 loopback 이미지에 각각 F2FS(compress_algorithm=zstd)와 Btrfs(compress=zstd)를 마운트하고, 압축 특성이 서로 다른 세 종류의 파일을 똑같이 복사했다.

  • repeated_highly_compressible.txt (18.6MB) — 같은 문장을 20만 번 반복한 극단적으로 압축이 잘 되는 텍스트
  • realistic_text_data.txt (821KB) — 실제 로그 파일, Python 소스, JSON을 이어붙인 현실적인 텍스트 데이터
  • random_incompressible.bin (10MB) — /dev/urandom으로 만든, 이론상 압축이 거의 불가능한 데이터

앞서 설명한 커널 제약 때문에 F2FS는 이 세 파일을 압축 없이 그대로 저장했다(복사 전후 사용량 차이가 원본 크기와 거의 정확히 일치했다). Btrfs는 마운트 직후(빈 상태)와 파일 복사 후의 df 사용량 차이로 실제 압축 효과를 측정했다.

원본(apparent) 크기Btrfs 실사용량(추정)비율
전체 (3개 파일 합)29.9MB10.82MB2.64배 (62.1% 절감)
텍스트 2개 파일만 (random.bin 제외)19.42MB약 0.86MB약 22.7배 (95.6% 절감)

“텍스트 2개 파일만” 행은 전체 사용량(10.82MB)에서 압축이 거의 불가능한 random_incompressible.bin의 원본 크기(10MB, 압축 안 됐다고 가정)를 뺀 값으로 역산한 추정치다. 반복 텍스트와 실제 로그/소스 텍스트를 합쳐 19.42MB였던 데이터가 압축 후 1MB도 안 되는 공간에 들어갔다는 뜻으로, Btrfs의 압축 휴리스틱이 압축 가능한 데이터는 확실히 압축하고 랜덤 데이터에는 헛수고를 하지 않는다는 것을 보여준다.

한 가지 주의할 점은, 이 압축 효과가 파일별 dustat의 블록 수에는 반영되지 않았다는 것이다. 개별 파일의 du --block-size=1 결과는 압축 여부와 무관하게 원본 크기를 4KB 단위로 반올림한 값과 거의 같게 나왔다. 반면 btrfs filesystem df가 보여주는 block group 단위의 실제 Data 사용량은 압축 효과를 정확히 반영했다. 즉 Btrfs에서 압축 효과를 파일 단위로 정밀하게 확인하려면 일반 du가 아니라 compsize 같은 전용 도구를 쓰는 것이 맞고, 전체 파일시스템 차원의 절감량을 보려면 btrfs filesystem df가 더 신뢰할 수 있는 지표다.

압축 단위 차이가 실무에 미치는 영향

F2FS의 16KB cluster와 Btrfs의 최대 128KB extent라는 단위 차이는 단순히 숫자 차이가 아니다. 압축 단위가 작을수록 파일 중간의 일부만 읽거나 덮어쓸 때 압축을 풀거나 다시 압축해야 하는 범위가 좁아져 랜덤 액세스에 유리하지만, 반복되는 패턴이 압축 단위보다 크면 압축 알고리즘이 그 반복을 활용할 기회가 줄어 압축률 자체는 다소 손해를 볼 수 있다. 반대로 Btrfs처럼 압축 단위가 크면 압축률은 유리해지지만, 그 안의 일부만 바뀌어도 CoW 특성상 전체 extent를 다시 압축해서 새로 써야 하므로 소규모 랜덤 쓰기가 잦은 워크로드에서는 오히려 쓰기 증폭이 커질 수 있다. F2FS가 모바일/임베디드처럼 상대적으로 작은 flash 저장 장치를 겨냥해 작은 압축 단위를 택하고, Btrfs가 범용 서버/데스크톱 스토리지를 겨냥해 큰 단위를 택한 것은 각자의 주 사용처를 생각하면 자연스러운 선택이다.

언제 어떤 걸 고를까

압축만 놓고 보면 Btrfs 쪽이 설정하기 쉽고(마운트 옵션 하나로 전체 적용) 커널 지원 여부를 걱정할 일도 적어 당장 결과를 확인하기 수월하다. F2FS 압축은 파일/디렉터리 단위로 세밀하게 제어할 수 있다는 장점이 있지만, 커널이 CONFIG_F2FS_FS_COMPRESSION을 포함해서 빌드됐는지부터 확인해야 하고, Android 기기나 임베디드 보드처럼 커스텀 커널을 쓰는 환경에서는 이 옵션이 실제로 켜져 있는지 직접 검증하지 않으면 이번 사례처럼 압축이 조용히 무시될 수 있다.

다만 압축 지원 여부 하나만으로 파일시스템을 고르는 것은 권하지 않는다. F2FS는 flash 장치의 물리적 제약에 맞춘 로그 구조 설계 자체가 선택 이유의 핵심이고, Btrfs는 스냅샷/체크섬/내장 RAID를 포함한 종합적인 관리 기능이 핵심이다. 압축은 두 파일시스템 모두에서 “있으면 좋은” 부가 기능에 가깝고, 실제 선택은 대상 장치(flash vs 범용 블록 디바이스)와 필요한 기능(스냅샷, RAID, 세밀한 GC 튜닝 등)을 먼저 따진 뒤 결정하는 편이 맞다.

정리

F2FS와 Btrfs는 둘 다 zstd를 포함한 여러 알고리즘으로 투명 압축을 지원하지만, 활성화 방식(opt-in vs 기본 적용)과 압축 단위(16KB cluster vs 최대 128KB extent), 그리고 커널 요구사항이 다르다. 이번 실측에서는 Btrfs가 압축 가능한 텍스트 데이터에 대해 약 22.7배(95.6% 절감)의 압축 효과를 보였고, 압축 불가능한 데이터가 섞인 전체 데이터셋 기준으로는 2.64배(62.1% 절감)를 기록했다. F2FS는 이 WSL2 환경의 커널이 압축 기능을 빌드에서 빼놓은 탓에 실측치를 얻지 못했는데, 이 자체가 F2FS 압축을 쓰려는 사람에게 남기는 실질적인 교훈이다 — 압축 관련 마운트 옵션이나 chattr 플래그가 에러 없이 받아들여졌다고 해서 실제로 압축이 적용되고 있다고 단정하지 말고, dmesg/proc/config.gz로 반드시 확인해야 한다.

답글 남기기