F2FS 파일시스템

F2FS(Flash-Friendly File System)는 삼성전자가 NAND flash 기반 저장 장치를 위해 설계해 2012년 공개하고 Linux 3.8부터 메인라인에 포함된 파일시스템이다. eMMC, SD 카드, SSD처럼 FTL(Flash Translation Layer)을 내장한 flash 장치의 물리적 특성을 고려해 설계됐고, 현재 Android 기기의 /data 파티션이나 임베디드 리눅스 장비에서 널리 쓰인다. 이 글에서는 F2FS가 왜 필요했는지, 내부적으로 어떻게 동작하는지, 그리고 실제로 파일시스템을 만들고 마운트하는 절차까지 다룬다.

기존 파일시스템이 flash에서 겪는 문제

ext4 같은 전통적인 파일시스템은 HDD를 염두에 두고 설계됐다. HDD는 임의의 섹터를 곧바로 덮어쓸 수 있지만, NAND flash는 페이지(page) 단위로만 쓰기가 가능하고 삭제는 그보다 훨씬 큰 블록(block) 단위로만 가능하다. 이미 데이터가 있는 페이지를 다시 쓰려면 블록 전체를 지운 뒤 다시 써야 한다. 이 간극을 메우기 위해 SSD/eMMC 내부의 FTL이 논리 주소를 물리 페이지에 매핑하고, 오래된 페이지를 무효화한 뒤 백그라운드에서 가비지 컬렉션(GC)으로 회수하는 역할을 담당한다.

문제는 ext4의 메타데이터 갱신 패턴이 FTL의 GC와 상성이 나쁘다는 점이다. ext4는 파일 하나를 수정해도 inode, indirect block, bitmap 등 트리 구조를 따라 상위 메타데이터까지 연쇄적으로 갱신해야 하는 경우가 있는데, 이를 “wandering tree problem”이라고 부른다. 갱신되는 블록의 물리적 위치가 매번 흩어지면서 FTL 입장에서는 유효 데이터와 무효 데이터가 뒤섞인 블록이 늘어나고, GC 비용이 커져 쓰기 성능과 장치 수명(내구도) 모두에 불리해진다. F2FS는 이 문제를 파일시스템 계층에서부터 flash 친화적인 방식으로 풀기 위해 나온 결과물이다.

로그 구조 파일시스템으로서의 F2FS

F2FS는 LFS(Log-structured File System)를 기반으로 한다. 기존 데이터를 제자리에서 덮어쓰지 않고, 변경된 데이터를 항상 로그의 끝에 순차적으로 추가(append-only)하는 방식이다. 이 방식은 flash의 “지우고 나서 써야 한다”는 제약과 잘 맞아떨어진다. 순차 쓰기 위주이기 때문에 랜덤 쓰기를 줄여 FTL의 GC 부담을 낮추고, 결과적으로 쓰기 증폭(write amplification)을 억제하는 효과가 있다.

다만 순수 LFS는 시간이 지날수록 유효 블록과 무효 블록이 뒤섞여 자체적인 GC 비용이 커지는 단점이 있다. F2FS는 이를 완화하기 위해 multi-head logging을 도입했다. 데이터를 갱신 빈도에 따라 hot/warm/cold 세 등급으로 나누고, 각각을 별도의 로그(로그 헤드)에 기록한다. 예를 들어 디렉터리 엔트리처럼 자주 바뀌는 데이터는 hot 로그에, 한번 쓰고 잘 안 바뀌는 데이터는 cold 로그에 모은다. 변경 빈도가 비슷한 데이터끼리 물리적으로 뭉쳐 있으면 나중에 무효화되는 시점도 비슷해지므로, GC가 한 번에 회수할 수 있는 유효 데이터 비율이 높아진다.

On-disk 레이아웃

F2FS는 저장 공간을 6개의 영역으로 나눈다.

  • Superblock (SB) — 파티션 맨 앞에 위치하며 기본 파일시스템 정보(블록 크기, 섹션/존 크기 등)를 담는다. 손상에 대비해 두 벌을 유지한다.
  • Checkpoint (CP) — 파일시스템을 특정 시점의 일관된 상태로 되돌리기 위한 체크포인트 정보를 저장한다. 이 역시 이중화되어 있다.
  • Segment Information Table (SIT) — 각 세그먼트에 들어있는 유효 블록 수와 bitmap을 기록한다. GC 대상 선정에 사용된다.
  • Node Address Table (NAT) — inode와 indirect node 블록의 물리 주소를 담는 테이블이다.
  • Segment Summary Area (SSA) — Main area에 있는 각 블록이 어떤 파일의 몇 번째 블록인지 역참조 정보를 담는다. GC 시 유효 블록을 이동할 때 이 정보로 원래 소유자를 찾는다.
  • Main area — 실제 데이터와 노드(inode, indirect node) 블록이 저장되는 공간이다. 2MB 크기의 세그먼트가 모여 섹션을, 섹션이 모여 존(zone)을 구성한다.

