F2FS 파일시스템 심화

플래시 저장장치 특성에 맞춘 F2FS를 중심으로 로그 구조 쓰기 경로, NAT/SIT/SSA 메타데이터의 역할, hot/warm/cold 분리 기록, foreground/background GC 정책, 체크포인트와 롤포워드 복구, segment 청소 비용 제어, 압축·fscrypt·fsverity 기능, Android 워크로드에서의 mount 옵션 및 수명/성능 튜닝 전략을 상세히 다룹니다.

전제 조건: MTDVFS 문서를 먼저 읽으세요. 플래시/읽기전용 계열 파일시스템은 쓰기 제약과 압축 정책이 성능/내구성에 직접 연결되므로 저장 매체 특성을 먼저 이해해야 합니다.
일상 비유: 이 주제는 지우기 어려운 메모지 관리와 비슷합니다. 한 번 쓴 내용을 쉽게 덮어쓸 수 없다는 제약을 전제로 배치와 정리를 설계해야 비용을 줄일 수 있습니다.

핵심 요약

  • 계층 이해 — VFS, 캐시, 하위 FS 경계를 구분합니다.
  • 메타데이터 우선 — inode/dentry 일관성을 먼저 확인합니다.
  • 저장 정책 — 저널링/압축/할당 정책 차이를 비교합니다.
  • 일관성 모델 — 로컬/원격/합성 FS의 반영 시점을 구분합니다.
  • 복구 관점 — 장애 시 재구성 경로를 함께 점검합니다.

단계별 이해

  1. 경계 계층 파악
    요청이 VFS에서 어디로 내려가는지 확인합니다.
  2. 메타/데이터 분리
    어느 경로에서 무엇이 갱신되는지 나눠 봅니다.
  3. 동기화/플러시 확인
    쓰기 반영 시점과 순서를 검증합니다.
  4. 복구 시나리오 점검
    비정상 종료 후 일관성 회복을 확인합니다.
관련 표준: F2FS On-Disk Layout (kernel.org) — F2FS 파일시스템 디스크 구조 공식 문서입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
관련 페이지: F2FS는 Flash 스토리지에 최적화된 로그 구조 파일시스템입니다. 블록 I/O 서브시스템은 Block I/O, VFS 계층은 VFS, 전통적 저널링 파일시스템은 ext4, Android 커널 통합은 Android 커널 페이지를 참고하세요.

개요 & 역사

F2FS(Flash-Friendly File System)는 Samsung Electronics가 개발한 Linux 파일시스템으로, NAND Flash 스토리지(eMMC, SD 카드, SSD)의 특성에 최적화된 로그 구조(Log-Structured) 설계를 채택합니다. Linux 3.8(2013년 2월)에 메인라인 머지되었습니다.

Flash 스토리지 특성

F2FS 설계를 이해하려면 NAND Flash의 물리적 제약을 먼저 알아야 합니다:

특성HDDNAND Flash
읽기 단위섹터 (512B)페이지 (4KB~16KB)
쓰기 단위섹터 (제자리)페이지 (빈 페이지만)
삭제 단위불필요블록 (128~512 페이지)
제자리 덮어쓰기가능불가능 (erase-before-write)
수명기계적 마모P/E cycle 제한 (TLC: ~3000, QLC: ~1000)

Flash 스토리지에서는 erase-before-write 제약으로 인해 제자리 갱신(in-place update)이 비효율적입니다. FTL(Flash Translation Layer)이 이를 추상화하지만, 파일시스템 레벨에서 Flash 특성을 고려하면 성능과 수명을 크게 개선할 수 있습니다.

설계 목표

F2FS 역사

시기이정표
2012.10Samsung, LSFMM에서 F2FS 설계 발표
2013.02Linux 3.8 메인라인 머지 (Jaegeuk Kim)
2014Motorola Moto G, F2FS 최초 상용 적용
2016Huawei P9, F2FS 기본 파일시스템 채택
2018Google Pixel 3, userdata 파티션 F2FS 채택
2019압축(LZO/LZ4) 지원, zoned storage 지원
2020ZSTD 압축 지원, 다중 디바이스 지원
2022Android 13+ 기본 파일시스템으로 광범위 채택

아키텍처 & 온디스크 레이아웃

F2FS는 전체 볼륨을 고정 크기 세그먼트(segment, 2MB)로 분할하고, 세그먼트를 묶어 섹션(section), 섹션을 묶어 존(zone)을 구성합니다. 온디스크 레이아웃은 6개 영역으로 나뉩니다:

Superblock (SB) Checkpoint (CP) ×2 SIT Segment Info NAT Node Address SSA Summary Area Main Area Node + Data Segments Node Seg Data Seg 세그먼트 구조: Block 0 | Block 1 | Block 2 | ... | Block 511 (512 blocks × 4KB = 2MB per segment) 계층 구조: Block(4KB) → Segment(2MB = 512 blocks) → Section(N segments) → Zone(N sections)

Superblock (SB)

볼륨의 기본 정보를 저장하며, 2개의 복사본이 존재합니다 (0번, 1번 블록):

/* include/linux/f2fs_fs.h */
struct f2fs_super_block {
    __le32 magic;                  /* 0xF2F52010 */
    __le16 major_ver;               /* 주 버전 */
    __le16 minor_ver;               /* 부 버전 */
    __le32 log_sectorsize;          /* 섹터 크기 (log2) */
    __le32 log_sectors_per_block;   /* 블록 당 섹터 수 (log2) */
    __le32 log_blocksize;           /* 블록 크기 (log2), 보통 12 (4KB) */
    __le32 log_blocks_per_seg;      /* 세그먼트 당 블록 (log2), 보통 9 (512) */
    __le32 segs_per_sec;            /* 섹션 당 세그먼트 수 */
    __le32 secs_per_zone;           /* 존 당 섹션 수 */
    __le32 checksum_offset;         /* SB 내 체크섬 위치 */
    __le64 block_count;             /* 전체 블록 수 */
    __le32 section_count;           /* 섹션 수 */
    __le32 segment_count;           /* 세그먼트 수 */
    __le32 segment_count_ckpt;      /* CP 영역 세그먼트 수 */
    __le32 segment_count_sit;       /* SIT 영역 세그먼트 수 */
    __le32 segment_count_nat;       /* NAT 영역 세그먼트 수 */
    __le32 segment_count_ssa;       /* SSA 영역 세그먼트 수 */
    __le32 segment_count_main;      /* Main Area 세그먼트 수 */
    __le32 segment0_blkaddr;        /* 세그먼트 0 시작 블록 주소 */
    __le32 cp_blkaddr;              /* CP 시작 블록 주소 */
    __le32 sit_blkaddr;             /* SIT 시작 블록 주소 */
    __le32 nat_blkaddr;             /* NAT 시작 블록 주소 */
    __le32 ssa_blkaddr;             /* SSA 시작 블록 주소 */
    __le32 main_blkaddr;            /* Main Area 시작 블록 주소 */
    /* ... feature flags, encryption, etc. */
};

6개 영역 요약

영역크기역할
Superblock (SB)1개 세그먼트볼륨 메타데이터, 매직 넘버, 레이아웃 정보
Checkpoint (CP)2개 세그먼트파일시스템 일관성 상태, 유효 NAT/SIT 저널, orphan inode
SIT (Segment Info Table)N 세그먼트각 세그먼트의 유효 블록 비트맵 & 카운트
NAT (Node Address Table)N 세그먼트노드 ID → 물리 블록 주소 매핑 (wandering tree 해결)
SSA (Segment Summary Area)N 세그먼트블록별 역매핑: 어느 노드의 어느 오프셋인지
Main Area대부분실제 노드 블록 + 데이터 블록 저장

로그 구조 쓰기 (LFS)

전통적인 LFS(Log-Structured File System)는 단일 로그 헤드를 사용하지만, F2FS는 6개의 로그 헤드(current segment)를 동시에 운용합니다. 데이터 온도(temperature)에 따라 분리 기록하여 GC 효율을 극대화합니다:

로그 헤드온도용도
CURSEG_HOT_NODEHot직접 노드 블록 (inode)
CURSEG_WARM_NODEWarm간접 노드 블록
CURSEG_COLD_NODECold간접-간접 노드 블록
CURSEG_HOT_DATAHot디렉토리 엔트리
CURSEG_WARM_DATAWarm일반 파일 데이터
CURSEG_COLD_DATACold멀티미디어, GC에 의해 이동된 데이터
/* fs/f2fs/segment.h */
enum {
    CURSEG_HOT_DATA  = 0,
    CURSEG_WARM_DATA,
    CURSEG_COLD_DATA,
    CURSEG_HOT_NODE,
    CURSEG_WARM_NODE,
    CURSEG_COLD_NODE,
    NR_CURSEG_TYPE,    /* = 6 */
};

Append-Only 쓰기 패턴

F2FS는 데이터를 제자리에서 갱신(in-place update)하지 않고, 항상 현재 세그먼트의 빈 블록에 순차적으로 추가(append)합니다. 이전 위치의 블록은 무효(invalid)로 표시됩니다:

/* fs/f2fs/segment.c - 새 블록 할당 */
static void __allocate_new_segment(struct f2fs_sb_info *sbi,
                                   int type, bool new_sec)
{
    struct curseg_info *curseg = CURSEG_I(sbi, type);
    unsigned int old_segno = curseg->segno;

    /* 현재 세그먼트가 가득 찼으면 새 세그먼트 할당 */
    if (!curseg->inited || !get_valid_blocks(sbi, old_segno, false) ||
        curseg->alloc_type == SSR)
        new_curseg(sbi, type, true);
}

할당 방식: LFS vs SSR

F2FS는 두 가지 할당 방식을 사용합니다:

LFS vs SSR 전환: 여유 세그먼트가 부족하면 F2FS는 자동으로 LFS에서 SSR 모드로 전환합니다. SSR은 랜덤 쓰기가 발생하지만, GC를 즉시 수행하는 것보다 지연 시간이 짧습니다.

NAT (Node Address Table)

NAT은 F2FS의 핵심 혁신으로, 전통적 LFS의 wandering tree 문제를 해결합니다.

Wandering Tree 문제

전통적 LFS에서 데이터 블록이 새 위치에 기록되면, 이를 가리키는 간접 노드 → inode → inode map 전체가 연쇄적으로 갱신되어야 합니다. F2FS는 NAT 테이블을 도입하여 노드 ID와 물리 주소 사이에 간접 계층을 둡니다:

전통 LFS (연쇄 갱신) Data Direct Node Inode Inode Map ← 4곳 갱신! F2FS (NAT 간접 변환) Data Direct Node (nid 참조) NAT Inode ← NAT만 갱신!
/* include/linux/f2fs_fs.h */
struct f2fs_nat_entry {
    __u8  version;       /* 버전 (SIT와 일관성 체크) */
    __le32 ino;          /* 소속 inode 번호 */
    __le32 block_addr;   /* 노드의 실제 물리 블록 주소 */
} __packed;

