리눅스 커널에서 압축을 지원하는 파일시스템은?

리눅스 커널에는 파일시스템이 많지만, 그중 데이터를 투명하게 압축(transparent compression)해서 저장 공간을 아껴주는 것은 일부에 불과하다. 가장 많이 쓰이는 ext4나 XFS는 압축 기능 자체가 없고, Btrfs와 F2FS는 쓰기 가능한(writable) 파일시스템이면서 압축을 지원하며, SquashFS와 EROFS는 애초에 읽기 전용(read-only)으로 설계된 이미지 파일시스템이라 압축 방식도 다르다. 이 글에서는 v6.18 커널 소스를 기준으로 어떤 파일시스템이 어떤 알고리즘을, 어떤 단위로, 어떻게 지원하는지 정리한다.

압축을 지원하지 않는 파일시스템

ext4와 XFS는 커널 메인라인 기준으로 파일 단위 투명 압축 기능이 없다. ext4에 압축을 붙이려는 e2compr 같은 시도가 과거에 있었지만 메인라인에 병합된 적은 없고, XFS도 마찬가지로 압축을 지원하지 않는다. 이 두 파일시스템에서 저장 공간을 줄이려면 dm-crypt 같은 블록 레이어가 아니라 VDO(Virtual Data Optimizer) 같은 별도의 블록 계층을 앞에 두거나, 애초에 압축을 지원하는 파일시스템을 선택해야 한다.

쓰기 가능한 파일시스템의 압축 — Btrfs와 F2FS

Btrfs와 F2FS는 둘 다 일반적인 읽기/쓰기 워크로드를 위한 파일시스템이면서 압축을 지원한다. 다만 압축 단위와 활성화 방식은 상당히 다르다.

Btrfs

Btrfs는 fs/btrfs/super.c의 마운트 옵션 파서(btrfs_match_compress_type) 기준으로 zlib, lzo, zstd 세 가지 알고리즘을 지원한다. zlib은 레벨 1~9(기본 3), zstd는 레벨 -15~15(기본 3)까지 지정할 수 있고, lzo는 레벨 개념이 없다. 압축 단위는 extent(최대 128KB)이며, 마운트 옵션으로 지정하면 그 시점 이후 새로 쓰는 데이터에 적용된다.

# 마운트 시점에 zstd 레벨 3으로 전체 압축 활성화
mount -o compress=zstd:3 /dev/sdb1 /mnt

# 압축률이 안 좋아도 강제로 압축 시도 (compress-force)
mount -o compress-force=zstd:15 /dev/sdb1 /mnt

compress=는 커널이 파일 내용을 보고 압축 효율이 낮다고 판단하면(랜덤 데이터, 이미 압축된 파일 등) 압축을 건너뛰는 휴리스틱이 있고, compress-force=는 이 휴리스틱을 무시하고 무조건 압축을 시도한다. 파일이나 디렉터리 단위로도 압축 알고리즘을 지정할 수 있는데, fs/btrfs/props.cprop_compression_apply가 이를 처리한다.

# 특정 파일/디렉터리에만 압축 속성 지정
btrfs property set /mnt/data compression zstd

# 이미 저장된 파일을 지정한 알고리즘으로 재압축(defrag)
btrfs filesystem defrag -r -c zstd /mnt/data

마운트 옵션이나 property는 그 이후에 쓰는 데이터에만 적용되므로, 이미 있는 파일을 압축하려면 defrag -c로 명시적으로 재압축해야 한다는 점이 Btrfs 압축을 다룰 때 자주 놓치는 부분이다.

F2FS

F2FS는 lzo, lzo-rle, lz4, lz4hc, zstd 다섯 가지를 지원하며(fs/f2fs/Kconfig), 압축 단위는 cluster다. 하나의 cluster는 4 << n(n≥0) 개의 논리 페이지로 구성되고, cluster 안의 모든 블록이 유효 데이터로 채워져 있으면서 압축률이 특정 임계값 이하일 때만 압축된다. NAND flash를 겨냥한 log-structured 설계라 overwrite 시 write amplification을 줄이는 쪽으로 압축 조건이 맞춰져 있다.