세그먼트/섹션/존 크기는 mkfs 시점에 하위 flash 장치의 소거 단위(erase unit)에 맞춰 정렬할 수 있고, 이렇게 정렬하면 파일시스템의 쓰기 단위와 장치의 물리적 소거 단위가 어긋나지 않아 불필요한 GC를 줄일 수 있다.

체크포인트와 복구

F2FS는 저널링 대신 체크포인트 기반으로 일관성을 보장한다. 주기적으로(또는 sync, 언마운트 시) 그 순간까지의 NAT, SIT, 열려있는 로그의 현재 쓰기 위치 등을 CP 영역에 원자적으로 기록한다. 갑작스러운 전원 손실이 발생해도 파일시스템은 마지막 체크포인트 시점으로 롤백해 항상 일관된 상태를 보장한다.

체크포인트 사이에 발생한 쓰기가 통째로 사라지는 것을 막기 위해 roll-forward recovery 메커니즘도 갖추고 있다. fsync가 호출된 파일에 한해서는 노드 블록에 다음 블록의 위치를 가리키는 정보를 남겨두는데, 마운트 시 마지막 체크포인트 이후에 fsync된 블록들이 있는지 이 체인을 따라가며 찾아내 복구한다. 즉 체크포인트 주기와 무관하게 fsync를 호출한 데이터는 유실되지 않는다.

가비지 컬렉션(GC)

append-only 쓰기 방식의 대가로 F2FS 자신도 GC가 필요하다. 유휴 세그먼트가 부족해지면 무효 블록 비율이 높은 세그먼트를 회수 대상(victim)으로 골라, 그 안에 남아있는 유효 블록만 다른 곳으로 옮겨 쓰고 세그먼트 전체를 비운다. victim 선정 정책은 두 가지다.

  • Greedy — 유효 블록 수가 가장 적은 세그먼트를 우선 회수한다. 회수 비용이 가장 낮다.
  • Cost-benefit — 유효 블록 수뿐 아니라 마지막으로 수정된 시점(age)까지 고려해 오래 방치된 세그먼트를 우선한다. 배경 GC에서 주로 쓰인다.

GC는 foreground(공간이 급하게 부족할 때 I/O 경로에서 동기적으로 수행)와 background(유휴 시간에 커널 스레드가 비동기로 수행) 두 방식으로 동작하며, background_gc 마운트 옵션으로 배경 GC의 동작 방식을 조절할 수 있다.

f2fs-tools로 파일시스템 만들어보기

Ubuntu에서는 f2fs-tools 패키지로 관련 유틸리티를 설치한다.

$ sudo apt install f2fs-tools

이 패키지에는 mkfs.f2fs, fsck.f2fs, dump.f2fs, resize.f2fs, defrag.f2fs 등이 포함된다. 테스트용 loopback 이미지로 실습해본다.

$ dd if=/dev/zero of=f2fs.img bs=1M count=512
$ mkfs.f2fs -l mytest f2fs.img

mkfs.f2fs 실행 결과에는 세그먼트 개수, 섹션당 세그먼트 수, 존당 섹션 수 등 앞서 설명한 on-disk 레이아웃 정보가 그대로 출력된다. 이제 마운트한다.

$ mkdir -p /mnt/f2fs
$ sudo mount -o loop f2fs.img /mnt/f2fs
$ df -T /mnt/f2fs

현재 상태를 점검하고 싶다면 fsck.f2fs를 dry-run으로 돌려볼 수 있다.

$ sudo umount /mnt/f2fs
$ fsck.f2fs -n f2fs.img

알아두면 유용한 마운트 옵션

  • background_gc=on|off|sync — 배경 GC 동작 여부와 방식을 지정한다. 기본값은 on이다.
  • discard — 무효화된 블록에 대해 TRIM 명령을 내려 FTL에게 즉시 알려준다. SSD/eMMC에서 GC 효율을 높인다.
  • mode=adaptive|lfsadaptive는 상황에 따라 제자리 갱신에 가까운 최적화를 허용하고, lfs는 순수 append-only 정책을 강제한다.
  • extent_cache — 연속된 논리-물리 블록 매핑을 캐시해 읽기 성능을 높인다. 기본으로 켜져 있다.
  • compress_algorithm=lzo|lz4|zstd — 파일 단위 투명 압축을 지원한다(F2FS 5.x 이후 커널).
  • checkpoint=disable — 체크포인트 갱신을 멈추고 마지막 체크포인트로 계속 롤백되게 만든다. 팩토리 리셋이나 스냅샷 테스트 용도로 쓰이며, 일반 운영 환경에서는 데이터 유실 위험이 있으므로 주의해야 한다.

메타데이터 오버헤드와 볼륨 크기

앞서 본 SB/CP/SIT/NAT/SSA 같은 고정 메타데이터 영역과, mkfs 시점에 예약되는 overprovision 영역은 둘 다 사용자가 실제로 쓸 수 있는 용량을 깎아먹는다. 그런데 이 오버헤드가 차지하는 비율은 볼륨 크기에 따라 크게 달라진다. 말로 설명하기보다 직접 측정해보는 게 정확하다. truncate로 만든 sparse 이미지에 mkfs.f2fs를 기본 옵션으로 실행한 뒤 dump.f2fs -d 1로 슈퍼블록/체크포인트 필드를 뽑아 실제 세그먼트 수와 사용 가능 블록 수를 비교했다.