NAT 영역은 SIT와 마찬가지로 이중 버퍼링됩니다. 체크포인트 시 현재 유효한 NAT 페이지 세트가 기록되며, CP의 nat_bitmap이 어느 세트가 최신인지 가리킵니다.

SIT (Segment Information Table)

SIT는 각 세그먼트의 상태를 추적합니다. 세그먼트 내 각 블록의 유효/무효 여부를 비트맵으로 관리하고, 유효 블록 수를 카운트합니다:

/* include/linux/f2fs_fs.h */
struct f2fs_sit_entry {
    __le16 vblocks;                     /* 유효 블록 수 + type(10bit+6bit) */
    __u8   valid_map[SIT_VBLOCK_MAP_SIZE]; /* 유효 블록 비트맵 (64 bytes = 512 bits) */
    __le64 mtime;                        /* 세그먼트 수정 시간 (GC 비용 계산) */
} __packed;

SIT 이중 버퍼링

SIT 영역은 두 개의 복사본(set 0, set 1)으로 구성됩니다. 체크포인트마다 번갈아 갱신하여 원자적 갱신을 보장합니다. CP의 sit_nat_version_bitmap이 각 SIT 페이지의 유효한 세트를 가리킵니다.

SIT 활용

SSA (Segment Summary Area)

SSA는 Main Area의 각 블록에 대한 역매핑(reverse mapping) 정보를 저장합니다. 블록의 물리 주소로부터 해당 블록이 어느 노드의 어느 오프셋에 속하는지 역추적할 수 있습니다:

/* include/linux/f2fs_fs.h */
struct f2fs_summary {
    __le32 nid;      /* 부모 노드 ID */
    union {
        __u8 reserved;
        struct {
            __u8 version;  /* 노드 버전 */
            __le16 ofs_in_node; /* 부모 노드 내 오프셋 */
        };
    };
} __packed;

GC에서의 SSA 역할

GC가 세그먼트를 정리할 때, 각 유효 블록을 새 위치로 이동해야 합니다. SSA 엔트리를 통해 해당 블록의 부모 노드를 찾고, 노드의 포인터를 갱신합니다:

  1. GC 대상 세그먼트 선택 (SIT 기반)
  2. SIT 비트맵에서 유효 블록 식별
  3. SSA에서 각 유효 블록의 부모 노드(nid) 조회
  4. NAT에서 부모 노드의 물리 주소 조회
  5. 유효 블록을 새 세그먼트로 복사
  6. 부모 노드의 포인터 갱신 (out-of-place)

체크포인트 & 복구

F2FS는 이중 체크포인트(dual checkpoint) 방식으로 파일시스템 일관성을 보장합니다. 체크포인트는 특정 시점의 파일시스템 상태를 안정적으로 기록합니다.

체크포인트 구조

/* include/linux/f2fs_fs.h */
struct f2fs_checkpoint {
    __le64 checkpoint_ver;          /* 체크포인트 버전 (단조 증가) */
    __le64 user_block_count;         /* 사용자 블록 총 수 */
    __le64 valid_block_count;        /* 유효 블록 수 */
    __le32 rsvd_segment_count;       /* 예약 세그먼트 (GC 용) */
    __le32 overprov_segment_count;   /* 오버프로비저닝 세그먼트 */
    __le32 free_segment_count;       /* 여유 세그먼트 수 */
    __le32 ckpt_flags;              /* CP_UMOUNT_FLAG, CP_FSCK_FLAG 등 */
    __le32 cp_pack_total_block_count; /* CP 팩 전체 블록 수 */
    __le32 cp_pack_start_sum;        /* 요약 블록 시작 오프셋 */
    __le32 valid_node_count;         /* 유효 노드 수 */
    __le32 valid_inode_count;        /* 유효 inode 수 */
    __le32 next_free_nid;            /* 다음 사용 가능 노드 ID */
    __le32 sit_ver_bitmap_bytesize;  /* SIT 버전 비트맵 크기 */
    __le32 nat_ver_bitmap_bytesize;  /* NAT 버전 비트맵 크기 */
    __le32 checksum_offset;          /* 체크섬 위치 */
    __le64 elapsed_time;             /* 마운트 누적 시간 */
    /* 이후 current segment 정보, orphan inode, SIT/NAT 저널 */
};

체크포인트 절차

  1. 모든 dirty 노드/데이터 페이지 flush
  2. dirty NAT/SIT 엔트리를 NAT/SIT 영역에 기록 (또는 CP 내 저널에 저장)
  3. CP 팩 기록: header → orphan inode → 요약(summary) → footer
  4. CP 팩의 footer에 체크섬 기록 → 원자적 커밋 포인트
이중 CP의 의미: CP 영역은 pack #0, pack #1 두 곳에 번갈아 기록됩니다. 마운트 시 checkpoint_ver이 더 큰 쪽이 최신 유효 CP입니다. CP 기록 중 비정상 종료가 발생해도 이전 CP가 유효하므로 데이터 손실을 방지합니다.

Roll-Forward 복구

F2FS는 마지막 체크포인트 이후에 fsync()된 데이터를 roll-forward 기법으로 복구합니다:

  1. 마운트 시 최신 CP 로드
  2. CP 이후에 기록된 노드 블록을 Main Area에서 스캔
  3. fsync 마크(FADVISE_FSYNC_BIT)가 있는 노드의 데이터를 복구
  4. NAT/SIT 갱신하여 파일시스템 상태 일관성 확보
/* fs/f2fs/recovery.c */
static int f2fs_recover_fsync_data(struct f2fs_sb_info *sbi, bool check_only)
{
    /* Step 1: curseg 이후의 노드 스캔 */
    find_fsync_dnodes(sbi, &inode_list, check_only);
    /* Step 2: 유효한 fsync 노드의 데이터 복구 */
    do_recover_data(sbi, &inode_list);
    /* Step 3: 복구 완료 후 체크포인트 */
    f2fs_write_checkpoint(sbi, CP_RECOVERY_FLAG);
    return 0;
}

가비지 컬렉션 (GC)

로그 구조 파일시스템에서 GC는 핵심 동작입니다. 무효화된 블록이 포함된 세그먼트를 정리하여 깨끗한 세그먼트를 확보합니다.

GC 모드

모드트리거대상 선택특징
Background GC (BG_GC)커널 스레드 주기적 실행Cost-Benefit유휴 시 점진적, I/O 영향 최소
Foreground GC (FG_GC)여유 세그먼트 부족Greedy즉시 공간 확보, 지연 발생

대상 선택 알고리즘

Greedy 정책 (Foreground GC): 유효 블록이 가장 적은 세그먼트를 선택합니다. 이동할 데이터가 적어 빠르게 공간을 확보할 수 있습니다.

Cost-Benefit 정책 (Background GC): 유효 블록 수뿐 아니라 세그먼트의 나이(age)도 고려합니다. 오래된 세그먼트의 데이터는 Cold 데이터일 확률이 높아, 이동 후 다시 무효화될 가능성이 낮습니다:

설명 요약:
  • Cost-Benefit 공식:
  • cost = (1 - u) / (2 * u * age)
  • u = valid_blocks / total_blocks (utilization)
  • age = current_time - segment_mtime
  • 비용이 낮은 세그먼트가 GC에 유리 */

GC 실행 흐름

/* fs/f2fs/gc.c */
static int f2fs_gc(struct f2fs_sb_info *sbi, struct f2fs_gc_control *gc_control)
{
    /* 1. victim 세그먼트 선택 */
    f2fs_get_victim(sbi, &segno, gc_type, type, alloc_mode, age);

    /* 2. 유효 블록 이동 */
    do_garbage_collect(sbi, segno, &gc_list, gc_type, force);
    /*   - 데이터 블록: 새 COLD_DATA 세그먼트로 복사 */
    /*   - 노드 블록: 새 노드 세그먼트로 복사 */
    /*   - SIT/NAT 갱신 */

    /* 3. 정리된 세그먼트를 free 세그먼트로 반환 */
    return seg_freed;
}

GC 튜닝 파라미터

sysfs 파라미터기본값설명
gc_min_sleep_time30000 (30s)BG GC 최소 대기 시간 (ms)
gc_max_sleep_time60000 (60s)BG GC 최대 대기 시간 (ms)
gc_no_gc_sleep_time300000 (5m)GC 불필요 시 대기 시간
gc_idle00=비활성, 1=BG GC, 2=FG GC 강제
gc_urgent00=일반, 1=저속 GC, 2=고속 GC, 3=최고속
gc_urgent_high_limit-긴급 GC 공간 상한 (사용률 %)

인라인 데이터 & 인라인 디렉토리

소형 파일과 디렉토리에 대해 별도의 데이터 블록을 할당하지 않고, inode 블록 내부의 여유 공간에 직접 저장합니다.

인라인 데이터

F2FS inode는 4KB 블록을 차지하며, 메타데이터 이후의 여유 공간(약 3.4KB)에 소형 파일의 데이터를 직접 저장할 수 있습니다:

/* fs/f2fs/f2fs.h */
#define MAX_INLINE_DATA(inode) \
    (sizeof(__le32) * (DEF_ADDRS_PER_INODE - \
     get_inline_xattr_addrs(inode) - \
     F2FS_INLINE_XATTR_ADDRS - 1))
/* 보통 ~3,488 bytes까지 인라인 저장 가능 */

인라인 디렉토리

소규모 디렉토리(엔트리 수가 적은 경우)도 inode 블록 내에 저장됩니다:

/* fs/f2fs/f2fs.h */
#define NR_INLINE_DENTRY(inode) \
    (MAX_INLINE_DATA(inode) * BITS_PER_BYTE / \
     ((SIZE_OF_DIR_ENTRY + F2FS_SLOT_LEN) * BITS_PER_BYTE + 1))
/* 약 20~30개 엔트리까지 인라인 저장 가능 */
Android 효과: 모바일 환경에서는 소형 파일(설정, 캐시)이 매우 많습니다. 인라인 데이터/디렉토리로 이러한 파일의 I/O를 크게 줄여 앱 실행 속도가 개선됩니다.

Extent Cache

F2FS는 논리 블록 주소 → 물리 블록 주소 매핑을 인메모리 extent 캐시로 가속합니다. Red-black tree로 관리되며, 빈번한 파일 읽기에서 NAT/노드 탐색을 우회합니다:

/* fs/f2fs/extent_cache.c */
struct extent_info {
    unsigned int fofs;    /* 파일 내 시작 오프셋 (블록 단위) */
    unsigned int len;     /* 연속 블록 수 */
    block_t      blk;     /* 물리 시작 블록 주소 */
};

struct extent_node {
    struct rb_node rb_node;    /* rb-tree 노드 */
    struct extent_info ei;     /* extent 정보 */
    struct list_head list;    /* LRU 리스트 */
};

압축 (Compression)

F2FS는 Linux 5.4부터 투명 파일 압축을 지원합니다. 클러스터(cluster) 단위로 연속 블록을 압축하여 저장 공간을 절약합니다.