F2FS의 압축은 파일시스템 전체가 아니라 파일/디렉터리 단위로 opt-in한다. 방법은 네 가지다.

  • chattr +c file — 파일 하나에 압축 플래그 지정
  • chattr +c dir; touch dir/file — 디렉터리에 지정하면 이후 생성되는 파일에 상속
  • mount -o compress_extension=extext 확장자 파일 생성
  • mount -o compress_extension=* — 모든 신규 파일에 압축 적용
# 압축 기능을 포함해서 포맷
mkfs.f2fs -O extra_attr,compression /dev/sdb1

# zstd 알고리즘, 확장자 무관 전체 압축, 체크섬 포함
mount -o compress_algorithm=zstd,compress_extension=*,compress_chksum /dev/sdb1 /mnt

chattr +c /mnt/bigfile.dat

F2FS는 compress_mode 마운트 옵션으로 압축 시점도 고를 수 있다. 기본값인 compress_mode=fs는 writeback 시점에 커널이 자동으로 압축하고, compress_mode=user는 자동 압축을 끄고 F2FS_IOC_COMPRESS_FILE/F2FS_IOC_DECOMPRESS_FILE ioctl로 사용자가 원하는 시점에 직접 압축·해제한다. 참고로 커널 빌드에 CONFIG_F2FS_FS_COMPRESSION이 빠져 있으면 mount/chattr 명령 자체는 에러 없이 성공하지만 실제로는 압축이 조용히 무시되므로, 압축이 적용됐는지는 dmesg로 확인하는 습관이 필요하다.

읽기 전용 이미지 파일시스템의 압축 — SquashFS와 EROFS

Btrfs와 F2FS가 일반 저장 장치에 마운트해 계속 쓰고 지우는 용도라면, SquashFS와 EROFS는 이미지를 한 번 만들고 나면 읽기만 하는 용도다. 압축도 마운트 이후가 아니라 이미지를 만드는 시점(mkfs)에 전체적으로 결정된다.

SquashFS

SquashFS는 zlib(기본), lz4, lzo, xz, zstd 다섯 가지 압축을 지원하며(fs/squashfs/Kconfig), 어떤 알고리즘을 쓸지는 mksquashfs로 이미지를 만드는 시점에 정해지고 마운트 후에는 바꿀 수 없다. 기본 블록 크기는 128K이고 최대 1M까지 키울 수 있으며, 압축은 파일 단위가 아니라 블록 단위로 이뤄진다.

# zstd 레벨 19로 이미지 생성
mksquashfs rootfs/ image.squashfs -comp zstd -Xcompression-level 19

# 압축률이 더 중요할 때는 xz, 블록 크기도 1M로
mksquashfs rootfs/ image.squashfs -comp xz -b 1M

디코딩 병렬성도 조절 가능하다. SQUASHFS_MOUNT_DECOMP_THREADS 커널 옵션이 켜져 있으면 마운트 시 threads= 옵션으로 병렬 디코더 개수를 지정할 수 있어, CPU 코어가 많은 시스템에서 압축 해제 병목을 줄일 수 있다. AppImage, snap 패키지, 라이브 CD/USB의 squashfs.img가 전부 이 방식이다.

EROFS

EROFS(Enhanced Read-Only File System)는 기본 알고리즘이 LZ4이고, 여기에 microLZMA(EROFS_FS_ZIP_LZMA), DEFLATE(zlib, EROFS_FS_ZIP_DEFLATE, 아직 실험적 기능), ZSTD(EROFS_FS_ZIP_ZSTD)를 옵션으로 켤 수 있다. SquashFS와 달리 “fixed-sized output compression” 방식을 쓰는데, 가변 크기 입력을 고정 크기 압축 블록(physical cluster)으로 만들어내는 구조라 압축 해제 시 page cache를 그대로 재사용하는 in-place decompression이 가능하고, 이 덕분에 메모리 압박이 심한 상황에서도 압축 해제 성능이 비교적 안정적이다.