$ truncate -s 1G img_1g
$ mkfs.f2fs -f img_1g
$ dump.f2fs -d 1 img_1g | grep -E "block_count|segment_count|overprov_segment_count"

64MB부터 1TB까지 볼륨 크기를 바꿔가며 반복 측정해 절대 크기로 정리하면 다음과 같다. 세그먼트는 크기에 관계없이 항상 2MB 고정이므로, “고정 메타데이터”(CP+SIT+NAT+SSA 세그먼트 수 × 2MB)와 “Overprovision”(mkfs가 기본으로 예약하는 세그먼트 수 × 2MB)을 각각 실제 MB/GB 단위로 환산했다. “실사용 가능 용량”은 이 둘을 뺀, 파일 데이터를 실제로 담을 수 있는 용량이다.

볼륨 크기고정 메타데이터Overprovision실사용 가능 용량오버헤드 비율
64MB14MB20MB28MB56.25%
128MB14MB26MB86MB32.81%
256MB14MB36MB204MB20.31%
512MB14MB50MB446MB12.89%
1GB18MB54MB950MB7.23%
4GB36MB100MB3.87GB3.37%
16GB112MB190MB15.70GB1.86%
64GB372MB372MB63.27GB1.14%
256GB756MB732MB254.54GB0.57%
1TB2.24GB1.43GB1020.33GB0.36%

“고정 메타데이터” 열을 보면 64MB~512MB 구간에서는 14MB로 완전히 동일하다. CP/SIT는 볼륨 크기와 무관하게 최소 2세그먼트씩 고정 할당되고, 이 구간에서는 NAT/SSA도 최소치(각 2세그먼트, 1세그먼트)에 머물러 있기 때문이다. 반면 “Overprovision” 열은 작은 볼륨에서도 절대 크기 자체는 크지 않지만(64MB에 20MB), 볼륨 대비 비중이 워낙 커서(64MB 중 20MB는 31%) 오버헤드 비율을 밀어올리는 주범이 된다. 볼륨이 1GB를 넘어가면 NAT/SSA가 세그먼트 수·inode 수에 비례해 실제로 커지기 시작하지만, 이때는 이미 볼륨 자체가 훨씬 커진 뒤라 오버헤드 비율은 계속 낮게 유지된다.

64MB 볼륨은 mkfs.f2fs 실행 로그에 Overprovision ratio = 55.000%가 그대로 찍힐 만큼 극단적이다. SIT/NAT/SSA는 세그먼트 수, inode 수 같은 볼륨 규모에 비례해 커지긴 하지만 최소 보장 크기(예: 위 표에서 CP/SIT는 아무리 작은 볼륨이라도 최소 2세그먼트)가 있고, 여기에 mkfs가 기본으로 계산하는 overprovision 비율 자체가 작은 볼륨일수록 더 높게 잡힌다. 두 요인이 겹치면서 수백 MB 이하 볼륨에서는 전체 용량의 20~50%가 메타데이터/예약 공간으로 사라지고, 볼륨이 커질수록 이 비율은 빠르게 줄어 수십 GB 이상에서는 1% 안팎으로 수렴한다.

실무적으로는, F2FS를 스마트폰 /data 파티션이나 수십 GB급 eMMC처럼 큰 볼륨에 쓸 때는 이 오버헤드를 신경 쓸 필요가 거의 없다. 하지만 수백 MB 크기의 SD 카드 파티션이나 소용량 임베디드 스토리지에 F2FS를 올릴 계획이라면 기본 overprovision 비율 때문에 체감 용량이 예상보다 크게 줄어들 수 있다. 이런 경우 mkfs.f2fs -o <percentage> 옵션으로 overprovision 비율을 직접 낮춰 지정할 수 있지만, 그만큼 GC가 회수할 수 있는 여유 공간이 줄어들어 쓰기 성능과 트레이드오프 관계에 있다는 점을 감안해야 한다.

정리

F2FS는 NAND flash의 페이지 단위 쓰기, 블록 단위 소거라는 물리적 제약을 정면으로 받아들여 설계된 파일시스템이다. 로그 구조 기반의 append-only 쓰기와 hot/warm/cold multi-head logging으로 FTL의 GC 부담을 줄이고, 체크포인트와 roll-forward recovery로 ext4의 저널링에 준하는 일관성을 보장한다. 다만 자체 GC 비용이 있고 여유 공간이 부족할수록 성능이 떨어지는 경향이 있어, 파티션을 지나치게 꽉 채워 쓰는 환경에는 불리하다. Android 기기나 임베디드 보드처럼 eMMC/SD 카드를 쓰는 환경에서 F2FS를 도입하기 전에는 실제 워크로드로 ext4 대비 벤치마크를 돌려보고 결정하는 편이 안전하다.

답글 남기기