지원 알고리즘

알고리즘커널 설정특징
LZOCONFIG_F2FS_FS_LZO빠른 압축/해제, 낮은 압축률
LZ4CONFIG_F2FS_FS_LZ4매우 빠른 해제, 모바일 환경 최적
ZSTDCONFIG_F2FS_FS_ZSTD높은 압축률, 상대적으로 느림
LZORLECONFIG_F2FS_FS_LZORLELZO + RLE, LZO 개선판

클러스터 기반 압축

파일을 고정 크기 클러스터(기본 16 블록 = 64KB)로 분할한 후, 각 클러스터를 독립적으로 압축합니다:

설명 요약:
  • 클러스터 압축 구조 (16블록 클러스터 예시)
  • 원본: [blk0][blk1]...[blk15] (16 * 4KB = 64KB)
  • ↓ LZ4 압축
  • 압축: [cblk0][cblk1]...[cblk9][NULL]...[NULL]
  • 10블록(40KB) 사용 + 6블록 절약
  • NULL 블록은 공간 반환 (sparse block)
/* fs/f2fs/compress.c */
static int f2fs_compress_pages(struct compress_ctx *cc)
{
    const struct f2fs_compress_ops *cops =
        f2fs_cops[F2FS_I(cc->inode)->i_compress_algorithm];

    /* 클러스터 페이지를 연속 버퍼에 복사 */
    f2fs_compress_gather_pages(cc);

    /* 알고리즘별 압축 수행 */
    ret = cops->compress_pages(cc);

    /* 압축 결과가 원본보다 크면 비압축 저장 */
    if (cc->nr_cpages >= cc->nr_rpages)
        return -EAGAIN;
    return 0;
}

압축 사용법

# 파일시스템 레벨 압축 활성화 (마운트 옵션)
mount -t f2fs -o compress_algorithm=lz4,compress_log_size=4 /dev/sda1 /mnt

# 특정 파일/디렉토리에 압축 속성 설정
f2fs_io setflags compression /mnt/data
chattr +c /mnt/data      # FS_COMPR_FL

# 마운트 옵션
compress_algorithm=lz4     # 압축 알고리즘
compress_log_size=4        # 클러스터 크기 (2^N 블록, 4=16블록)
compress_extension=*.log   # 자동 압축 대상 확장자
compress_chksum            # 압축 데이터 체크섬 검증

암호화 & 무결성

fscrypt (파일 레벨 암호화)

F2FS는 Linux 커널의 fscrypt 프레임워크를 사용하여 파일 단위 암호화를 지원합니다. Android에서 FBE(File-Based Encryption)의 기반 기술입니다:

# 디렉토리에 암호화 정책 설정
fscryptctl set_policy --contents=AES-256-XTS --filenames=AES-256-CTS /mnt/private/

# F2FS 마운트 옵션
mount -t f2fs -o inlinecrypt /dev/sda1 /mnt  # 하드웨어 inline encryption

fsverity (무결성 검증)

fs-verity는 파일의 읽기 전용 인증 메커니즘으로, Merkle tree 기반의 무결성 검증을 제공합니다:

대소문자 무시 (Casefold)

F2FS는 디렉토리별 대소문자 무시(case-insensitive) 기능을 지원합니다. Android에서 Windows/macOS 호환 파일 접근에 사용됩니다:

# casefold 활성화 (mkfs 시)
mkfs.f2fs -O casefold /dev/sda1

# 디렉토리에 casefold 속성 설정
chattr +F /mnt/shared  # 이후 생성되는 파일에 대소문자 무시 적용

다중 디바이스 & Zoned Storage

다중 디바이스 지원

F2FS는 최대 8개의 블록 디바이스로 구성된 볼륨을 지원합니다. Device Mapper 없이 파일시스템 레벨에서 직접 다중 디바이스를 관리합니다:

# 다중 디바이스로 F2FS 생성
mkfs.f2fs -f /dev/sda /dev/sdb /dev/sdc

# 메타데이터는 첫 번째 디바이스, 데이터는 분산 저장

Zoned Storage (ZNS/SMR)

F2FS의 로그 구조 설계는 Zoned Storage와 자연스럽게 호환됩니다. ZNS(Zoned Namespaces) SSD와 SMR(Shingled Magnetic Recording) HDD를 직접 지원합니다:

# ZNS SSD에 F2FS 생성
mkfs.f2fs -f -m /dev/nvme0n1   # -m: zoned device mode

# Conventional + Sequential 혼합 구성
mkfs.f2fs -f -c /dev/nvme0n2 /dev/nvme0n1  # conventional + zoned

Device Aliasing (커널 6.13+)

커널 6.13에서 F2FS에 device aliasing 기능이 도입되었습니다. 하나의 물리 디바이스를 여러 논리 영역으로 나누어, 각 영역에 서로 다른 쓰기 정책(LFS/adaptive)이나 라이프타임 힌트를 적용할 수 있습니다. 다중 디바이스 구성 없이도 핫/콜드 데이터 분리 효과를 얻을 수 있어, 단일 디바이스 환경에서의 GC 효율이 향상됩니다.

# device aliasing으로 F2FS 생성 (v6.13+)
# 단일 디바이스에서 논리적 영역 분리
mkfs.f2fs -f -a 1 /dev/nvme0n1

16KB 페이지 크기 지원 개선 (커널 6.19+)

ARM64 플랫폼에서 16KB 기본 페이지 크기 커널을 사용하는 환경(예: 최신 Android)에 맞춰, F2FS의 16KB 페이지 지원이 커널 6.19에서 개선되었습니다. NAND 플래시의 물리 페이지 크기(4KB~16KB)와 OS 페이지 크기의 불일치 문제를 해소하여, 16KB 페이지 커널에서의 공간 효율과 성능이 향상되었습니다.

Android 16KB 전환: Android 15+에서 16KB 페이지 크기 커널 지원이 강화됨에 따라, F2FS의 16KB 페이지 호환성 개선은 모바일 환경에서 특히 중요합니다.

Android 통합

F2FS는 Android 기기의 /data (userdata) 파티션에 광범위하게 사용됩니다. Android 환경에 특화된 여러 기능이 있습니다:

Android 최적화 기능

기능설명
Atomic writeSQLite WAL 파일의 원자적 갱신. F2FS_IOC_START_ATOMIC_WRITE / F2FS_IOC_COMMIT_ATOMIC_WRITE
GC urgent modeIDLE 상태에서 공격적 GC 실행. Android JobScheduler와 연계
Pin fileGC에 의한 이동 방지. 대용량 파일(OBB, 미디어)의 fragmentation 방지
FBE (File-Based Encryption)fscrypt 기반 파일별 암호화. CE/DE 키 분리
fsverityAPK Signature Scheme v4 인증
Checkpoint disableOTA 업데이트 중 CP 비활성화로 A/B 롤백 지원
CompressionLZ4 압축으로 /data 용량 절약 (Android 12+)

Atomic Write

Android의 SQLite 데이터베이스는 WAL(Write-Ahead Log) 모드를 사용합니다. F2FS의 atomic write는 SQLite의 fsync 빈도를 줄이면서도 데이터 무결성을 보장합니다:

/* Atomic write ioctl 흐름 */
ioctl(fd, F2FS_IOC_START_ATOMIC_WRITE);   /* COW 시작 */
write(fd, data, len);                       /* 별도 공간에 기록 */
ioctl(fd, F2FS_IOC_COMMIT_ATOMIC_WRITE);  /* 원자적 커밋 (또는 ABORT) */

Android 마운트 옵션 예시

# 일반적인 Android /data 마운트 옵션
/dev/block/by-name/userdata /data f2fs \
    noatime,nosuid,nodev,discard,fsync_mode=nobarrier,\
    reserve_root=32768,resgid=1065,\
    inlinecrypt,checkpoint_merge,\
    compress_algorithm=lz4,compress_log_size=3,\
    compress_extension=*.apk,compress_extension=*.dex

핵심 커널 자료구조

f2fs_sb_info

F2FS 파일시스템 인스턴스의 중앙 자료구조입니다. 마운트 시 할당되며 모든 서브시스템이 참조합니다:

/* fs/f2fs/f2fs.h */
struct f2fs_sb_info {
    struct super_block     *sb;            /* VFS superblock */
    struct f2fs_super_block *raw_super;     /* 온디스크 superblock */
    struct f2fs_checkpoint  *ckpt;          /* 현재 체크포인트 */

    /* 세그먼트 관리 */
    struct f2fs_sm_info    *sm_info;       /* 세그먼트 매니저 */
    struct sit_info        *sit_info;      /* SIT 관리 */
    struct free_segmap_info *free_info;    /* 여유 세그먼트 비트맵 */
    struct dirty_seglist_info *dirty_info; /* dirty 세그먼트 */
    struct curseg_info     *curseg_array;  /* 6개 current segment */

    /* 노드 관리 */
    struct f2fs_nm_info    *nm_info;       /* 노드 매니저 (NAT) */

    /* GC */
    struct f2fs_gc_kthread *gc_thread;     /* BG GC 스레드 */
    unsigned int            cur_victim_sec; /* 현재 GC 대상 */

    /* 통계 & 디버깅 */
    struct f2fs_stat_info  *stat_info;     /* /sys/kernel/debug/f2fs */
    atomic_t                nr_pages[NR_COUNT_TYPE]; /* 페이지 카운터 */
};

f2fs_inode_info

/* fs/f2fs/f2fs.h */
struct f2fs_inode_info {
    struct inode       vfs_inode;      /* VFS inode (임베디드) */
    unsigned long       i_flags;        /* F2FS inode 플래그 */
    unsigned char       i_advise;       /* fadvise 힌트 */
    unsigned char       i_dir_level;    /* 디렉토리 해시 레벨 */
    unsigned int        i_current_depth; /* 디렉토리 현재 깊이 */
    unsigned int        i_pino;         /* 부모 inode 번호 */
    umode_t             i_acl_mode;     /* ACL 모드 */

    /* 인라인 */
    unsigned int        i_inline_xattr_size; /* 인라인 xattr 크기 */

    /* 압축 */
    unsigned char       i_compress_algorithm; /* 압축 알고리즘 */
    unsigned char       i_log_cluster_size;  /* 클러스터 크기 (log2) */
    unsigned int        i_cluster_size;      /* 클러스터 블록 수 */

    /* extent cache */
    struct extent_tree  *extent_tree[NR_EXTENT_CACHES];
    struct rw_semaphore  i_gc_rwsem[2]; /* GC 보호 */
};

curseg_info

/* fs/f2fs/segment.h */
struct curseg_info {
    struct mutex         curseg_mutex;     /* 세그먼트 잠금 */
    struct f2fs_summary_block *sum_blk;   /* 요약 블록 캐시 */
    struct rw_semaphore  journal_rwsem;    /* 저널 잠금 */
    unsigned int         segno;            /* 현재 세그먼트 번호 */
    unsigned short       next_blkoff;      /* 다음 블록 오프셋 */
    unsigned int         zone;             /* 현재 존 번호 */
    unsigned int         next_segno;       /* 다음 세그먼트 */
    int                  alloc_type;       /* LFS or SSR */
    bool                 inited;           /* 초기화 여부 */
};