# 기본 lz4hc로 이미지 생성 (레벨 9)
mkfs.erofs -zlz4hc,9 image.erofs rootfs/

# 압축률 우선이면 zstd
mkfs.erofs -zstd,19 image.erofs rootfs/

mount -o loop image.erofs /mnt

EROFS는 cache_strategy= 마운트 옵션으로 압축 해제 캐싱 전략도 고를 수 있다(disabled/readahead/readaround). 그리고 EROFS_FS_ZIP_ACCEL 옵션을 켜면 crypto API를 통해 DEFLATE 압축 해제를 하드웨어로 오프로드할 수도 있다. 이런 특성 때문에 EROFS는 Android 11부터 시스템 파티션 기본 포맷으로 쓰이고 있고, OCI 컨테이너 이미지(composefs 등)에서도 채택되는 추세다.

상황별로 어떤 걸 고를까

  • 범용 서버/데스크톱, 스냅샷/서브볼륨도 쓰고 싶다 → Btrfs. compress-force로 강제 압축하거나 property로 파일별 알고리즘을 다르게 줄 수 있다.
  • 모바일/임베디드 등 NAND flash 위에서 쓰기 수명을 아끼고 싶다 → F2FS. cluster 단위 압축이 flash 특성에 맞춰져 있고 compress_mode=user로 압축 시점도 제어할 수 있다.
  • 라이브 CD, AppImage, snap처럼 만들고 나면 읽기만 하는 이미지 → SquashFS. 압축 알고리즘 선택지가 넓고 생태계 지원이 오래됐다.
  • Android 시스템 파티션, 컨테이너 이미지처럼 메모리 압박 속에서도 빠른 디코딩이 중요하다 → EROFS. in-place decompression과 하드웨어 오프로드 지원이 강점이다.

실제로 압축이 적용됐는지 확인하기

mount나 mkfs 명령이 에러 없이 끝났다고 압축이 실제로 걸렸다는 보장은 없다. F2FS 항목에서 언급했듯 CONFIG_F2FS_FS_COMPRESSION이 빠진 커널에서는 chattr +c가 조용히 무시된다. 파일시스템별로 실제 적용 여부를 확인하는 방법은 다음과 같다.

  • Btrfs: btrfs property get /mnt/data compression으로 설정된 알고리즘을 확인하고, 실제 압축 후 공간 절감률은 서드파티 도구인 compsizecompsize /mnt/data 형태로 측정한다.
  • F2FS: chattr +c 직후 lsattr로 플래그가 붙었는지 확인하되, 이는 “압축 요청됨” 상태일 뿐이므로 실제 압축 여부는 dmesg | grep f2fscompression options not supported 같은 에러가 없는지로 재차 확인해야 한다.
  • SquashFS: unsquashfs -s image.squashfs로 이미지 슈퍼블록에 기록된 압축 알고리즘과 블록 크기를 바로 확인할 수 있다.
  • EROFS: erofs-utils가 제공하는 dump.erofs -s image.erofs로 압축 알고리즘별 사용 통계를 확인한다.

정리

같은 “압축 지원”이라도 파일시스템마다 압축 단위(extent vs cluster vs 고정 블록), 활성화 시점(마운트 중 vs 이미지 생성 시), 활성화 단위(파일/디렉터리 vs 이미지 전체)가 전부 다르다. 쓰기 가능한 데이터를 다룬다면 Btrfs나 F2FS처럼 파일 단위로 세밀하게 제어할 수 있는 쪽을, 한 번 만들고 배포만 하는 이미지라면 SquashFS나 EROFS처럼 이미지 생성 시점에 전체 압축률을 최적화하는 쪽을 고르면 된다. 실제로 어떤 옵션을 쓸지는 결국 커널 설정(CONFIG_F2FS_FS_COMPRESSION 같은)이 빌드에 포함돼 있는지부터 확인하는 게 먼저다.

참고

답글 남기기