F2FS로 포맷한 SSD/eMMC의 실제 사용 가능 용량은 명목 용량보다 항상 작다. 이 차이를 “메타데이터가 공간을 많이 차지해서”라고 뭉뚱그려 설명하는 경우가 많은데, 실제로는 구조적 메타데이터(SIT·NAT·SSA 등)와 오버프로비저닝(GC를 위해 남겨두는 여유 공간)이라는 서로 다른 두 요인이 섞여 있고, 둘의 비중도 상당히 다르다. 이 글에서는 F2FS 메타데이터가 어떤 구조로 이뤄져 있는지, 커널 소스와 mkfs.f2fs 계산식을 근거로 실제로 얼마의 오버헤드가 발생하는지, 그리고 이를 확인하고 조정하는 방법을 정리한다.
F2FS 메타데이터 구성
F2FS는 볼륨을 세그먼트(segment, 고정 2MB = 512블록) 단위로 나눠 관리하고, 다음 여섯 영역으로 구성된다.
- 슈퍼블록(SB) — 파티션 맨 앞에 두 개의 복사본으로 저장되는 기본 파라미터.
- 체크포인트(CP) — 파일시스템 일관성 정보, NAT/SIT 유효 비트맵, 고아 아이노드 목록, 활성 세그먼트 요약을 담는다. 두 개의 복사본이 번갈아 최신 상태를 대표하는 shadow copy 방식이다.
- SIT(Segment Information Table) — 세그먼트별 유효 블록 수와 블록별 유효성 비트맵을 저장한다.
- NAT(Node Address Table) — 메인 영역에 있는 모든 노드 블록(inode 포함)의 물리 주소를 담는 테이블이다.
- SSA(Segment Summary Area) — 메인 영역의 모든 데이터·노드 블록이 어느 파일(부모 노드)에 속하는지 담아, GC 시 유효성 판별에 쓰인다.
- 메인 영역 — 실제 파일 데이터와 그 인덱스(노드 블록)가 저장되는 공간.
각 영역의 크기 공식
커널 온디스크 포맷 헤더(include/linux/f2fs_fs.h) 기준으로 SIT·NAT 엔트리 크기는 다음과 같다.
| 구조체 | 엔트리 크기 | 4KB 블록당 엔트리 수 | 설명 대상 |
|---|---|---|---|
| f2fs_sit_entry | 74바이트 (vblocks 2B + valid_map 64B + mtime 8B) | 55개 | 세그먼트 1개(2MB) |
| f2fs_nat_entry | 9바이트 (version 1B + ino 4B + block_addr 4B) | 455개 | 노드 블록 1개 |
| f2fs_summary | 7바이트 | – | SSA 블록 1개가 세그먼트 1개(512블록)를 통째로 커버 |
mkfs.f2fs 소스(mkfs/f2fs_format.c)의 실제 계산 로직은 다음과 같다.
// SIT: 세그먼트 개수에 비례, 이중화(x2)
blocks_for_sit = ALIGN(segment_count, SIT_ENTRY_PER_BLOCK); // 55개/블록
set_sb(segment_count_sit, SEG_ALIGN(blocks_for_sit) * 2);
// NAT: 메인 영역 블록 수(=노드가 될 수 있는 최악의 경우) 기준, 이중화(x2)
blocks_for_nat = ALIGN(total_valid_blks_available, NAT_ENTRY_PER_BLOCK); // 455개/블록
set_sb(segment_count_nat, SEG_ALIGN(blocks_for_nat) * 2); // max_nat_segments로 상한
// SSA: 메인 영역 세그먼트 1개당 요약 블록 1개
blocks_for_ssa = total_valid_blks_available / c.blks_per_seg + 1;
set_sb(segment_count_ssa, SEG_ALIGN(blocks_for_ssa));
// 오버프로비저닝: 메인 영역에서 예약 세그먼트를 제외한 나머지에 비율 적용
set_cp(overprov_segment_count,
(get_sb(segment_count_main) - get_cp(rsvd_segment_count)) * c.overprovision / 100);
NAT은 total_valid_blks_available(사실상 메인 영역 전체 블록 수)를 기준으로 계산된다는 점이 핵심이다. 이는 “메인 영역의 모든 블록이 각각 별도 파일의 노드가 되는” 최악의 경우까지 커버하기 위한 것으로, 실제 파일 개수와 무관하게 포맷 시점에 미리 확보되는 고정 예약분이다.
실제 오버헤드는 얼마나 되는가
위 공식을 볼륨 크기 대비 비율로 환산하면 다음과 같다(SIT·NAT·SSA 모두 이중화·정렬 오차를 무시하면 순수 비례식이라, max_nat_segments 상한에 걸리지 않는 한 볼륨 크기와 무관하게 거의 일정한 비율로 수렴한다).
| 영역 | 계산 | 볼륨 대비 비율 |
|---|---|---|
| SIT | 4KB 블록 1개(이중화 후 실질 2/55블록) ÷ 세그먼트(2MB) 55개 | 약 0.007% |
| NAT | 이중화된 4KB 블록 2개 ÷ 노드 455개(약 1.78MB) | 약 0.440% |
| SSA | 4KB 블록 1개 ÷ 세그먼트(2MB) 1개 | 약 0.195% |
| 구조적 메타데이터 합계 | – | 약 0.64% |
1TB 볼륨을 예로 들면 세그먼트가 524,288개 생기고, SIT 영역은 약 37MB, NAT 영역은 약 4.4GB, SSA 영역은 약 2GB 정도로 계산된다. 전체 구조적 메타데이터 중 NAT이 차지하는 비중이 약 68%로 압도적이다. dm-vdo의 UDS 인덱스가 전체 오버헤드를 지배했던 것과 비슷하게, F2FS에서는 NAT이 “모든 블록이 노드가 될 수 있다”는 최악의 시나리오를 커버하기 위해 가장 큰 몫을 차지한다.
오버프로비저닝은 별개다
mkfs.f2fs의 -o 옵션은 오버프로비저닝(overprovision) 비율을 지정하는데, 값을 따로 주지 않으면 get_best_overprovision() 함수가 볼륨 크기에 맞춰 자동으로 정한다. 메인 영역 세그먼트가 256개(약 512MB) 미만이면 10~95% 구간을, 그 이상이면 1~10% 구간을 후보로 놓고 실사용 가능 공간이 최대가 되는 값을 고르는 방식이라, 볼륨이 작을수록 훨씬 높은 비율이 자동으로 붙는다. 이는 메타데이터가 아니라 GC(가비지 컬렉션)가 항상 쓸 수 있는 여유 세그먼트를 확보해두는 용도이며, 아래에서 실측하겠지만 대부분의 크기 구간에서 구조적 메타데이터(SIT/NAT/SSA)보다 훨씬 큰 비중을 차지한다. 즉 F2FS 볼륨에서 명목 용량과 실사용 가능 용량의 차이 대부분은 메타데이터 구조 때문이 아니라, GC 여유 공간으로 의도적으로 남겨두는 오버프로비저닝 때문이다.
작은 볼륨에서 실측한 오버헤드
이론적 계산이 실제로 맞는지 확인하기 위해 512MB부터 8GB까지 5개 크기로 루프백 이미지를 만들어 f2fs-tools 1.16.0의 mkfs.f2fs 기본 옵션(오버프로비저닝 비율 미지정)으로 포맷하고, dump.f2fs로 슈퍼블록의 세그먼트 카운트를 직접 읽어 계산한 결과다.
truncate -s 512M img_512M.bin # 1G/2G/4G/8G도 동일하게 생성
mkfs.f2fs -f img_512M.bin
dump.f2fs -d 1 img_512M.bin | grep -E "segment_count|user_block_count"
| 볼륨 크기 | 오버프로비전 비율(자동 산정) | 구조적 메타데이터 (SB+CP+SIT+NAT+SSA) | GC 예약 (오버프로비저닝) | 실사용 가능 |
|---|---|---|---|---|
| 512 MB | 10.00% | 16 MB (3.12%) | 50 MB (9.77%) | 446 MB (87.11%) |
| 1 GB | 5.37% | 20 MB (1.95%) | 54 MB (5.27%) | 950 MB (92.77%) |
| 2 GB | 3.58% | 26 MB (1.27%) | 72 MB (3.52%) | 1,950 MB (95.21%) |
| 4 GB | 2.43% | 38 MB (0.93%) | 100 MB (2.44%) | 3,958 MB (96.63%) |
| 8 GB | 1.67% | 62 MB (0.76%) | 136 MB (1.66%) | 7,994 MB (97.58%) |
두 가지가 실측으로 확인된다. 첫째, 구조적 메타데이터 비율은 512MB의 3.12%에서 8GB의 0.76%까지 볼륨이 커질수록 빠르게 줄어들다가, 앞서 이론적으로 계산한 대용량 볼륨의 수렴값(약 0.64%)에 가까워진다. 둘째, GC 예약(오버프로비저닝) 비율도 512MB에서 9.77%로 시작해 8GB에서 1.66%까지 떨어지는데, 이는 앞 절에서 설명한 대로 mkfs.f2fs가 작은 볼륨일수록 실사용 가능 공간이 최대가 되는 지점을 찾다 보니 오히려 높은 비율을 선택하기 때문이다. 즉 “5%가 기본값”이라는 식의 고정값은 없고, 작은 볼륨일수록 구조적 메타데이터와 오버프로비저닝 둘 다 비율이 커지는 이중의 불리함을 겪는다.
실제 값 확인하기
계산값이 실제 포맷 결과와 맞아떨어지는지는 dump.f2fs로 슈퍼블록에 기록된 영역별 세그먼트 개수를 직접 확인하면 된다.
sudo dump.f2fs -s /dev/sdX | grep -E "segment_count|overprov"
segment_count_ckpt, segment_count_sit, segment_count_nat, segment_count_ssa, segment_count_main 값에 세그먼트 크기(2MB)를 곱해 더하면 각 영역의 실제 바이트 크기를 얻을 수 있고, 이를 전체 볼륨 크기로 나누면 실측 오버헤드 비율이 나온다.
오버헤드를 줄이거나 조정하는 방법
- 오버프로비저닝 비율 조정:
mkfs.f2fs -o <percent>로 자동 산정값보다 낮춰 실사용 가능 용량을 늘릴 수 있다. 특히 작은 볼륨은 자동 산정 비율 자체가 10%를 넘길 정도로 크므로 직접 값을 낮출 여지가 크다. 이미 컨트롤러 차원에서 여유 공간을 넉넉히 확보한 SSD, 또는 쓰기가 적고 읽기 위주인 워크로드라면 낮춰도 무방하지만, 랜덤 쓰기가 많은 워크로드에서 너무 낮추면 GC가 여유 세그먼트를 못 찾아 성능이 급격히 떨어질 수 있다. - inline_xattr / inline_dentry / inline_data 마운트 옵션: 작은 확장 속성·디렉터리 엔트리·파일 데이터를 별도 노드/데이터 블록에 쓰지 않고 inode 블록 안에 직접 채워 넣는다. SIT/NAT의 예약 용량 자체를 줄이지는 못하지만, 파일이 많은 워크로드에서 실제로 소비되는 노드·데이터 블록 수를 줄여준다.
- 구조적 예약분 자체는 재포맷 없이 줄일 수 없다: SIT/NAT/SSA는 mkfs 시점에 “메인 영역이 최악의 경우로 채워졌을 때”를 기준으로 자동 계산돼 고정된다. 사후에 조정하는 옵션은 없고, 다른 크기로 바꾸려면 볼륨을 다시 포맷해야 한다.
압축은 오버헤드와 다른 축의 최적화다
F2FS는 파일시스템 레벨 압축(CONFIG_F2FS_FS_COMPRESSION)도 지원한다. lzo, lzo-rle, lz4, zstd 네 가지 알고리즘 중에서 고를 수 있고(lz4는 레벨 3~16, zstd는 1~22 지정 가능), 압축 단위는 파일을 논리적으로 나눈 클러스터다. 클러스터 하나는 기본 16KB(4블록)이며 compress_log_size 마운트 옵션으로 4KB × (1 << n) 크기까지 조정할 수 있다.
# mkfs 시점에 압축 기능 포함 (extra_attr는 필수 선행 feature)
mkfs.f2fs -O extra_attr,compression /dev/sdX
# 파일/디렉터리 단위로 활성화
chattr +c /mnt/bigfile.dat
chattr +c /mnt/somedir && touch /mnt/somedir/newfile # 디렉터리에 걸면 이후 생성 파일에 상속
# 또는 마운트 시점에 확장자 기준으로 일괄 지정
mount -o compress_algorithm=zstd,compress_extension=* /dev/sdX /mnt
중요한 점은 압축이 이 글에서 다룬 구조적 메타데이터·오버프로비저닝과는 완전히 다른 축의 최적화라는 것이다. SIT/NAT/SSA는 mkfs 시점에 raw 세그먼트·블록 개수를 기준으로 미리 고정 계산되므로, 이후 파일을 압축해서 쓰든 안 쓰든 그 예약 크기 자체는 전혀 줄어들지 않는다. 압축은 대신 메인 영역에서 실제로 소비되는 유효 블록 수를 줄이는 방식으로 동작한다. 클러스터 안의 데이터가 잘 압축되면 남는 블록은 물리적으로 쓰지 않고 COMPRESS_ADDR라는 특수 블록 주소로만 표시해두기 때문에, 같은 논리 데이터를 더 적은 물리 블록에 담을 수 있다. 즉 “오버헤드 비율을 낮추는” 방법이 아니라 “같은 물리 용량에 더 많은 데이터를 담는” 방법이며, 결과적으로 여유 공간이 늘어나 오버프로비저닝 압박이 줄어드는 간접 효과는 있지만 메타데이터 예약 자체를 줄여주지는 않는다.
주의사항
- 위 계산은
max_nat_segments상한에 걸리지 않는 일반적인 볼륨 크기를 전제로 한다. 아주 큰 볼륨에서는 NAT이 상한에 걸려 비율이 오히려 낮아질 수 있으므로dump.f2fs로 실측하는 편이 안전하다. - 매우 작은 볼륨은 각 영역이 최소 세그먼트 단위로 정렬(
SEG_ALIGN)되고 오버프로비저닝 자동 산정 비율도 함께 커지기 때문에, 소형 파티션(임베디드 eMMC 등)에서는 실제 오버헤드가 이 글에서 실측한 512MB~8GB 표보다도 더 클 수 있다. 실제로 배포할 크기로 직접dump.f2fs를 돌려 확인하는 것이 가장 정확하다. - 오버프로비저닝을 낮추는 것은 순수한 용량 확보 조치이지 성능 개선이 아니다. 쓰기 워크로드가 있는 환경에서는 오히려 GC 빈도를 늘려 쓰기 증폭(write amplification)을 키울 수 있다.
마무리
F2FS의 구조적 메타데이터(SIT·NAT·SSA) 오버헤드는 대용량 볼륨에서는 약 0.64% 수준에 수렴하지만, 512MB~8GB 구간을 실측해보면 볼륨이 작을수록 비율이 빠르게 커진다(512MB 기준 3.12%). 그중 NAT이 최악의 경우를 대비해 예약해두는 공간이 대부분을 차지한다. 명목 용량과 실사용 가능 용량의 차이를 만드는 더 큰 요인은 메타데이터가 아니라 오버프로비저닝인데, 이 비율조차 고정값이 아니라 볼륨이 작을수록(512MB에서 10%, 8GB에서 1.67%) 자동으로 커지도록 설계돼 있어 작은 볼륨은 메타데이터와 오버프로비저닝 양쪽에서 이중으로 불리하다. 용량을 더 확보하고 싶다면 메타데이터 구조를 손댈 게 아니라 워크로드 특성에 맞춰 오버프로비저닝 비율을 직접 낮게 지정하는 쪽이 훨씬 효과적이고, 파일이 많은 환경이라면 inline_xattr/inline_dentry/inline_data 옵션으로 실제 소비되는 노드 블록 수를 줄이는 쪽이 현실적인 개선 방향이다.