성능 튜닝

주요 마운트 옵션

옵션설명
noatimeatime 갱신 비활성화 (쓰기 감소)
discard / nodiscard실시간 TRIM 활성/비활성
fsync_mode=posix|strict|nobarrierfsync 동작 모드. nobarrier는 배터리 백업 환경용
checkpoint_merge다중 체크포인트 요청을 병합하여 I/O 감소
gc_mergeGC I/O를 체크포인트와 병합
iostat/sys/kernel/debug/f2fs에 I/O 통계 활성화
mode=adaptive|lfsLFS 전용 모드 (zoned device) 또는 적응 모드
active_logs=2|4|6활성 로그 헤드 수 (기본 6)
alloc_mode=default|reuse세그먼트 할당 모드

sysfs 튜닝 파라미터

F2FS는 /sys/fs/f2fs/<devname>/ 경로에 다양한 런타임 튜닝 파라미터를 제공합니다:

# 주요 sysfs 파라미터 확인/설정
echo 40 > /sys/fs/f2fs/sda1/gc_urgent_sleep_time   # GC 간격 (ms)
echo 100 > /sys/fs/f2fs/sda1/min_seq_blocks        # 순차 쓰기 판단 블록 수
cat /sys/fs/f2fs/sda1/gc_urgent                     # 긴급 GC 모드
cat /sys/fs/f2fs/sda1/dirty_segments                # dirty 세그먼트 수
cat /sys/fs/f2fs/sda1/free_segments                 # 여유 세그먼트 수
cat /sys/fs/f2fs/sda1/ovp_segments                  # 오버프로비저닝 세그먼트
cat /sys/fs/f2fs/sda1/lifetime_write_kbytes         # 누적 기록량

I/O 스케줄러 조합

권장 조합: F2FS + eMMC/SSD에서는 none(noop) 또는 mq-deadline I/O 스케줄러를 권장합니다. F2FS 자체가 I/O 순서를 최적화하므로 복잡한 스케줄러는 불필요합니다.
# I/O 스케줄러 설정
echo none > /sys/block/sda/queue/scheduler

# discard granularity 확인
cat /sys/block/sda/queue/discard_granularity

관리 도구 (f2fs-tools)

f2fs-tools 패키지는 F2FS 파일시스템 생성, 검사, 디버깅을 위한 유저스페이스 도구를 제공합니다:

mkfs.f2fs

# 기본 생성
mkfs.f2fs /dev/sda1

# 주요 옵션
mkfs.f2fs -f \                     # 강제 포맷
  -l "myvolume" \                # 볼륨 라벨
  -O extra_attr,inode_checksum \   # feature 활성화
  -O encrypt,verity,compression \  # 보안/압축 feature */
  -O casefold \                    # 대소문자 무시
  -s 1 \                            # 섹션 당 세그먼트 수
  -z 1 \                            # 존 당 섹션 수
  -o 5 \                            # 오버프로비저닝 비율 (%) */
  /dev/sda1

fsck.f2fs

# 검사만 수행
fsck.f2fs /dev/sda1

# 자동 복구
fsck.f2fs -a /dev/sda1     # auto repair
fsck.f2fs -f /dev/sda1     # 강제 전체 검사
fsck.f2fs -p /dev/sda1     # preen (가벼운 검사)

dump.f2fs

# superblock 정보 출력
dump.f2fs /dev/sda1

# 특정 inode 덤프
dump.f2fs -i 3 /dev/sda1      # inode #3 (root dir)

# NAT 영역 덤프
dump.f2fs -n 0~100 /dev/sda1 # NAT 엔트리 0~100

# SIT 영역 덤프
dump.f2fs -s 0~10 /dev/sda1  # SIT 엔트리 0~10

기타 도구

도구설명
sload.f2fs오프라인 상태에서 디렉토리 트리를 F2FS 이미지에 로드
resize.f2fsF2FS 볼륨 크기 조절 (오프라인)
defrag.f2fs파일 단편화 해소
f2fs_ioF2FS ioctl 래퍼 (압축, pin_file 등)
f2fstatF2FS 통계 모니터링 (debugfs 기반)

ext4 / Btrfs / XFS 비교

항목F2FSext4BtrfsXFS
설계 철학Flash 최적화 LFS범용 저널링COW B-tree고성능 저널링
쓰기 패턴Append-only (out-of-place)In-place + 저널COW (out-of-place)In-place + WAL
메타데이터 구조NAT/SIT/SSA 테이블Extent Tree, 비트맵COW B-treeB+tree (AG별)
GC 필요성필수불필요불필요 (balance 별도)불필요
Flash 최적화네이티브제한적일부 (COW)없음
압축LZO/LZ4/ZSTD없음ZLIB/LZO/ZSTD없음
암호화fscrypt (파일 레벨)fscrypt없음 (계획 중)없음
스냅샷없음없음네이티브없음
최대 볼륨16TB1EB16EB8EB
최대 파일3.94TB16TB16EB8EB
랜덤 쓰기매우 우수보통우수 (COW)보통
순차 읽기우수우수우수매우 우수
모바일 적합성최적양호부적합부적합
Zoned Storage네이티브없음지원없음
주 사용처모바일, SSD, IoT범용 서버/데스크톱NAS, 데스크톱엔터프라이즈 서버
선택 가이드: eMMC/UFS 기반 모바일 기기 → F2FS, 범용 서버 → ext4 또는 XFS, 스냅샷/체크섬 필요 → Btrfs, 대용량 엔터프라이즈 → XFS.

세그먼트 / 섹션 / 존 구조 심화

F2FS의 공간 관리 계층은 Block → Segment → Section → Zone 4단계로 구성됩니다. 이 계층이 GC, 할당 정책, Zoned Storage 매핑의 기본 단위가 됩니다.

Zone (N sections) Section 0 (N segments) Segment 0 (512 blocks = 2MB) B0 B1 B2 B3 ... 511 Segment 1 (512 blocks = 2MB) ... Segment N-1 GC 단위 = Section (전체 section 정리) Section 1 Segment N Segment N+1 ... 유효(valid) 블록 무효(invalid) 블록 미사용(free) 블록 기본 크기 계산 Block = 4KB Segment = 512 blocks = 2MB Section = segs_per_sec segments Zone = secs_per_zone sections

6개 세그먼트 타입

F2FS는 Main Area의 세그먼트를 6가지 타입으로 분류합니다. 각 타입은 전용 current segment(로그 헤드)를 갖습니다:

타입상수데이터 종류온도GC 특성
HOT_DATACURSEG_HOT_DATA디렉토리 엔트리Hot자주 갱신, 빠르게 무효화
WARM_DATACURSEG_WARM_DATA일반 파일 데이터Warm중간 갱신 빈도
COLD_DATACURSEG_COLD_DATA멀티미디어, GC 이동 데이터Cold거의 갱신 안 됨
HOT_NODECURSEG_HOT_NODE직접 노드 (inode)Hot메타데이터 변경 시 갱신
WARM_NODECURSEG_WARM_NODE간접 노드Warm파일 확장 시 갱신
COLD_NODECURSEG_COLD_NODE이중 간접 노드Cold대형 파일에서만 사용

관리 단위와 GC 관계

GC는 섹션(section) 단위로 수행됩니다. 하나의 섹션 내 모든 세그먼트의 유효 블록을 이동한 후, 섹션 전체를 free로 반환합니다. Zoned Storage에서는 섹션이 존(zone)과 1:1 매핑되어 zone reset 명령으로 일괄 해제합니다.

/* fs/f2fs/segment.h - 세그먼트/섹션/존 변환 매크로 */
#define GET_SEC_FROM_SEG(sbi, segno) \
    ((segno) / (sbi)->segs_per_sec)

#define GET_ZONE_FROM_SEC(sbi, secno) \
    ((secno) / (sbi)->secs_per_zone)

#define GET_SEG_FROM_SEC(sbi, secno) \
    ((secno) * (sbi)->segs_per_sec)

/* 섹션 내 유효 블록 수 계산 */
static unsigned int get_valid_blocks(
    struct f2fs_sb_info *sbi,
    unsigned int segno, bool section)
{
    if (section) {
        unsigned int start = GET_SEG_FROM_SEC(sbi,
            GET_SEC_FROM_SEG(sbi, segno));
        unsigned int vblocks = 0;
        for (int i = 0; i < sbi->segs_per_sec; i++)
            vblocks += get_seg_entry(sbi, start + i)->valid_blocks;
        return vblocks;
    }
    return get_seg_entry(sbi, segno)->valid_blocks;
}
segs_per_sec 의미: mkfs.f2fs-s 옵션으로 설정합니다. 기본값은 1(섹션 = 세그먼트)이지만, Zoned Storage에서는 존 크기에 맞춰 자동 조정됩니다. 값이 클수록 GC 단위가 커져 공간 효율이 좋지만, GC 한 번의 I/O 비용도 증가합니다.

다중 헤드 로깅 심화

F2FS의 다중 로그 헤드 설계는 전통적 LFS의 단일 로그 헤드에서 발생하는 hot/cold 데이터 혼합 문제를 해결합니다. 각 로그 헤드는 독립적인 curseg_info 구조체로 관리됩니다.

F2FS 다중 헤드 로깅 (6개 Active Log) VFS 쓰기 요청 온도 분류기 Hot / Warm / Cold HOT_DATA 디렉토리 엔트리 WARM_DATA 일반 파일 데이터 COLD_DATA 멀티미디어, GC 이동 HOT_NODE inode (직접 노드) WARM_NODE 간접 노드 COLD_NODE 이중 간접 노드 Main Area Seg #42 (hot data) Seg #107 (warm data) Seg #250 (cold data) Seg #15 (hot node) Seg #88 (warm node) Seg #301 (cold node) active_logs 마운트 옵션 active_logs=6: Hot/Warm/Cold 완전 분리 (기본, 최적 GC) active_logs=4: Hot/Cold만 분리 (Warm 생략) | active_logs=2: Node/Data만 분리 (온도 무시)

온도 결정 로직

F2FS는 데이터의 종류와 접근 패턴을 분석하여 온도를 결정합니다:

/* fs/f2fs/segment.c - 데이터 온도 결정 */
static int __get_segment_type(struct f2fs_io_info *fio)
{
    if (fio->type == DATA) {
        struct inode *inode = fio->page->mapping->host;

        /* 디렉토리 데이터 → HOT_DATA */
        if (S_ISDIR(inode->i_mode))
            return CURSEG_HOT_DATA;

        /* GC에 의해 이동된 데이터 → COLD_DATA */
        if (is_cold_data(fio->page))
            return CURSEG_COLD_DATA;

        /* 사용자 힌트 또는 확장자 기반 → COLD_DATA */
        if (file_is_cold(inode))
            return CURSEG_COLD_DATA;

        /* 기본 → WARM_DATA */
        return CURSEG_WARM_DATA;
    }

    /* NODE 타입 분류 */
    if (IS_DNODE(fio->page)) {
        if (is_cold_node(fio->page))
            return CURSEG_WARM_NODE;
        return CURSEG_HOT_NODE;
    }
    return CURSEG_COLD_NODE;
}

Active Log 관리

각 current segment는 curseg_info 구조체에서 다음 쓰기 위치(next_blkoff)를 추적합니다. 세그먼트가 가득 차면(512번째 블록 기록 후) 새 세그먼트를 할당합니다:

/* fs/f2fs/segment.c - 블록 할당 핵심 경로 */
static void do_write_page(struct f2fs_summary *sum,
    struct f2fs_io_info *fio)
{
    int type = __get_segment_type(fio);
    struct curseg_info *curseg = CURSEG_I(sbi, type);

    down_read(&sbi->sm_info->curseg_lock);
    mutex_lock(&curseg->curseg_mutex);

    /* 현재 세그먼트의 다음 빈 블록에 기록 */
    __allocate_new_block(sbi, curseg, type);

    /* 요약 정보 기록 (SSA용) */
    __add_sum_entry(sbi, type, sum, curseg->next_blkoff);

    /* 세그먼트가 가득 찼으면 새 세그먼트로 교체 */
    if (curseg->next_blkoff >= sbi->blocks_per_seg)
        allocate_segment_by_default(sbi, type, false);

    mutex_unlock(&curseg->curseg_mutex);
    up_read(&sbi->sm_info->curseg_lock);
}
active_logs=2 주의: 로그 헤드를 2개로 줄이면 hot/cold 데이터가 같은 세그먼트에 혼합됩니다. GC가 세그먼트를 정리할 때 여전히 유효한 cold 블록을 불필요하게 이동해야 하므로 쓰기 증폭(write amplification)이 크게 증가합니다.

GC 알고리즘 심화

F2FS의 GC는 Foreground GC(FG_GC)Background GC(BG_GC) 두 가지 모드를 운용합니다. 각 모드는 서로 다른 대상 선택 정책을 사용하며, 공간 긴급도에 따라 자동 전환됩니다.

F2FS Garbage Collection 흐름 여유 세그먼트 확인 has_enough_free_secs()? 충분 (idle) Background GC (BG_GC) f2fs_gc_thread 주기적 실행 부족! Foreground GC (FG_GC) 할당 경로에서 직접 호출 Cost-Benefit 정책 cost = (1-u) / (2 * u * age) 나이 고려 → cold 세그먼트 우선 Greedy 정책 유효 블록 최소 세그먼트 선택 최대 공간 확보 → 빠른 해제 do_garbage_collect() 1. SIT 비트맵 스캔 2. SSA 역매핑 조회 3. 유효 블록 복사 4. NAT/SIT 갱신 섹션 free 반환 free_segment_count 증가

Cost-Benefit 분석 상세

Background GC의 핵심은 비용-이득 분석(Cost-Benefit Analysis)입니다. 단순히 유효 블록이 적은 세그먼트를 선택하는 것이 아니라, 세그먼트의 나이도 함께 고려합니다:

/* fs/f2fs/gc.c - Cost-Benefit 비용 계산 */
static unsigned int get_cb_cost(struct f2fs_sb_info *sbi,
    unsigned int segno)
{
    struct sit_info *sit_i = SIT_I(sbi);
    unsigned int secno = GET_SEC_FROM_SEG(sbi, segno);
    unsigned int start = GET_SEG_FROM_SEC(sbi, secno);
    unsigned long long mtime = 0;
    unsigned int vblocks;
    unsigned int usable_segs = f2fs_usable_segs_in_sec(sbi, segno);

    /* 섹션 내 모든 세그먼트의 평균 수정 시간 계산 */
    for (unsigned int i = 0; i < usable_segs; i++)
        mtime += get_seg_entry(sbi, start + i)->mtime;
    mtime = div_u64(mtime, usable_segs);

    /* 유효 블록 수 */
    vblocks = get_valid_blocks(sbi, segno, true);

    /* 사용률 u = vblocks / total_blocks */
    /* age = current_time - mtime */
    /* cost = (1-u) / (2*u) * age  (값이 높을수록 GC에 유리) */
    if (vblocks == 0)
        return UINT_MAX;  /* 유효 블록 없음 → 최우선 */

    return div_u64(mtime * (usable_segs * sbi->blocks_per_seg - vblocks),
                    2 * vblocks);
}
Cost-Benefit의 직관적 이해: 오래된(cold) 세그먼트는 age 값이 크므로 비용이 높아져 우선 선택됩니다. 이런 세그먼트의 유효 블록은 cold 데이터일 확률이 높아 이동 후 다시 무효화될 가능성이 낮으므로, 장기적으로 GC 빈도를 줄이는 효과가 있습니다.

GC Urgent 모드

Android 환경에서는 IDLE 상태일 때 공격적으로 GC를 수행하여 향후 쓰기 성능을 확보합니다:

gc_urgent 값모드동작사용 시나리오
0일반기본 BG GC일반 운용
1저속 긴급BG GC 주기 단축약간의 여유 공간 확보
2고속 긴급FG GC 수준의 공격적 GCAndroid IDLE maintenance
3최고속sleep 없이 연속 GC긴급 공간 확보
/* fs/f2fs/gc.c - GC 스레드 메인 루프 */
static int gc_thread_func(void *data)
{
    struct f2fs_sb_info *sbi = data;
    struct f2fs_gc_kthread *gc_th = sbi->gc_thread;
    wait_queue_head_t *wq = &sbi->gc_thread->gc_wait_queue_head;

    do {
        wait_event_interruptible_timeout(*wq,
            kthread_should_stop() || freezing(current) ||
            waitqueue_active(wq),
            msecs_to_jiffies(gc_th->min_sleep_time));

        /* urgent 모드에 따라 GC 강도 조절 */
        if (sbi->gc_mode == GC_URGENT_HIGH)
            gc_control.init_gc_type = FG_GC;  /* 공격적 */
        else
            gc_control.init_gc_type = BG_GC;  /* 일반 */

        f2fs_gc(sbi, &gc_control);
    } while (!kthread_should_stop());

    return 0;
}

NAT / SIT 구조 심화

NAT와 SIT는 F2FS 메타데이터 관리의 두 축입니다. 둘 다 이중 버퍼링저널(journal) 메커니즘을 사용하여 체크포인트 빈도를 줄이고 성능을 개선합니다.

NAT/SIT 이중 버퍼링 & 저널 NAT (Node Address Table) Set A NAT Page 0 NAT Page 1 NAT Page 2 ... Set B NAT Page 0' NAT Page 1' NAT Page 2' ... nat_ver_bitmap (CP 내) bit=0 → Set A 유효 | bit=1 → Set B 유효 체크포인트마다 dirty 페이지만 반대쪽 Set에 기록 NAT Journal (CP 팩 내) 소량의 dirty NAT 엔트리를 CP 팩에 직접 저장 → NAT 영역 I/O 없이 빠른 체크포인트 인메모리 NAT 캐시 (f2fs_nm_info) nat_root: radix tree (nid → nat_entry) dirty_nat_list → CP 시 flush 대상 SIT (Segment Information Table) Set A SIT Block 0 SIT Block 1 SIT Block 2 ... Set B SIT Block 0' SIT Block 1' SIT Block 2' ... sit_ver_bitmap (CP 내) SIT 엔트리: valid_map(64B 비트맵) + vblocks + mtime 하나의 SIT 블록에 ~55개 세그먼트 정보 저장 SIT Journal (CP 팩 내) 소량의 dirty SIT 엔트리를 CP 팩에 직접 저장 → SIT 영역 전체 갱신 없이 체크포인트 완료 인메모리 SIT 테이블 (sit_info) sentries[]: 전체 세그먼트의 인메모리 sit_entry 배열 dirty_sentries_bitmap → CP 시 flush 대상

NAT 저널 상세

NAT 저널은 체크포인트 팩 내에 소규모 dirty NAT 엔트리를 직접 저장하는 최적화입니다. dirty 엔트리 수가 저널 용량을 초과하면 NAT 영역에 flush합니다:

/* include/linux/f2fs_fs.h - NAT 저널 구조 */
struct f2fs_nat_journal {
    struct nat_journal_entry entries[NAT_JOURNAL_ENTRIES];
    __u8 reserved[NAT_JOURNAL_RESERVED];
} __packed;

struct nat_journal_entry {
    __le32 nid;                 /* 노드 ID */
    struct f2fs_nat_entry ne;   /* NAT 엔트리 (version, ino, block_addr) */
} __packed;

/* NAT_JOURNAL_ENTRIES = ~480 엔트리
 * dirty NAT 엔트리가 이 수를 초과하면
 * NAT 영역에 직접 기록 (flush) */

SIT 저널 상세

/* include/linux/f2fs_fs.h - SIT 저널 구조 */
struct f2fs_sit_journal {
    struct sit_journal_entry entries[SIT_JOURNAL_ENTRIES];
    __u8 reserved[SIT_JOURNAL_RESERVED];
} __packed;

struct sit_journal_entry {
    __le32 segno;               /* 세그먼트 번호 */
    struct f2fs_sit_entry se;   /* SIT 엔트리 (vblocks, valid_map, mtime) */
} __packed;

/* SIT_JOURNAL_ENTRIES = ~6 엔트리
 * SIT는 저널 용량이 작으므로 대부분 flush됨
 * NAT보다 flush 빈도가 높음 */
저널의 성능 효과: NAT 저널 덕분에 소규모 파일 쓰기(fsync 포함)에서 체크포인트 I/O가 크게 감소합니다. Android의 SQLite 트랜잭션처럼 소량 메타데이터 변경이 빈번한 워크로드에서 특히 효과적입니다.

체크포인트 심화

F2FS의 체크포인트는 파일시스템의 일관된 스냅샷을 디스크에 기록하는 핵심 동작입니다. CP 팩의 구성, 복구 흐름, fsync 최적화를 상세히 살펴봅니다.

체크포인트 팩 구조 & 복구 흐름 CP Pack #0 (ver=N) CP Header (f2fs_checkpoint) Orphan Inode Blocks (삭제 중 비정상 종료 대비) NAT Journal SIT Journal Data/Node Summary Blocks (6개 current segment 요약) CP Footer (checksum) ← 커밋 포인트 CP Pack #1 (ver=N-1) CP Header (이전 버전) (이전 체크포인트) CP #0 기록 실패 시 이 팩이 유효 CP Footer (checksum) 마운트 시 복구 흐름 1. SB 읽기 매직넘버 확인 2. CP 선택 ver 비교 → 최신 3. NAT/SIT 복원 bitmap + journal 4. Roll-Forward fsync 노드 복구 Roll-Forward 복구 상세 1) CP 이후 기록된 노드 블록을 Main Area에서 순차 스캔 2) FADVISE_FSYNC_BIT 마크된 노드 식별 → fsync 데이터 복구 3) 복구 완료 후 새 체크포인트 기록 (CP_RECOVERY_FLAG)

CP 팩 구조 상세

CP 팩은 여러 블록으로 구성되며, 첫 블록(header)과 마지막 블록(footer)에 동일한 f2fs_checkpoint 구조체가 기록됩니다. footer의 체크섬이 원자적 커밋 포인트 역할을 합니다:

/* fs/f2fs/checkpoint.c - CP 기록 순서 */
static int do_checkpoint(struct f2fs_sb_info *sbi,
    struct cp_control *cpc)
{
    struct f2fs_checkpoint *ckpt = F2FS_CKPT(sbi);

    /* 1. 모든 dirty 페이지 flush */
    f2fs_flush_merged_writes(sbi);

    /* 2. orphan inode 목록 기록 */
    write_orphan_inodes(sbi, sbi->cur_cp_pack);

    /* 3. NAT/SIT 저널 또는 flush */
    flush_nat_entries(sbi, cpc);
    flush_sit_entries(sbi, cpc);

    /* 4. current segment 요약 블록 기록 */
    write_data_summaries(sbi, sbi->cur_cp_pack);
    write_node_summaries(sbi, sbi->cur_cp_pack);

    /* 5. CP header 기록 (checkpoint_ver++) */
    ckpt->checkpoint_ver = cpu_to_le64(
        le64_to_cpu(ckpt->checkpoint_ver) + 1);

    /* 6. CP footer 기록 + checksum → 커밋 포인트! */
    commit_checkpoint(sbi, ckpt, sbi->cur_cp_pack);

    /* 7. 다음 CP는 반대쪽 팩에 기록 */
    sbi->cur_cp_pack ^= 1;
    return 0;
}

fsync 최적화

전통적 방식에서 fsync()는 전체 체크포인트를 트리거하지만, F2FS는 노드 레벨 fsync 마킹으로 이를 최적화합니다:

/* fs/f2fs/file.c - fsync 최적화 */
static int f2fs_do_sync_file(struct file *file,
    loff_t start, loff_t end, int datasync)
{
    struct inode *inode = file->f_mapping->host;

    /* CP 이후 변경이 없으면 바로 반환 */
    if (!f2fs_is_volatile_file(inode) &&
        !f2fs_need_inode_block_update(sbi, inode->i_ino))
        return 0;

    /* 방법 1: CP 이후 변경량이 적으면 노드만 기록 */
    if (need_node_only_sync(inode)) {
        f2fs_write_inode(inode, NULL);
        f2fs_set_fsync_mark(inode);  /* FADVISE_FSYNC_BIT 설정 */
        f2fs_issue_flush(sbi);
        return 0;
    }

    /* 방법 2: 변경량이 크면 전체 체크포인트 */
    return f2fs_write_checkpoint(sbi, CP_SYNC_FLAG);
}
checkpoint_merge 옵션: Android에서는 여러 앱이 동시에 fsync를 호출합니다. checkpoint_merge 마운트 옵션은 이런 동시 CP 요청을 하나로 병합하여 디스크 I/O를 크게 줄입니다. 이 최적화 덕분에 앱 설치, SQLite 쓰기 등의 성능이 10~30% 개선됩니다.

압축 모드 심화

F2FS의 투명 압축은 클러스터(cluster) 단위로 수행됩니다. 각 클러스터는 독립적으로 압축/해제되어 랜덤 읽기 성능에 미치는 영향을 최소화합니다.

F2FS 클러스터 기반 압축 원본 파일 (논리 블록) Cluster 0 (16 blocks = 64KB) Cluster 1 (16 blocks = 64KB) Cluster 2 (16 blocks = 64KB) Cluster 3 (부분 사용) LZ4 LZ4 LZ4 비압축 디스크 저장 (물리 블록) 압축 데이터 (10블록) NULL (6) 6블록 절약 (37.5%) 압축 데이터 (12블록) NULL 4블록 절약 (25%) 압축 (8블록) NULL (8) 8블록 절약 (50%) 비압축 (원본 그대로) 절약 없음 알고리즘별 특성 비교 LZ4 압축률: 보통 (50~65%) 해제 속도: 매우 빠름 Android 기본 선택 LZO / LZORLE 압축률: 보통 (50~60%) 압축 속도: 빠름 임베디드 환경 적합 ZSTD 압축률: 높음 (30~50%) CPU 비용: 높음 저장 공간 우선 시 선택 인라인 데이터와 압축의 관계 인라인 데이터(~3.4KB): 압축 미적용 (이미 inode 블록 내 저장, 클러스터 단위 미만)

압축 컨텍스트 (compress_ctx)

압축의 핵심 자료구조인 compress_ctx는 한 클러스터의 압축/해제 과정을 관리합니다:

/* fs/f2fs/f2fs.h - 압축 컨텍스트 */
struct compress_ctx {
    struct inode  *inode;        /* 대상 inode */
    pgoff_t       cluster_idx;   /* 클러스터 인덱스 */
    unsigned int  cluster_size;  /* 클러스터 블록 수 (2^N) */
    unsigned int  log_cluster_size; /* log2(cluster_size) */
    struct page **rpages;        /* 원본(raw) 페이지 배열 */
    unsigned int  nr_rpages;     /* 원본 페이지 수 */
    struct page **cpages;        /* 압축(compressed) 페이지 배열 */
    unsigned int  nr_cpages;     /* 압축 페이지 수 */
    void         *rbuf;          /* 원본 데이터 버퍼 */
    void         *cbuf;          /* 압축 데이터 버퍼 */
    size_t        rlen;          /* 원본 크기 */
    size_t        clen;          /* 압축 크기 */
};

압축 읽기 경로

압축된 클러스터를 읽을 때는 디스크에서 압축 블록을 읽은 후, 메모리에서 해제하여 원본 데이터를 복원합니다:

/* fs/f2fs/compress.c - 압축 해제 읽기 */
static int f2fs_decompress_pages(struct decompress_io_ctx *dic)
{
    const struct f2fs_compress_ops *cops =
        f2fs_cops[F2FS_I(dic->inode)->i_compress_algorithm];

    /* 압축 블록을 연속 버퍼에 모으기 */
    f2fs_decompress_gather(dic);

    /* 알고리즘별 해제 수행 */
    ret = cops->decompress_pages(dic);
    if (ret) {
        f2fs_err(sbi, "cluster %lu decompression failed: %d",
                 dic->cluster_idx, ret);
        return ret;
    }

    /* 체크섬 검증 (compress_chksum 옵션) */
    if (F2FS_I(dic->inode)->i_flags & F2FS_COMPR_FL)
        f2fs_verify_compress_chksum(dic);

    return 0;
}
압축과 암호화 조합: F2FS에서 압축과 fscrypt 암호화를 동시에 사용할 때는 압축 먼저 → 암호화 순서로 처리됩니다. 암호화된 데이터는 랜덤 바이트 패턴이므로 압축이 되지 않기 때문입니다. 커널 5.16+에서 이 조합이 지원됩니다.

ZNS SSD 지원 심화

ZNS(Zoned Namespaces) SSD는 데이터를 존(zone) 단위로 관리하며, 각 존은 순차 쓰기만 허용합니다. F2FS의 로그 구조 설계는 이 제약과 자연스럽게 호환됩니다.

F2FS ↔ ZNS SSD 매핑 F2FS 레이어 Section 0 Section 1 Section 2 Section 3 Section N ... mode=lfs 순차 쓰기(append-only) 보장 segs_per_sec = zone_cap/2MB 1:1 매핑 ZNS SSD Conv. Zone 0 Seq. Zone 1 Seq. Zone 2 Seq. Zone 3 Seq. Zone N 메타데이터/랜덤 쓰기 데이터 (순차 쓰기만) GC 시 zone reset Conventional Zone: 랜덤 R/W 허용 (SB, CP, SIT, NAT 저장) Sequential Zone: 순차 쓰기만 허용 (Main Area 데이터/노드, GC 시 전체 zone reset)

존-섹션 매핑

F2FS는 ZNS SSD의 각 존을 하나의 섹션(section)에 1:1 매핑합니다. 이 매핑의 핵심은 segs_per_sec을 존 용량에 맞게 설정하는 것입니다:

/* fs/f2fs/super.c - ZNS 디바이스 초기화 */
static int f2fs_init_zoned_device(struct f2fs_sb_info *sbi)
{
    struct request_queue *q = bdev_get_queue(sbi->sb->s_bdev);
    sector_t zone_sectors = bdev_zone_sectors(sbi->sb->s_bdev);

    /* 존 크기 = segs_per_sec * 세그먼트 크기
     * 예: 256MB zone → segs_per_sec = 256MB / 2MB = 128 */
    sbi->segs_per_sec = zone_sectors >>
        (sbi->log_blocksize - 9 + sbi->log_blocks_per_seg);

    /* mode=lfs 강제 (순차 쓰기만 허용) */
    set_opt(sbi, LFS);

    /* 존 타입 배열 초기화 */
    f2fs_report_zones(sbi);

    return 0;
}

GC와 Zone Reset

ZNS SSD에서 GC는 특별한 의미를 갖습니다. 기존 SSD에서는 GC 후 무효 블록을 TRIM하지만, ZNS에서는 존 전체를 zone reset하여 쓰기 포인터를 초기화합니다:

/* fs/f2fs/segment.c - 섹션 해제 시 zone reset */
static void f2fs_reset_zone(struct f2fs_sb_info *sbi,
    unsigned int secno)
{
    sector_t zone_start = sec_start_blkaddr(sbi, secno)
        << (sbi->log_blocksize - 9);
    sector_t zone_len = (sector_t)sbi->segs_per_sec
        << (sbi->log_blocks_per_seg + sbi->log_blocksize - 9);

    /* 존 리셋: 쓰기 포인터를 존 시작으로 초기화 */
    blkdev_zone_mgmt(sbi->sb->s_bdev, REQ_OP_ZONE_RESET,
                      zone_start, zone_len, GFP_NOFS);
}
ZNS의 이점: ZNS SSD에서 FTL의 GC가 제거되므로, 호스트(F2FS) GC만 수행합니다. 이로 인해 쓰기 증폭(Write Amplification)이 디바이스 레벨에서 1.0에 가까워지고, SSD의 예비 용량(over-provisioning)을 줄여 사용 가능 용량이 증가합니다. 또한 GC 지연 시간이 예측 가능해져 QoS가 향상됩니다.

커널 내부 자료구조 심화

F2FS의 인메모리 자료구조는 f2fs_sb_info를 중심으로 세그먼트 매니저, 노드 매니저, GC 스레드가 계층적으로 연결됩니다.

F2FS 인메모리 자료구조 계층 f2fs_sb_info VFS super_block, raw_super, ckpt f2fs_sm_info (세그먼트 매니저) sit_info, free_info, dirty_info, curseg_array sit_info sentries[], dirty_bitmap free_segmap_info free_segmap, free_secmap curseg_info[NR_CURSEG_TYPE] segno, next_blkoff, alloc_type, sum_blk [0]=HOT_DATA ... [5]=COLD_NODE f2fs_nm_info (노드 매니저) nat_root, free_nid_root, dirty_nat_list nat_root radix tree (nid→entry) free_nid_list 사용 가능 NID 캐시 f2fs_gc_kthread gc_wake, urgent_sleep_time extent_tree (per-inode) rb_root, extent_node[], read_extent / age_extent VFS 레이어 연결 super_operations: f2fs_sops inode_operations: f2fs_dir_inode_ops file_operations: f2fs_file_operations

f2fs_sb_info 초기화

마운트 시 f2fs_fill_super()에서 f2fs_sb_info를 할당하고 각 서브시스템을 초기화합니다:

/* fs/f2fs/super.c - 마운트 초기화 */
static int f2fs_fill_super(struct super_block *sb,
    void *data, int silent)
{
    struct f2fs_sb_info *sbi;

    /* 1. f2fs_sb_info 할당 */
    sbi = kzalloc(sizeof(*sbi), GFP_KERNEL);
    sb->s_fs_info = sbi;
    sbi->sb = sb;

    /* 2. 슈퍼블록 읽기 & 검증 */
    read_raw_super_block(sbi, &raw_super, &valid_super_block);

    /* 3. 체크포인트 읽기 */
    f2fs_get_valid_checkpoint(sbi);

    /* 4. 세그먼트 매니저 초기화 */
    f2fs_build_segment_manager(sbi);  /* SIT, free_info, curseg */

    /* 5. 노드 매니저 초기화 */
    f2fs_build_node_manager(sbi);     /* NAT 캐시, free NID */

    /* 6. GC 스레드 시작 */
    f2fs_start_gc_thread(sbi);

    /* 7. roll-forward 복구 */
    f2fs_recover_fsync_data(sbi, false);

    return 0;
}

f2fs_inode_info 캐시

F2FS는 VFS inode에 F2FS 전용 필드를 추가한 f2fs_inode_info를 사용합니다. slab 캐시(f2fs_inode_cachep)로 관리되며, container_of 매크로로 VFS inode에서 변환합니다:

/* fs/f2fs/f2fs.h */
static inline struct f2fs_inode_info *F2FS_I(
    struct inode *inode)
{
    return container_of(inode, struct f2fs_inode_info, vfs_inode);
}

/* 주요 F2FS inode 플래그 */
#define FI_INLINE_DATA      0x0001  /* 인라인 데이터 */
#define FI_INLINE_DENTRY    0x0002  /* 인라인 디렉토리 */
#define FI_ATOMIC_FILE      0x0004  /* atomic write 진행 중 */
#define FI_VOLATILE_FILE    0x0008  /* volatile 파일 */
#define FI_COMPRESS_ENABLED 0x0010  /* 압축 활성화 */
#define FI_PIN_FILE         0x0020  /* GC 이동 금지 */
메모리 관리: F2FS는 NAT 캐시, SIT 테이블, extent 캐시 등 상당량의 인메모리 자료구조를 유지합니다. /sys/fs/f2fs/<dev>/ram_thresh 파라미터(기본 10%)로 전체 메모리 대비 F2FS 캐시 사용량 상한을 제어할 수 있습니다. 모바일 기기에서는 이 값을 낮게 설정하여 메모리 압박을 줄이기도 합니다.

ftrace / bpftrace F2FS 추적

F2FS는 풍부한 tracepoint를 제공하여 GC, 체크포인트, 세그먼트 할당 등 내부 동작을 실시간으로 추적할 수 있습니다.

F2FS tracepoint 아키텍처 VFS 레이어 open/read/write fsync/sync unlink/truncate mkdir/mknod F2FS 핵심 경로 (tracepoint 계측) f2fs_readpage f2fs_write_data_page f2fs_sync_file_enter f2fs_unlink f2fs_new_inode f2fs_truncate GC 서브시스템 f2fs_gc_begin / f2fs_gc_end gc_type, seg_freed, sec_freed CP 서브시스템 f2fs_write_checkpoint reason, cp_ver, free_segs BIO / 세그먼트 할당 f2fs_submit_page_bio blkaddr, type, rw trace_pipe / perf / bpftrace /sys/kernel/debug/tracing/

주요 tracepoint 목록

tracepoint설명주요 필드
f2fs:f2fs_gc_beginGC 시작gc_type, no_bg_gc, nr_free_secs
f2fs:f2fs_gc_endGC 종료ret, seg_freed, sec_freed
f2fs:f2fs_write_checkpoint체크포인트 시작reason, msg
f2fs:f2fs_submit_page_bio페이지 BIO 제출ino, blkaddr, type
f2fs:f2fs_submit_write_bio쓰기 BIO 제출type, size
f2fs:f2fs_new_inode새 inode 생성ino, dir_ino
f2fs:f2fs_unlink_enter파일 삭제 시작ino, name
f2fs:f2fs_truncate파일 잘라내기ino, size, blocks
f2fs:f2fs_sync_file_enterfsync 시작ino, datasync
f2fs:f2fs_sync_file_exitfsync 완료ino, need_cp, ret
f2fs:f2fs_readpage페이지 읽기ino, index, blkaddr
f2fs:f2fs_do_write_data_page데이터 쓰기ino, index, blkaddr
f2fs:f2fs_compress_pages_start압축 시작ino, cluster_idx
f2fs:f2fs_decompress_pages압축 해제ino, cluster_idx

ftrace 사용 예시

# GC 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/f2fs/f2fs_gc_begin/enable
echo 1 > /sys/kernel/debug/tracing/events/f2fs/f2fs_gc_end/enable
cat /sys/kernel/debug/tracing/trace_pipe

# 체크포인트 추적
echo 1 > /sys/kernel/debug/tracing/events/f2fs/f2fs_write_checkpoint/enable

# fsync 성능 분석
echo 1 > /sys/kernel/debug/tracing/events/f2fs/f2fs_sync_file_enter/enable
echo 1 > /sys/kernel/debug/tracing/events/f2fs/f2fs_sync_file_exit/enable

# 전체 F2FS 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/f2fs/enable

# 추적 중지 및 결과 저장
echo 0 > /sys/kernel/debug/tracing/events/f2fs/enable
cat /sys/kernel/debug/tracing/trace > /tmp/f2fs_trace.txt

bpftrace 고급 추적

# GC 소요 시간 히스토그램
bpftrace -e '
tracepoint:f2fs:f2fs_gc_begin { @start[tid] = nsecs; }
tracepoint:f2fs:f2fs_gc_end /@start[tid]/ {
    @gc_latency_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}
'

# fsync 호출 빈도 (프로세스별)
bpftrace -e '
tracepoint:f2fs:f2fs_sync_file_enter {
    @fsync_count[comm] = count();
}
interval:s:5 { print(@fsync_count); clear(@fsync_count); }
'

# 체크포인트 사유 추적
bpftrace -e '
tracepoint:f2fs:f2fs_write_checkpoint {
    printf("%s CP reason=%d\n", strftime("%H:%M:%S", nsecs), args->reason);
}
'

# 세그먼트 할당 패턴 분석
bpftrace -e '
kprobe:f2fs_allocate_data_block {
    @alloc_type[arg3] = count();  /* segment type */
}
interval:s:10 { print(@alloc_type); clear(@alloc_type); }
'

debugfs 통계

F2FS는 /sys/kernel/debug/f2fs/에 상세 통계를 노출합니다:

# F2FS 전체 통계
cat /sys/kernel/debug/f2fs/status

# 출력 예시:
# [f2fs: sda1]
# - Main Area: 16384 segments, 2048 sections, 256 zones
# - valid blocks: 1234567 / 8388608
# - dirty segments: 42
# - free segments: 4096
# - node pages: 12345 (dirty: 67)
# - data pages: 23456 (dirty: 89)
# - GC calls: 128 (BG: 120, FG: 8)
# - CP calls: 256 (total: 512s)
# - extent cache: hit=98.5% miss=1.5%

# I/O 통계 (iostat 마운트 옵션 필요)
cat /sys/kernel/debug/f2fs/<dev>/iostat_info
Android GC 분석: Android 기기에서 F2FS GC 성능 문제를 진단할 때는 f2fs_gc_begin/end tracepoint와 함께 /sys/fs/f2fs/<dev>/gc_urgent, free_segments 값을 모니터링하세요. FG_GC 빈도가 높으면 over-provisioning 비율 증가 또는 gc_urgent 모드 활용을 검토합니다.

성능 튜닝 심화

F2FS는 다양한 마운트 옵션, sysfs 파라미터, mkfs 설정을 통해 워크로드에 맞춘 미세 조정이 가능합니다.

F2FS 성능 튜닝 계층 포맷 시 (mkfs) (변경 불가, 재포맷 필요) -o N : 오버프로비저닝 % -s N : segs_per_sec -z N : secs_per_zone -O features : 기능 플래그 -c dev : 다중 디바이스 -e ext : 기본 압축 확장자 마운트 시 (-o 옵션) (리마운트로 일부 변경 가능) active_logs=2|4|6 fsync_mode=posix|strict|nobarrier checkpoint_merge compress_algorithm=lz4|zstd mode=adaptive|lfs discard|nodiscard 런타임 (sysfs) (즉시 변경, 재부팅 시 초기화) gc_urgent (0~3) gc_min/max_sleep_time gc_urgent_high_limit min_seq_blocks ram_thresh discard_granularity 성능 영향 범위 쓰기 증폭 (WAF) GC 빈도, 온도 분리 fsync 지연시간 CP 빈도, 저널 크기 순차 쓰기 처리량 active_logs, alloc_mode 공간 효율 압축, 인라인, OVP 오버프로비저닝 ↑ → WAF ↓ GC 빈도 ↓ | active_logs=6 → 온도 분리 ↑ GC 효율 ↑ | checkpoint_merge → fsync 지연 ↓ compress + noatime → 공간 효율 ↑ 쓰기 ↓ | fsync_mode=nobarrier → 배터리 백업 환경에서 성능 ↑

오버프로비저닝(OVP) 분석

오버프로비저닝은 GC가 동작할 여유 공간을 확보합니다. OVP 비율이 높을수록 GC 빈도가 줄어 쓰기 증폭이 감소하지만, 사용 가능 용량이 줄어듭니다:

OVP 비율사용 가능 용량GC 빈도쓰기 증폭권장 환경
3% (기본)97%높음높음용량 우선
5%95%중간중간일반 모바일
10%90%낮음낮음수명 우선
20%80%매우 낮음매우 낮음고성능 SSD
# OVP 5%로 포맷
mkfs.f2fs -f -o 5 /dev/sda1

# 현재 OVP 세그먼트 확인
cat /sys/fs/f2fs/sda1/ovp_segments

# 예약 세그먼트 확인 (GC 최소 여유)
cat /sys/fs/f2fs/sda1/reserved_blocks

워크로드별 최적 설정

워크로드마운트 옵션sysfs 조정설명
Android /datanoatime,checkpoint_merge,compress_algorithm=lz4,active_logs=6gc_urgent=2 (IDLE 시)SQLite fsync, 소형 파일 최적화
데이터베이스noatime,fsync_mode=strict,active_logs=6gc_min_sleep_time=10000데이터 일관성 우선
미디어 저장소noatime,compress_algorithm=zstd,compress_log_size=5gc_urgent=0대형 파일, 공간 절약
IoT 임베디드noatime,mode=lfs,active_logs=2ram_thresh=5제한된 RAM, 순차 쓰기
ZNS SSDnoatime,mode=lfs(자동)존 매핑 자동 설정

성능 모니터링 체크리스트

#!/bin/bash - F2FS 성능 진단 스크립트
DEV=sda1

# 1. 여유 공간 상태
FREE=$(cat /sys/fs/f2fs/$DEV/free_segments)
DIRTY=$(cat /sys/fs/f2fs/$DEV/dirty_segments)
OVP=$(cat /sys/fs/f2fs/$DEV/ovp_segments)
echo "Free=$FREE Dirty=$DIRTY OVP=$OVP"

# 2. GC 상태
echo "GC urgent: $(cat /sys/fs/f2fs/$DEV/gc_urgent)"

# 3. 누적 기록량 (수명 추정)
WRITTEN=$(cat /sys/fs/f2fs/$DEV/lifetime_write_kbytes)
echo "Lifetime written: $((WRITTEN / 1048576)) GB"

# 4. extent cache 적중률
cat /sys/kernel/debug/f2fs/status | grep "extent"

# 5. BG/FG GC 횟수
cat /sys/kernel/debug/f2fs/status | grep "GC"

# 6. 경고: 여유 세그먼트가 OVP의 2배 미만이면 위험
if [ $FREE -lt $((OVP * 2)) ]; then
    echo "WARNING: 여유 공간 부족! FG_GC 빈발 가능"
    echo "  → gc_urgent=2 설정 또는 불필요 파일 삭제 권장"
fi
fstrim vs discard: discard 마운트 옵션은 블록 해제 시 즉시 TRIM을 발생시켜 I/O 지연이 발생할 수 있습니다. 성능이 중요한 환경에서는 nodiscard로 마운트하고, 주기적으로 fstrim을 실행(예: 주 1회)하는 것이 효율적입니다. Android에서는 IDLE maintenance 작업에서 fstrim을 수행합니다.

쓰기 증폭(WAF) 분석

쓰기 증폭(Write Amplification Factor)은 호스트가 요청한 쓰기량 대비 실제 디스크에 기록된 양의 비율입니다. F2FS에서 WAF를 측정하고 최적화하는 방법을 살펴봅니다:

# WAF 측정
# 호스트 쓰기량 (F2FS가 측정한 누적 기록량)
HOST_WRITTEN=$(cat /sys/fs/f2fs/sda1/lifetime_write_kbytes)

# 디바이스 쓰기량 (SSD SMART 데이터)
NAND_WRITTEN=$(smartctl -A /dev/sda | grep "Total_LBAs_Written" | awk '{print $10}')

# WAF = NAND 쓰기량 / 호스트 쓰기량
# 이상적 WAF = 1.0, 일반적 F2FS WAF = 1.1~2.5

# F2FS 내부 쓰기 유형별 통계
cat /sys/kernel/debug/f2fs/status | grep -A 20 "IO"
# 출력 예시:
# - data write: 1234567 blocks
# - node write: 234567 blocks
# - meta write: 34567 blocks
# - gc data write: 45678 blocks  ← GC에 의한 추가 쓰기
# - gc node write: 12345 blocks  ← GC에 의한 추가 쓰기

WAF 감소 전략

전략설정효과비용
OVP 증가mkfs.f2fs -o 10GC 빈도 30~50% 감소사용 가능 용량 10% 감소
active_logs=6마운트 옵션hot/cold 분리로 GC 효율 향상RAM 약간 증가
checkpoint_merge마운트 옵션CP I/O 40~60% 감소약간의 fsync 지연
압축 활성화compress_algorithm=lz4쓰기량 20~50% 감소CPU 사용량 증가
noatime마운트 옵션읽기 시 불필요한 쓰기 제거atime 정보 미갱신
BG GC 최적화gc_urgent=2 (IDLE)FG GC 빈도 감소IDLE 시 I/O 증가

Android 환경 튜닝 가이드

Android 기기에서 F2FS 성능을 최적화할 때 고려해야 할 주요 사항입니다:

# Android /data 파티션 권장 마운트 옵션 (Android 13+)
/dev/block/by-name/userdata /data f2fs \
    noatime,nosuid,nodev,\
    discard,\
    fsync_mode=nobarrier,\
    reserve_root=32768,\
    resgid=1065,\
    inlinecrypt,\
    checkpoint_merge,\
    gc_merge,\
    compress_algorithm=lz4,\
    compress_log_size=3,\
    compress_extension=*.apk,\
    compress_extension=*.dex,\
    compress_extension=*.so,\
    atgc,\
    age_extent_cache

# ATGC (Age-Threshold-based GC) 파라미터
# Android에서 GC 성능 개선을 위한 확장
echo 600 > /sys/fs/f2fs/sda1/atgc_candidate_ratio
echo 25 > /sys/fs/f2fs/sda1/atgc_candidate_count
echo 1073741824 > /sys/fs/f2fs/sda1/atgc_age_weight
echo 60 > /sys/fs/f2fs/sda1/atgc_age_threshold
ATGC(Age-Threshold GC): Google이 Android용으로 개발한 GC 정책 확장입니다. 세그먼트의 나이를 더 세밀하게 분석하여 Cost-Benefit 정책을 개선합니다. atgc 마운트 옵션으로 활성화하며, Android 12+에서 기본 적용됩니다.

SSD 수명 추정

F2FS의 누적 기록량 정보를 활용하여 SSD 수명을 추정할 수 있습니다:

#!/bin/bash - SSD 수명 추정 스크립트
DEV=sda1
ENDURANCE_TB=600  # SSD TBW 스펙 (테라바이트)

# 누적 기록량 (KB)
WRITTEN_KB=$(cat /sys/fs/f2fs/$DEV/lifetime_write_kbytes)
WRITTEN_TB=$(echo "scale=2; $WRITTEN_KB / 1073741824" | bc)

# 사용률
USED_PCT=$(echo "scale=1; $WRITTEN_TB * 100 / $ENDURANCE_TB" | bc)

echo "누적 기록: ${WRITTEN_TB} TB"
echo "SSD TBW: ${ENDURANCE_TB} TB"
echo "수명 소모: ${USED_PCT}%"

# 마운트 시간 (F2FS 내부 시계)
MOUNT_TIME=$(cat /sys/fs/f2fs/$DEV/mounted_time_sec)
DAYS=$((MOUNT_TIME / 86400))
DAILY_GB=$(echo "scale=1; $WRITTEN_KB / 1048576 / $DAYS" | bc)
echo "일 평균 기록: ${DAILY_GB} GB/day"

# 남은 수명 추정
REMAIN_TB=$(echo "scale=2; $ENDURANCE_TB - $WRITTEN_TB" | bc)
REMAIN_DAYS=$(echo "scale=0; $REMAIN_TB * 1048576 / $DAILY_GB" | bc 2>/dev/null)
echo "예상 잔여 수명: 약 ${REMAIN_DAYS} 일"

I/O 패턴 분석과 최적화

F2FS의 I/O 통계를 분석하여 워크로드 특성을 파악하고 최적화 방향을 결정합니다:

# iostat 활성화 (마운트 옵션에 iostat 추가)
mount -o remount,iostat /dev/sda1 /data

# I/O 통계 확인
cat /sys/kernel/debug/f2fs/sda1/iostat_info

# 주요 관찰 항목:
# - app_dio_read / app_dio_write : Direct I/O
# - app_bio_read / app_bio_write : Buffered I/O
# - fs_gc_data_io / fs_gc_node_io : GC I/O
# - fs_cp_data_io / fs_cp_node_io : CP I/O

# GC I/O 비율이 높으면 → OVP 증가 또는 gc_urgent 조정
# CP I/O 비율이 높으면 → checkpoint_merge 활성화
# DIO 비율이 높으면 → 버퍼 캐시 효율 점검

# 세그먼트 사용 패턴 확인
cat /sys/kernel/debug/f2fs/status | grep -E "segment|utilization"
# 높은 utilization(>80%)은 FG_GC 빈발의 원인
F2FS vs ext4 랜덤 쓰기 성능: 일반적으로 F2FS는 ext4 대비 랜덤 4KB 쓰기에서 2~5배 높은 IOPS를 달성합니다. 이는 F2FS의 append-only 패턴이 FTL의 내부 GC를 최소화하기 때문입니다. 반면 순차 쓰기에서는 ext4와 비슷하거나 약간 낮을 수 있습니다 (F2FS의 메타데이터 오버헤드).

일반적인 문제와 해결

증상원인해결
쓰기 지연 스파이크FG_GC 발생OVP 증가, IDLE 시 gc_urgent=2
마운트 시간 길어짐Roll-forward 복구정상 unmount 보장, fsck 수행
공간 부족 (df에는 여유)예약 블록/OVPreserve_root 조정
높은 CPU 사용압축/해제 부하LZ4 선택 또는 압축 비활성화
fsync 느림매번 CP 트리거checkpoint_merge, fsync_mode 조정
디스크 수명 급감높은 WAFactive_logs=6, noatime, 압축
extent cache miss 높음단편화 심각defrag.f2fs 실행
OOM 발생F2FS 캐시 과다ram_thresh 감소 (기본 10%)

벤치마크 비교 참고

테스트F2FS (기본)F2FS (최적화)ext4비고
랜덤 4K 쓰기 (IOPS)~45,000~52,000~18,000F2FS 압도적 우위
랜덤 4K 읽기 (IOPS)~48,000~50,000~47,000비슷
순차 쓰기 (MB/s)~420~445~460ext4 약간 우위
순차 읽기 (MB/s)~480~485~490비슷
SQLite insert (TPS)~1,200~1,800~600F2FS 최적화 큰 효과
SQLite update (TPS)~900~1,400~450atomic write 효과
파일 생성 (files/s)~8,500~9,200~5,500인라인 데이터 효과
마운트 시간 (ms)~120~100~80ext4 약간 빠름
벤치마크 주의사항: 위 수치는 특정 하드웨어/커널 버전/워크로드에서의 대략적 참고값입니다. 실제 성능은 SSD 종류, FTL 구현, 파일 크기 분포, 동시 접근 패턴 등에 따라 크게 달라집니다. 정확한 비교를 위해서는 대상 환경에서 fio, dbench, filebench 등으로 직접 측정하세요.

F2FS와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.