Btrfs 파일시스템 심화
Btrfs의 핵심인 COW B-tree 설계를 바탕으로 트랜잭션 커밋, 루트 트리/청크 트리/extent 트리 관계, 서브볼륨과 스냅샷의 메타데이터 공유 모델, send/receive 증분 복제, 데이터·메타데이터 체크섬, 압축과 RAID 프로파일 선택, scrub/balance/defrag 운영 전략, ENOSPC와 조각화 대응까지 실제 운영 이슈 중심으로 상세히 정리합니다.
핵심 요약
- 계층 이해 — VFS, 캐시, 하위 FS 경계를 구분합니다.
- 메타데이터 우선 — inode/dentry 일관성을 먼저 확인합니다.
- 저장 정책 — 저널링/압축/할당 정책 차이를 비교합니다.
- 일관성 모델 — 로컬/원격/합성 FS의 반영 시점을 구분합니다.
- 복구 관점 — 장애 시 재구성 경로를 함께 점검합니다.
단계별 이해
- 경계 계층 파악
요청이 VFS에서 어디로 내려가는지 확인합니다. - 메타/데이터 분리
어느 경로에서 무엇이 갱신되는지 나눠 봅니다. - 동기화/플러시 확인
쓰기 반영 시점과 순서를 검증합니다. - 복구 시나리오 점검
비정상 종료 후 일관성 회복을 확인합니다.
개요 & 역사
Btrfs(B-tree File System, "버터 FS"로 발음)는 Copy-on-Write(COW) 기반의 차세대 Linux 파일시스템입니다. Oracle의 Chris Mason이 2007년 개발을 시작하여, Linux 2.6.29(2009)에서 메인라인에 병합되었습니다.
개발 역사
| 시기 | 커널 버전 | 주요 이정표 |
|---|---|---|
| 2007 | - | Chris Mason(Oracle)이 개발 시작, ZFS 대안 목표 |
| 2009 | 2.6.29 | 메인라인 병합, 기본 COW + B-tree 구조 |
| 2012 | 3.4 | send/receive, qgroup, device replace 추가 |
| 2013 | 3.9 | RAID 5/6 실험적 지원, skinny metadata |
| 2018 | 4.14+ | zstd 압축 지원 추가 |
| 2020 | 5.5 | space_cache v2 (free space tree) 기본 활성화 |
| 2022 | 5.15+ | Fedora 33+ 기본 FS 채택, SUSE 엔터프라이즈 지원 |
| 2024 | 6.7+ | RAID1 성능 개선, extent tree v2 개발 진행 |
주요 스펙
| 항목 | 값 |
|---|---|
| 최대 볼륨 크기 | 16 EiB |
| 최대 파일 크기 | 16 EiB (논리적), 실질적으로 볼륨 크기에 제한 |
| 최대 파일 수 | 264 |
| 파일명 길이 | 255 바이트 |
| 블록(섹터) 크기 | 4K (기본), metadata nodesize 16K (기본) |
| 체크섬 | crc32c (기본), xxhash, sha256, blake2b |
| 내장 RAID | 0, 1, 10, 5/6 (실험적), DUP |
| COW | 전체 메타데이터 + 데이터 (기본) |
| 압축 | zlib, lzo, zstd |
| 타임스탬프 범위 | 1970-01-01 ~ 2486 (나노초 정밀도) |
설계 철학
Btrfs의 설계는 세 가지 핵심 원칙에 기반합니다:
- COW 기반 일관성 — 기존 데이터를 덮어쓰지 않으므로 저널링이 불필요하며, 전원 장애 시에도 항상 일관된 상태 보장
- B-tree 통합 관리 — 파일 데이터, 메타데이터, 체크섬, 디렉토리 등 모든 요소를 B-tree로 통합 관리
- 스토리지 풀 — 여러 물리 장치를 하나의 볼륨으로 통합하고, 데이터/메타데이터에 서로 다른 RAID 프로파일 적용 가능
아키텍처 & 디스크 레이아웃
Btrfs는 ext4처럼 고정 크기 Block Group을 사용하지 않습니다. 대신 Chunk 기반 동적 할당으로 스토리지를 관리하며, 모든 메타데이터를 B-tree에 저장합니다.
Superblock
Btrfs superblock은 디바이스의 고정 오프셋(64K, 64M, 256G)에 위치하며, 파일시스템 부팅에 필요한 핵심 정보를 담고 있습니다:
/* fs/btrfs/ctree.h */
struct btrfs_super_block {
/* 체크섬: 첫 32바이트는 나머지 블록의 체크섬 */
u8 csum[BTRFS_CSUM_SIZE]; /* 32B */
u8 fsid[BTRFS_FSID_SIZE]; /* 16B - 파일시스템 UUID */
__le64 bytenr; /* superblock 자신의 위치 */
__le64 flags;
__le64 magic; /* "_BHRfS_M" */
__le64 generation; /* 트랜잭션 세대 번호 */
__le64 root; /* Root Tree의 루트 블록 위치 */
__le64 chunk_root; /* Chunk Tree 루트 */
__le64 log_root; /* Tree-log 루트 */
__le64 total_bytes; /* 전체 크기 */
__le64 bytes_used; /* 사용된 바이트 */
__le64 num_devices; /* 디바이스 수 */
__le32 sectorsize; /* 섹터 크기 (보통 4096) */
__le32 nodesize; /* B-tree 노드 크기 (보통 16384) */
__le16 csum_type; /* 체크섬 알고리즘 */
/* ... 추가 필드 생략 ... */
u8 sys_chunk_array[BTRFS_SYSTEM_CHUNK_ARRAY_SIZE]; /* 부트스트랩 chunk 매핑 */
} __attribute__((packed));
핵심 B-tree 종류
| Tree | Object ID | 역할 |
|---|---|---|
| Root Tree | 1 | "tree of trees" — 모든 서브 트리의 루트 포인터 저장 |
| Extent Tree | 2 | 데이터/메타데이터 extent의 할당 상태, 역참조(back-reference) |
| Chunk Tree | 3 | 논리 → 물리 주소 매핑 (Chunk 할당) |
| Dev Tree | 4 | 물리 디바이스별 할당 상태 |
| FS Tree | 5+ (서브볼륨별) | 파일/디렉토리 메타데이터, extent 참조 |
| Checksum Tree | 7 | 데이터 extent의 체크섬 |
| Free Space Tree | 10 | 블록 그룹별 미사용 공간 (space_cache v2) |
| UUID Tree | 9 | 서브볼륨 UUID → ID 매핑 (send/receive용) |
| Quota Tree | 8 | 서브볼륨 공간 할당량(qgroup) 추적 |
| Tree-Log | -7~-8 | fsync 최적화용 임시 트리 (재부팅 시 replay) |
B-tree 노드 구조
Btrfs의 모든 B-tree 노드는 nodesize(기본 16K) 크기이며, 헤더 + 아이템 배열로 구성됩니다:
/* B-tree 노드 헤더 */
struct btrfs_header {
u8 csum[BTRFS_CSUM_SIZE]; /* 노드 전체의 체크섬 */
u8 fsid[BTRFS_FSID_SIZE]; /* 파일시스템 UUID */
__le64 bytenr; /* 이 노드의 논리 주소 */
__le64 flags;
u8 chunk_tree_uuid[BTRFS_UUID_SIZE];
__le64 generation; /* COW 시점의 트랜잭션 번호 */
__le64 owner; /* 이 노드가 속한 tree ID */
__le32 nritems; /* 아이템 수 */
u8 level; /* 0=리프, 1+=내부 노드 */
};
/* 리프 노드 아이템 */
struct btrfs_item {
struct btrfs_disk_key key; /* (objectid, type, offset) */
__le32 offset; /* 리프 내 데이터 시작 오프셋 */
__le32 size; /* 데이터 크기 */
};
Chunk 매핑
Btrfs는 논리 주소(logical address)와 물리 주소(physical address)를 분리합니다. Chunk Tree가 논리 → 물리 매핑을 담당하며, 이를 통해 여러 디바이스에 걸친 RAID 배치가 가능합니다:
/* Chunk 매핑 정보 */
struct btrfs_chunk {
__le64 length; /* chunk 크기 */
__le64 owner; /* Extent Tree objectid */
__le64 stripe_len; /* stripe 길이 (보통 64K) */
__le64 type; /* DATA | METADATA | SYSTEM + RAID 프로파일 */
__le16 num_stripes; /* stripe 수 */
__le16 sub_stripes; /* RAID10용 sub-stripe 수 */
struct btrfs_stripe stripe[]; /* 가변 길이 stripe 배열 */
};
Copy-on-Write (COW)
COW는 Btrfs의 가장 근본적인 메커니즘입니다. 기존 블록을 직접 수정하지 않고 새 위치에 수정된 데이터를 기록한 후 포인터를 갱신합니다.
COW 동작 원리
파일의 한 블록을 수정하면 다음 과정이 일어납니다:
- 새로운 데이터 블록에 수정된 내용 기록
- 해당 리프 노드를 새 위치에 COW (새 데이터 블록 포인터 포함)
- 부모 노드들을 루트까지 재귀적으로 COW
- Superblock에 새 루트 위치 기록 (atomic commit point)
- 이전 블록들은 해제 대상이 됨 (스냅샷이 참조하지 않는 경우)
트랜잭션 모델
Btrfs는 COW를 기반으로 세대(generation) 기반 트랜잭션을 구현합니다:
/* fs/btrfs/transaction.h */
struct btrfs_transaction {
u64 transid; /* 트랜잭션 ID (generation) */
atomic_t num_writers; /* 현재 writer 수 */
atomic_t use_count; /* 참조 카운트 */
enum btrfs_trans_state state; /* RUNNING → COMMIT_START → COMMIT_DOING → COMMITTED */
struct list_head list; /* 트랜잭션 리스트 */
struct extent_io_tree dirty_pages; /* 더티 페이지 추적 */
};
/* 트랜잭션 참여 */
struct btrfs_trans_handle *btrfs_start_transaction(
struct btrfs_root *root, unsigned int num_items);
int btrfs_commit_transaction(struct btrfs_trans_handle *trans);
커밋 과정은 두 단계로 진행됩니다:
- 트랜잭션 실행: 모든 수정 사항이 메모리에 COW로 반영되고, 새 블록들이 디스크에 기록됨
- 슈퍼블록 기록: 모든 데이터가 디스크에 안착한 후 슈퍼블록의 root 포인터를 원자적으로 갱신 (commit point)
NODATACOW
NODATACOW 설정 시 해당 파일의 데이터는 COW가 비활성화되어 체크섬도 비활성화됩니다. 데이터베이스 파일이나 VM 이미지처럼 랜덤 쓰기가 빈번한 경우에 유용하지만, 데이터 무결성 검증이 불가능해집니다.
# 특정 파일/디렉토리에 NODATACOW 속성 설정
$ chattr +C /path/to/file
# 마운트 옵션으로 전체 적용
$ mount -o nodatacow /dev/sda1 /mnt
# 상태 확인
$ lsattr /path/to/file
---------------C---- /path/to/file
서브볼륨 & 스냅샷
서브볼륨(subvolume)은 Btrfs의 핵심 기능으로, 하나의 파일시스템 내에서 독립적인 POSIX 파일 트리를 형성합니다. 각 서브볼륨은 고유한 FS Tree(tree ID)를 가지며, 스냅샷은 서브볼륨의 특수한 형태입니다.
서브볼륨 개념
# 서브볼륨 생성
$ btrfs subvolume create /mnt/@rootfs
$ btrfs subvolume create /mnt/@home
# 서브볼륨 목록 확인
$ btrfs subvolume list /mnt
ID 256 gen 100 top level 5 path @rootfs
ID 257 gen 100 top level 5 path @home
# 특정 서브볼륨으로 마운트
$ mount -o subvol=@rootfs /dev/sda1 /
$ mount -o subvol=@home /dev/sda1 /home
# 서브볼륨별 마운트 (subvolid 사용)
$ mount -o subvolid=256 /dev/sda1 /
스냅샷
스냅샷은 COW를 활용하여 서브볼륨의 시점 복사본을 O(1) 시간에 생성합니다:
# 읽기 전용 스냅샷 (백업/복구에 권장)
$ btrfs subvolume snapshot -r /home /mnt/@snap-home-$(date +%Y%m%d)
# 쓰기 가능 스냅샷
$ btrfs subvolume snapshot /home /mnt/@home-work
# 스냅샷 삭제
$ btrfs subvolume delete /mnt/@snap-home-20240101
# 스냅샷에서 롤백 (서브볼륨 교체 방식)
$ btrfs subvolume delete /mnt/@rootfs
$ btrfs subvolume snapshot /mnt/@snap-rootfs-good /mnt/@rootfs
Send/Receive
Btrfs send/receive는 스냅샷 간 차이(delta)를 스트림으로 전송하여 증분 백업이나 원격 복제를 구현합니다:
# 전체 전송 (최초 백업)
$ btrfs send /mnt/@snap-day1 | btrfs receive /backup/
# 증분 전송 (이전 스냅샷 대비 변경분만)
$ btrfs send -p /mnt/@snap-day1 /mnt/@snap-day2 | btrfs receive /backup/
# SSH를 통한 원격 전송
$ btrfs send -p /mnt/@snap-day1 /mnt/@snap-day2 | \
ssh remote_host btrfs receive /backup/
# 파일로 저장
$ btrfs send /mnt/@snap-day1 -f /tmp/snap-day1.btrfs
btrfs send의 --compressed-data 옵션(커널 5.18+)은 압축된 데이터를 그대로 전송하여 CPU 사용을 줄입니다.
기본 서브볼륨
# 기본 서브볼륨 변경 (부팅 시 자동 마운트될 서브볼륨)
$ btrfs subvolume set-default 256 /mnt
# 현재 기본 서브볼륨 확인
$ btrfs subvolume get-default /mnt
ID 256 gen 100 top level 5 path @rootfs
데이터 무결성
Btrfs는 모든 데이터와 메타데이터에 체크섬을 적용하여 bit rot, 불량 섹터, 기타 무음 데이터 손상(silent corruption)을 탐지합니다.
체크섬 알고리즘
| 알고리즘 | 크기 | 유형 | 커널 버전 | 특징 |
|---|---|---|---|---|
| crc32c | 4B | 비암호화 | 기본 | CPU 가속(SSE4.2), 높은 성능, 대부분의 워크로드에 적합 |
| xxhash | 8B | 비암호화 | 5.5+ | crc32c 대비 더 강한 충돌 저항, ARM에서 빠름 |
| sha256 | 32B | 암호화 | 5.5+ | 높은 보안 수준, 성능 오버헤드 큼 |
| blake2b | 32B | 암호화 | 5.5+ | sha256 대비 빠른 암호화 해시, 보안 + 성능 균형 |
# 파일시스템 생성 시 체크섬 알고리즘 지정 (포맷 후 변경 불가)
$ mkfs.btrfs --csum xxhash /dev/sda1
$ mkfs.btrfs --csum blake2b /dev/sda1
# 현재 사용 중인 체크섬 확인
$ btrfs inspect-internal dump-super /dev/sda1 | grep csum_type
csum_type xxhash64 (2)
Checksum Tree
데이터 블록의 체크섬은 Checksum Tree(tree ID=7)에 저장됩니다. 각 항목은 연속된 데이터 블록 범위의 체크섬 배열입니다:
/* fs/btrfs/file-item.c - 체크섬 조회 */
int btrfs_lookup_csums_range(
struct btrfs_root *root,
u64 start, /* 시작 바이트 오프셋 */
u64 end, /* 끝 바이트 오프셋 */
struct list_head *list, /* 결과 체크섬 리스트 */
int search_commit /* 커밋된 루트에서 검색 여부 */
);
/* 체크섬 검증 흐름:
* 1. 데이터 블록 읽기
* 2. Checksum Tree에서 저장된 체크섬 조회
* 3. 읽은 데이터로 체크섬 계산
* 4. 비교 → 불일치 시 -EIO 또는 미러에서 복구 시도
*/
Scrub
Scrub은 온라인 상태에서 전체 파일시스템의 데이터 무결성을 검증하는 백그라운드 작업입니다:
# scrub 시작
$ btrfs scrub start /mnt
# 진행 상태 확인
$ btrfs scrub status /mnt
UUID: 12345678-...
Scrub started: Mon Jan 1 00:00:00 2024
Status: running
Duration: 0:05:32
Total to scrub: 100.00GiB
Bytes scrubbed: 45.20GiB (45.20%)
Rate: 140.00MiB/s
Error summary: csum=0
# scrub 일시 중지 / 재개
$ btrfs scrub cancel /mnt
$ btrfs scrub resume /mnt
자가 복구
RAID1/10/DUP 구성에서 체크섬 불일치가 감지되면, Btrfs는 정상 미러에서 데이터를 읽어 손상된 복사본을 자동으로 복구합니다:
- fs/btrfs/raid56.c, volumes.c - 자가 복구 흐름 */
- 읽기 I/O 완료 → 체크섬 검증 실패
- bio에서 미러 번호 확인
- 다른 미러(mirror_num)에서 동일 블록 재읽기
- 체크섬 검증 성공 시:
- 정상 데이터를 요청자에게 반환
- 손상된 미러에 정상 데이터를 기록 (repair)
- 모든 미러 실패 시 → -EIO 반환
압축
Btrfs는 파일 데이터의 투명 압축(transparent compression)을 지원하여 디스크 공간을 절약하고, 특정 워크로드에서는 I/O 대역폭도 개선합니다.
압축 알고리즘
| 알고리즘 | 커널 버전 | 압축률 | 속도 | 레벨 | 특징 |
|---|---|---|---|---|---|
| zlib | 초기 | 높음 | 느림 | 1~9 (기본 3) | 전통적, 최고 압축률이 필요할 때 |
| lzo | 2.6.38 | 낮음 | 빠름 | 없음 | CPU 오버헤드 최소, 임베디드/SBC에 적합 |
| zstd | 4.14 | 높음 | 빠름 | 1~15 (기본 3) | 최신, 최적의 압축률/속도 균형 (권장) |
압축 설정
# 마운트 옵션으로 전체 압축 활성화
$ mount -o compress=zstd /dev/sda1 /mnt
$ mount -o compress=zstd:3 /dev/sda1 /mnt # 레벨 지정
$ mount -o compress-force=zstd /dev/sda1 /mnt # 강제 압축 (비압축성 데이터 포함)
# 파일/디렉토리별 압축 설정 (btrfs property)
$ btrfs property set /mnt/logs compression zstd
$ btrfs property get /mnt/logs compression
compression=zstd
# 기존 파일 재압축
$ btrfs filesystem defragment -r -czstd /mnt/data/
내부 구조
Btrfs 압축은 최대 128K 단위로 동작합니다. 파일의 각 extent는 독립적으로 압축되며, 압축 유형은 extent 메타데이터에 기록됩니다:
/* fs/btrfs/ctree.h - extent 데이터 */
struct btrfs_file_extent_item {
__le64 generation;
__le64 ram_bytes; /* 압축 전 크기 (원본) */
u8 compression; /* 0=없음, 1=zlib, 2=lzo, 3=zstd */
u8 encryption; /* 미사용 (예약) */
__le16 other_encoding; /* 미사용 */
u8 type; /* INLINE / REG / PREALLOC */
__le64 disk_bytenr; /* 디스크상의 시작 위치 */
__le64 disk_num_bytes; /* 압축 후 디스크 크기 */
__le64 offset; /* extent 내 오프셋 */
__le64 num_bytes; /* 논리 크기 */
};
/* 압축 비율 확인 */
$ compsize /mnt
Processed 12345 files, 6789 regular extents (7000 refs)
Type Perc Disk Usage Uncompressed
TOTAL 65% 6.5G 10G
zstd 62% 5.8G 9.3G
none 100% 700M 700M
RAID & 멀티 디바이스
Btrfs는 볼륨 매니저(LVM) 없이 자체적으로 여러 디바이스를 관리하고 RAID를 구성합니다. 데이터와 메타데이터에 서로 다른 RAID 프로파일을 적용할 수 있습니다.
RAID 프로파일
| 프로파일 | 최소 디바이스 | 중복도 | 가용 용량 | 읽기 성능 | 상태 |
|---|---|---|---|---|---|
| single | 1 | 없음 | 100% | 1x | 안정 |
| DUP | 1 | 2 복사 (같은 디바이스) | ~50% | 1x | 안정 (메타데이터 기본) |
| RAID0 | 2 | 없음 | N × 최소 | Nx | 안정 |
| RAID1 | 2 | 2 복사 (다른 디바이스) | ~50% | 2x | 안정 |
| RAID1C3 | 3 | 3 복사 | ~33% | 3x | 안정 (5.5+) |
| RAID1C4 | 4 | 4 복사 | ~25% | 4x | 안정 (5.5+) |
| RAID10 | 4 | 2 복사 + 스트라이핑 | ~50% | Nx | 안정 |
| RAID5 | 3 | 1 패리티 | (N-1) × 최소 | (N-1)x | 불안정 |
| RAID6 | 4 | 2 패리티 | (N-2) × 최소 | (N-2)x | 불안정 |
디바이스 관리
# 멀티 디바이스 파일시스템 생성
$ mkfs.btrfs -d raid1 -m raid1 /dev/sda /dev/sdb
# 디바이스 추가
$ btrfs device add /dev/sdc /mnt
$ btrfs balance start -dconvert=raid1 -mconvert=raid1 /mnt
# 디바이스 제거
$ btrfs device remove /dev/sdb /mnt
# 결함 디바이스 교체 (온라인)
$ btrfs replace start /dev/sdb /dev/sdd /mnt
$ btrfs replace status /mnt
# 디바이스 사용량 확인
$ btrfs device usage /mnt
/dev/sda, ID: 1
Device size: 500.00GiB
Data,RAID1: 200.00GiB
Metadata,RAID1: 10.00GiB
System,RAID1: 32.00MiB
Unallocated: 289.97GiB
프로파일 변환
# single → RAID1 변환 (온라인)
$ btrfs balance start -dconvert=raid1 -mconvert=raid1 /mnt
# RAID1 → RAID10 변환 (4+ 디바이스 필요)
$ btrfs balance start -dconvert=raid10 /mnt
# 필터를 사용한 부분 변환
$ btrfs balance start -dconvert=raid1,soft /mnt # 이미 raid1인 chunk는 건너뜀
핵심 커널 자료구조
Btrfs 커널 코드(fs/btrfs/)의 핵심 자료구조를 살펴봅니다.
btrfs_key
B-tree의 모든 아이템은 (objectid, type, offset) 3-tuple 키로 정렬됩니다:
struct btrfs_key {
__le64 objectid; /* 대상 객체 ID (inode 번호, tree ID 등) */
u8 type; /* 아이템 타입 */
__le64 offset; /* 타입별 의미 다름 (offset, size 등) */
};
주요 아이템 타입:
| 상수 | 값 | objectid 의미 | offset 의미 |
|---|---|---|---|
BTRFS_INODE_ITEM_KEY | 1 | inode 번호 | 0 |
BTRFS_DIR_ITEM_KEY | 84 | 부모 inode | 이름의 crc32c 해시 |
BTRFS_DIR_INDEX_KEY | 96 | 부모 inode | 시퀀스 번호 |
BTRFS_EXTENT_DATA_KEY | 108 | inode 번호 | 파일 내 오프셋 |
BTRFS_EXTENT_ITEM_KEY | 168 | 바이트 오프셋 | extent 크기 |
BTRFS_CHUNK_ITEM_KEY | 228 | tree ID (보통 256) | 논리 오프셋 |
BTRFS_ROOT_ITEM_KEY | 132 | tree ID | 0 또는 transid |
btrfs_fs_info
파일시스템 전체의 런타임 상태를 관리하는 최상위 구조체:
/* fs/btrfs/fs.h */
struct btrfs_fs_info {
struct btrfs_root *tree_root; /* Root Tree */
struct btrfs_root *chunk_root; /* Chunk Tree */
struct btrfs_root *extent_root; /* Extent Tree */
struct btrfs_root *csum_root; /* Checksum Tree */
struct btrfs_root *uuid_root; /* UUID Tree */
struct btrfs_root *free_space_root; /* Free Space Tree */
struct super_block *sb; /* VFS super_block */
u64 generation; /* 현재 트랜잭션 세대 */
u64 last_trans_committed; /* 마지막 커밋된 세대 */
struct btrfs_transaction *running_transaction;
struct btrfs_space_info *data_sinfo; /* 데이터 공간 정보 */
struct btrfs_space_info *meta_sinfo; /* 메타데이터 공간 정보 */
unsigned long mount_opt; /* 마운트 옵션 비트필드 */
u32 sectorsize; /* 섹터 크기 */
u32 nodesize; /* B-tree 노드 크기 */
/* ... 수백 개의 필드 생략 ... */
};
btrfs_root
/* fs/btrfs/ctree.h */
struct btrfs_root {
struct rb_node rb_node; /* fs_info의 rbtree에 연결 */
struct extent_buffer *node; /* 루트 노드 (메모리) */
struct extent_buffer *commit_root; /* 커밋된 루트 (읽기용) */
struct btrfs_root_item root_item; /* 디스크 아이템 */
struct btrfs_key root_key; /* Root Tree 내의 키 */
struct btrfs_fs_info *fs_info; /* 역참조 */
u64 root_key_objectid; /* tree ID */
u64 last_trans; /* 마지막 수정 트랜잭션 */
};
btrfs_inode
/* fs/btrfs/btrfs_inode.h */
struct btrfs_inode {
struct btrfs_root *root; /* 소속 FS Tree */
struct btrfs_key location; /* (ino, INODE_ITEM, 0) */
u64 disk_i_size; /* 디스크 상 파일 크기 */
u64 generation; /* 생성 트랜잭션 */
u64 flags; /* NODATACOW, COMPRESS 등 */
struct extent_io_tree io_tree; /* I/O 상태 추적 */
struct inode vfs_inode; /* VFS inode (임베딩) */
};
btrfs_path
B-tree 검색/순회에 사용되는 경로 구조체:
/* fs/btrfs/ctree.h */
struct btrfs_path {
struct extent_buffer *nodes[BTRFS_MAX_LEVEL]; /* 루트→리프 노드 배열 */
int slots[BTRFS_MAX_LEVEL]; /* 각 레벨의 슬롯 인덱스 */
u8 locks[BTRFS_MAX_LEVEL]; /* 잠금 상태 */
int keep_locks; /* 순회 시 잠금 유지 */
int lowest_level; /* 검색 중단 레벨 */
};
/* B-tree 검색 예시 */
struct btrfs_path *path = btrfs_alloc_path();
struct btrfs_key key = { .objectid = ino, .type = BTRFS_INODE_ITEM_KEY, .offset = 0 };
int ret = btrfs_search_slot(trans, root, &key, path, 0, 0);
/* ret == 0: 정확한 키 발견
* ret > 0: 키 없음, path는 삽입 위치
* ret < 0: 에러 */
btrfs_free_path(path);
공간 관리
Btrfs의 공간 관리는 블록 그룹(Chunk), 미사용 공간 추적, balance, defrag 등 여러 메커니즘으로 구성됩니다.
블록 그룹
Btrfs는 스토리지를 블록 그룹(Block Group) 단위로 관리합니다. 각 블록 그룹은 하나의 Chunk에 대응하며, DATA/METADATA/SYSTEM 타입 중 하나를 가집니다:
# 블록 그룹 할당 상태 확인
$ btrfs filesystem usage /mnt
Overall:
Device size: 1.00TiB
Device allocated: 500.03GiB
Device unallocated: 523.97GiB
Used: 400.00GiB
Data,RAID1: Size:240.00GiB, Used:195.50GiB (81.46%)
Metadata,RAID1: Size:10.00GiB, Used:5.20GiB (52.00%)
System,RAID1: Size:32.00MiB, Used:48.00KiB (0.15%)
미사용 공간 추적
Btrfs는 두 가지 방식으로 블록 그룹 내 미사용 공간을 추적합니다:
| 방식 | 저장 위치 | 특징 |
|---|---|---|
| space_cache v1 | 숨겨진 inode 파일 | 기존 방식, 마운트 시 전체 로드 |
| space_cache v2 (Free Space Tree) | 전용 B-tree (tree ID=10) | 5.15+ 기본값, COW 보호, 빠른 마운트 |
# space_cache v2 강제 활성화
$ mount -o space_cache=v2 /dev/sda1 /mnt
# space_cache 상태 확인
$ btrfs inspect-internal dump-super /dev/sda1 | grep cache
compat_ro_flags 0x1
( FREE_SPACE_TREE | FREE_SPACE_TREE_VALID )
Balance
Balance는 블록 그룹 간 데이터를 재배치하는 작업입니다. RAID 프로파일 변환, 디바이스 추가/제거 후 균등 분배, 공간 회수에 사용됩니다:
# 전체 balance (모든 블록 그룹 재배치 - 시간 많이 소요)
$ btrfs balance start /mnt
# 사용률이 낮은 블록 그룹만 balance (공간 회수)
$ btrfs balance start -dusage=50 /mnt # 사용률 50% 미만 데이터 그룹
$ btrfs balance start -musage=50 /mnt # 사용률 50% 미만 메타데이터 그룹
# balance 상태 / 취소
$ btrfs balance status /mnt
$ btrfs balance cancel /mnt
Defrag
# 파일 단편화 해소
$ btrfs filesystem defragment /mnt/large_file
# 디렉토리 재귀 defrag + 압축 적용
$ btrfs filesystem defragment -r -czstd /mnt/data/
# defrag 범위 지정 (오프셋 + 길이)
$ btrfs filesystem defragment -s 0 -l 1G /mnt/large_file
Resize
# 파일시스템 확장 (온라인)
$ btrfs filesystem resize +100G /mnt
$ btrfs filesystem resize max /mnt # 디바이스 전체로 확장
# 파일시스템 축소 (온라인, 주의 필요)
$ btrfs filesystem resize -50G /mnt
# 특정 디바이스만 resize (devid 지정)
$ btrfs filesystem resize 1:+100G /mnt
ENOSPC 문제
Btrfs는 Chunk 단위 할당 방식 때문에 디스크에 빈 공간이 있어도 ENOSPC가 발생할 수 있습니다:
# ENOSPC 진단
$ btrfs filesystem usage /mnt # allocated vs unallocated 확인
$ btrfs filesystem df /mnt # 타입별 사용량
# ENOSPC 대처: 사용률 낮은 블록 그룹 재배치
$ btrfs balance start -dusage=0 /mnt # 빈 데이터 그룹 해제
$ btrfs balance start -dusage=10 /mnt # 10% 미만 그룹 통합
$ btrfs balance start -musage=10 /mnt # 메타데이터도 동일
관리 도구 (btrfs-progs)
btrfs-progs는 Btrfs 사용자 공간 도구 모음입니다. btrfs 명령어를 통해 파일시스템 관리의 모든 측면을 제어합니다.
주요 명령어
| 명령어 | 설명 | 예시 |
|---|---|---|
mkfs.btrfs | 파일시스템 생성 | mkfs.btrfs -L myfs -d raid1 /dev/sd{a,b} |
btrfs filesystem | FS 관리 (usage, df, resize, defrag, show) | btrfs fi usage /mnt |
btrfs subvolume | 서브볼륨 관리 (create, delete, list, snapshot) | btrfs sub list /mnt |
btrfs device | 디바이스 관리 (add, remove, usage) | btrfs dev usage /mnt |
btrfs balance | 블록 그룹 재배치 | btrfs bal start -dusage=50 /mnt |
btrfs scrub | 데이터 무결성 검증 | btrfs scrub start /mnt |
btrfs replace | 디바이스 교체 | btrfs replace start /dev/sda /dev/sdb /mnt |
btrfs send/receive | 스냅샷 전송/수신 | btrfs send /snap | btrfs receive /backup |
btrfs check | 오프라인 검사 (fsck) | btrfs check /dev/sda1 |
btrfs rescue | 복구 도구 | btrfs rescue super-recover /dev/sda1 |
btrfs property | 속성 관리 | btrfs prop set /dir compression zstd |
btrfs quota | 할당량 관리 | btrfs quota enable /mnt |
btrfs qgroup | 쿼터 그룹 관리 | btrfs qgroup show /mnt |
filesystem usage
# 가장 유용한 공간 확인 명령어
$ btrfs filesystem usage /mnt
Overall:
Device size: 1.00TiB
Device allocated: 600.03GiB
Device unallocated: 423.97GiB
Device missing: 0.00B
Device slack: 0.00B
Used: 450.00GiB
Free (estimated): 350.00GiB (min: 250.00GiB)
Free (statfs, df): 350.00GiB
Data ratio: 2.00
Metadata ratio: 2.00
Global reserve: 512.00MiB (used: 0.00B)
Multiple profiles: no
Data,RAID1: Size:280.00GiB, Used:220.00GiB (78.57%)
/dev/sda 280.00GiB
/dev/sdb 280.00GiB
Metadata,RAID1: Size:20.00GiB, Used:5.00GiB (25.00%)
/dev/sda 20.00GiB
/dev/sdb 20.00GiB
System,RAID1: Size:32.00MiB, Used:48.00KiB (0.15%)
/dev/sda 32.00MiB
/dev/sdb 32.00MiB
Unallocated:
/dev/sda 211.97GiB
/dev/sdb 211.97GiB
inspect-internal
# superblock 정보 덤프
$ btrfs inspect-internal dump-super /dev/sda1
# B-tree 덤프 (디버깅용)
$ btrfs inspect-internal dump-tree /dev/sda1
# 특정 tree만 덤프
$ btrfs inspect-internal dump-tree -t 2 /dev/sda1 # Extent Tree
# inode → 경로 역참조
$ btrfs inspect-internal inode-resolve 256 /mnt
/mnt/some/file.txt
# 논리 주소 → 물리 주소 변환
$ btrfs inspect-internal logical-resolve 12345678 /mnt
성능 튜닝
Mount 옵션
| 옵션 | 기본값 | 설명 |
|---|---|---|
compress=zstd:N | 없음 | 투명 압축 (zstd 권장, 레벨 1~15) |
compress-force=zstd | 없음 | 비압축성 파일도 강제 압축 시도 |
ssd | 자동 감지 | SSD 최적화 활성화 |
discard=async | 없음 | 비동기 TRIM (SSD 필수) |
noatime | relatime | atime 갱신 비활성화 (성능 향상) |
space_cache=v2 | v2 (5.15+) | Free Space Tree 사용 |
autodefrag | 없음 | 자동 조각 모음 (랜덤 쓰기 워크로드) |
commit=N | 30 | 트랜잭션 커밋 주기(초) |
thread_pool=N | 코어 수 + 2 | 워커 스레드 수 |
max_inline=N | 2048 | 인라인 extent 최대 크기 |
nodatacow | 없음 | COW 비활성화 (DB, VM 이미지용) |
flushoncommit | 없음 | 커밋 시 강제 flush (안정성 ↑, 성능 ↓) |
SSD 최적화
# SSD에 최적화된 마운트 예시
$ mount -o compress=zstd:1,ssd,discard=async,noatime,space_cache=v2 \
/dev/nvme0n1p2 /
# fstab 예시
UUID=xxx / btrfs defaults,compress=zstd:1,ssd,discard=async,noatime,space_cache=v2,subvol=@ 0 0
discard=async는 커널 6.2+에서 크게 개선되어 대부분 SSD 환경에서 권장됩니다. 다만 장치/워크로드에 따라 주기적 fstrim이 더 적합할 수 있으므로 실측 비교 후 선택하세요. ssd 옵션은 대개 자동 감지되므로 명시하지 않아도 됩니다.
워크로드별 튜닝
# 데스크톱 / 범용 서버
$ mount -o compress=zstd:1,noatime,discard=async,space_cache=v2 ...
# 데이터베이스 (MySQL, PostgreSQL)
$ mount -o nodatacow,noatime,discard=async ...
# + DB 데이터 디렉토리에 chattr +C 설정
$ chattr +C /var/lib/mysql/
$ chattr +C /var/lib/postgresql/
# 가상머신 이미지 저장소
$ mount -o nodatacow,noatime ...
$ chattr +C /var/lib/libvirt/images/
# 로그 서버 (대량 순차 쓰기)
$ mount -o compress=zstd:3,noatime,commit=120,autodefrag ...
# NAS / 미디어 스토리지
$ mount -o compress=zstd:3,noatime,space_cache=v2 ...
모니터링
# 실시간 할당 상태
$ watch -n 5 btrfs fi usage /mnt
# 디바이스 I/O 통계
$ btrfs device stats /mnt
[/dev/sda].write_io_errs 0
[/dev/sda].read_io_errs 0
[/dev/sda].flush_io_errs 0
[/dev/sda].corruption_errs 0
[/dev/sda].generation_errs 0
# 에러 카운터 리셋
$ btrfs device stats -z /mnt
# 커널 메시지에서 btrfs 관련 로그 확인
$ dmesg | grep -i btrfs
다른 파일시스템 비교
기능 비교표
| 기능 | Btrfs | ext4 | XFS | ZFS |
|---|---|---|---|---|
| COW | O | X | X (reflink O) | O |
| 스냅샷 | O (서브볼륨 단위) | X (LVM 의존) | X | O (데이터셋 단위) |
| 데이터 체크섬 | O | X (메타만) | X | O |
| 투명 압축 | O (zlib/lzo/zstd) | X | X | O (lz4/gzip/zstd) |
| 내장 RAID | O (0/1/10/5/6) | X | X | O (Z1/Z2/Z3) |
| 인라인 dedupe | X (오프라인만) | X | X | O |
| Send/Receive | O | X | X | O |
| 온라인 축소 | O | O | X | X |
| 최대 볼륨 크기 | 16 EiB | 1 EiB | 8 EiB | 256 ZiB |
| 라이선스 | GPL | GPL | GPL | CDDL |
| 안정성 | 양호 (RAID5/6 제외) | 매우 높음 | 매우 높음 | 매우 높음 |
선택 가이드
| 사용 사례 | 추천 | 이유 |
|---|---|---|
| 엔터프라이즈 서버 (보수적) | ext4 / XFS | 검증된 안정성, 성숙한 fsck 도구 |
| 데스크톱 / 워크스테이션 | Btrfs | 스냅샷 롤백, 압축, 유연한 관리 |
| NAS / 스토리지 서버 | Btrfs / ZFS | 데이터 무결성, 스냅샷, RAID |
| 컨테이너 호스트 | Btrfs | 서브볼륨, 스냅샷, overlay 성능 |
| 데이터베이스 전용 | ext4 / XFS | COW 오버헤드 없음, 예측 가능한 I/O |
| 대용량 데이터 레이크 | XFS | 대규모 병렬 I/O 최적화 |
| 최고 수준 데이터 보호 | ZFS | 검증된 RAIDZ, 인라인 dedupe, 성숙도 |
Btrfs vs ZFS
두 파일시스템 모두 COW 기반이며 유사한 기능 세트를 제공하지만, 근본적인 차이가 있습니다:
| 측면 | Btrfs | ZFS |
|---|---|---|
| 라이선스 | GPL v2 (커널 내장) | CDDL (외부 모듈, OpenZFS) |
| RAID 안정성 | RAID1/10 안정, RAID5/6 불안정 | RAIDZ1/Z2/Z3 매우 안정 |
| 메모리 사용 | 상대적으로 적음 | ARC 캐시로 대량 메모리 사용 (RAM 절반 이상) |
| 커널 통합 | 메인라인 포함 | 별도 DKMS/kmod 설치 필요 |
| 인라인 Dedupe | 미지원 (오프라인만) | 지원 |
| Send/Receive | 지원 | 지원 |
| 축소 | 온라인 축소 가능 | 불가 |
| 성숙도 | 발전 중 | 매우 성숙 (Solaris 계보) |
CoW B-tree 심화
Btrfs의 B-tree는 일반적인 B+ tree와 달리 모든 수정이 Copy-on-Write로 처리됩니다. 이 섹션에서는 COW B-tree의 노드 분할/병합 메커니즘, 쓰기 증폭(write amplification), 그리고 트리 높이에 따른 성능 영향을 심층적으로 분석합니다.
노드 분할 (Split)
B-tree 리프 노드가 가득 차면 COW와 함께 분할이 일어납니다. 일반 B-tree와 달리 원본 노드를 수정하지 않고 새 노드를 할당하므로, 스냅샷이 원본을 계속 참조할 수 있습니다:
/* fs/btrfs/ctree.c - 리프 분할 */
static noinline int split_leaf(
struct btrfs_trans_handle *trans,
struct btrfs_root *root,
const struct btrfs_key *ins_key,
struct btrfs_path *path,
int data_size,
int extend)
{
struct extent_buffer *l = path->nodes[0];
struct extent_buffer *right;
int mid, nritems;
nritems = btrfs_header_nritems(l);
mid = (nritems + 1) / 2;
/* 새 리프 노드 할당 (COW 방식) */
right = btrfs_alloc_tree_block(trans, root, 0,
root->root_key.objectid,
&disk_key, 0, l->start, 0);
/* 아이템의 후반부를 새 리프로 복사 */
copy_for_split(trans, path, l, right, mid, nritems);
/* 부모 노드에 새 키 삽입 */
insert_ptr(trans, path, &disk_key,
right->start, path->slots[1] + 1, 1);
return 0;
}
노드 병합 (Merge)
삭제 연산으로 리프 사용률이 낮아지면 인접 노드와 병합(merge)을 시도합니다. 병합도 COW 방식으로 진행되므로 스냅샷 참조에 영향을 주지 않습니다:
/* fs/btrfs/ctree.c - 삭제 후 리프 병합 판단 */
static void try_merge_after_delete(
struct btrfs_trans_handle *trans,
struct btrfs_root *root,
struct btrfs_path *path)
{
struct extent_buffer *leaf = path->nodes[0];
u32 used = btrfs_leaf_data_size(leaf);
u32 threshold = BTRFS_LEAF_DATA_SIZE(root->fs_info) / 3;
/* 사용률이 1/3 이하면 인접 리프와 병합 시도 */
if (used < threshold) {
/* 왼쪽 또는 오른쪽 형제 노드에 아이템 이동 */
push_leaf_left(trans, root, path, 1, 1);
push_leaf_right(trans, root, path, 1, 1);
}
}
쓰기 증폭 분석
| 파일시스템 크기 | 예상 트리 높이 | 단일 쓰기 시 COW 노드 수 | 쓰기 증폭량 (nodesize=16K) |
|---|---|---|---|
| 1 GiB | 2 | 2 (리프+루트) | 32 KiB |
| 100 GiB | 3 | 3 | 48 KiB |
| 10 TiB | 4 | 4 | 64 KiB |
| 1 PiB | 5 | 5 | 80 KiB |
스냅샷 트리 심화
스냅샷은 서브볼륨의 FS Tree 루트를 공유하는 새로운 트리입니다. 이 섹션에서는 스냅샷의 내부 트리 구조, 메타데이터 공유 모델, 스냅샷 생성/삭제 비용, 그리고 qgroup 기반 공간 추적을 분석합니다.
스냅샷 생성 내부
/* fs/btrfs/ioctl.c - 스냅샷 생성 핵심 */
static noinline int btrfs_mksubvol(
const struct path *parent,
const char *name, int namelen,
struct btrfs_root *snap_src,
bool readonly)
{
/* 1. 현재 트랜잭션 시작 */
trans = btrfs_start_transaction(root, 0);
/* 2. 소스 FS Tree의 루트 노드를 COW로 복사
* → 새 트리 ID 할당, Root Tree에 ROOT_ITEM 삽입 */
btrfs_create_snapshot(trans, snap_src,
parent_root, dentry, readonly);
/* 3. extent 참조 카운트는 암묵적으로 공유
* (삭제/수정 시에만 back-reference 갱신) */
btrfs_commit_transaction(trans);
return 0;
}
스냅샷 삭제 비용
스냅샷 삭제는 독점(exclusive) extent의 해제와 공유 extent의 참조 카운트 감소를 포함하므로 생성보다 훨씬 비용이 큽니다:
/* fs/btrfs/extent-tree.c - 스냅샷 삭제 시 extent 해제 */
static int do_walk_down(
struct btrfs_trans_handle *trans,
struct btrfs_root *root,
struct btrfs_path *path,
struct walk_control *wc)
{
/* 각 노드를 순회하며:
* - 공유 노드(다른 스냅샷이 참조): refcount-- 만 수행
* - 독점 노드(이 스냅샷만 참조): 실제 extent 해제
* → 스냅샷 수가 많을수록 삭제 시간 증가 */
if (wc->refs[level] > 1) {
/* 공유 노드: refcount 감소만 */
btrfs_free_tree_block(trans, root, eb, 0, 1);
} else {
/* 독점 노드: 하위 트리 전체 순회 필요 */
walk_down_proc(trans, root, path, wc);
}
}
Qgroup 공간 추적
# qgroup 활성화 (서브볼륨별 공간 추적)
$ btrfs quota enable /mnt
# 서브볼륨별 사용량 확인 (exclusive/shared)
$ btrfs qgroup show -reF /mnt
qgroupid rfer excl
-------- ---- ----
0/256 10.00GiB 5.00GiB # @rootfs: 10G 참조, 5G 독점
0/257 8.00GiB 3.00GiB # @home: 8G 참조, 3G 독점
0/258 8.00GiB 200.00MiB # @snap-home: 8G 참조, 200M 독점
# 서브볼륨에 공간 할당 한도 설정
$ btrfs qgroup limit 50G /mnt/@home
squota(simple quota) 모드가 도입되어 오버헤드가 크게 줄었습니다.
Send/Receive 심화
Btrfs send/receive는 두 읽기 전용 스냅샷 사이의 차이를 바이너리 스트림으로 직렬화하여 전송합니다. 이 섹션에서는 send 스트림 형식, 증분 전송 알고리즘, 그리고 원격 복제 파이프라인을 상세히 분석합니다.
Send 알고리즘
/* fs/btrfs/send.c - 증분 send 핵심 */
static int full_send_tree(struct send_ctx *sctx)
{
struct btrfs_root *send_root = sctx->send_root;
struct btrfs_root *parent_root = sctx->parent_root;
/* 두 트리를 동시에 순회하며 차이점 감지 */
ret = btrfs_compare_trees(send_root, parent_root,
changed_cb, sctx);
/* changed_cb가 감지하는 변경 유형:
* BTRFS_COMPARE_TREE_NEW → 새 아이템 (파일 생성)
* BTRFS_COMPARE_TREE_DELETED → 삭제된 아이템
* BTRFS_COMPARE_TREE_CHANGED → 수정된 아이템
*/
return ret;
}
/* 변경 콜백: 각 변경에 대해 send 명령 생성 */
static int changed_cb(struct send_ctx *sctx,
enum btrfs_compare_tree_result result,
struct btrfs_key *key)
{
switch (result) {
case BTRFS_COMPARE_TREE_NEW:
send_create_inode(sctx, key);
break;
case BTRFS_COMPARE_TREE_DELETED:
send_unlink(sctx, key);
break;
case BTRFS_COMPARE_TREE_CHANGED:
send_write_or_clone(sctx, key);
break;
}
}
고급 사용법
# 다중 클론 소스를 사용한 증분 전송 (공유 데이터 최대 활용)
$ btrfs send -p /snap/@base -c /snap/@other1 -c /snap/@other2 \
/snap/@latest | btrfs receive /backup/
# v2 프로토콜: 압축 데이터 그대로 전송 (커널 5.18+)
$ btrfs send --compressed-data /snap/@latest | btrfs receive /backup/
# 파이프로 진행률 확인
$ btrfs send -p /snap/@day1 /snap/@day2 | \
pv -pterb | btrfs receive /backup/
# 스트림을 파일로 저장 후 나중에 적용
$ btrfs send -p /snap/@base /snap/@incr -f /tmp/incr.btrfs
$ btrfs receive -f /tmp/incr.btrfs /backup/
# SSH 원격 전송 (대역폭 제한 + 압축)
$ btrfs send -p /snap/@day1 /snap/@day2 | \
zstd -1 | ssh -l 10M user@remote "zstd -d | btrfs receive /backup/"
-c(clone source) 옵션으로 여러 스냅샷을 지정하면 CLONE 명령이 더 많이 생성되어 실제 데이터 전송량이 줄어듭니다. v2 프로토콜의 --compressed-data는 압축 해제/재압축 오버헤드를 제거합니다.
Scrub 메커니즘 심화
Scrub은 Btrfs의 온라인 데이터 무결성 검증 시스템입니다. 모든 extent를 순차적으로 읽어 체크섬을 검증하고, RAID 미러가 있는 경우 손상된 데이터를 자동으로 복구합니다. 이 섹션에서는 scrub의 내부 구조, I/O 스케줄링, 복구 전략을 심층 분석합니다.
Scrub 내부 구조
/* fs/btrfs/scrub.c - scrub 핵심 구조체 */
struct scrub_ctx {
struct btrfs_fs_info *fs_info;
int readonly; /* 복구 쓰기 허용 여부 */
struct scrub_bio *bios[SCRUB_BIOS_PER_SCTX];
int curr; /* 현재 bio 인덱스 */
atomic_t cancel_req; /* 취소 요청 */
struct scrub_stat stat; /* 통계 */
};
struct scrub_stat {
u64 data_extents_scrubbed; /* 검사한 데이터 extent 수 */
u64 tree_extents_scrubbed; /* 검사한 메타데이터 extent 수 */
u64 data_bytes_scrubbed; /* 검사한 데이터 바이트 */
u64 tree_bytes_scrubbed; /* 검사한 메타데이터 바이트 */
u64 read_errors; /* 읽기 에러 수 */
u64 csum_errors; /* 체크섬 불일치 수 */
u64 verify_errors; /* 메타데이터 검증 에러 */
u64 corrected_errors; /* 자동 복구된 에러 수 */
u64 uncorrectable_errors; /* 복구 불가 에러 수 */
};
Scrub 스케줄링
# I/O 우선순위 제어 (ionice로 scrub 영향 최소화)
$ ionice -c 3 btrfs scrub start /mnt
# systemd timer로 주기적 scrub 설정
$ systemctl enable btrfs-scrub@-.timer # 루트 파일시스템
$ systemctl enable btrfs-scrub@mnt-data.timer # /mnt/data
# 사용자 정의 scrub 타이머 (매주 일요일 02:00)
$ cat /etc/systemd/system/btrfs-scrub-weekly.timer
[Timer]
OnCalendar=Sun *-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
# scrub 상세 결과 확인
$ btrfs scrub status -d /mnt
UUID: a1b2c3d4-...
Scrub started: Sun Mar 1 02:00:00 2026
Status: finished
Duration: 2:15:32
Total to scrub: 500.00GiB
Rate: 63.00MiB/s
Error summary: csum=2 read=0 super=0 verify=0 nocsum=0
Corrected: 2
Uncorrectable: 0
RAID 프로파일 심화
Btrfs의 RAID는 전통적인 mdadm RAID와 달리 Chunk 레벨에서 동작합니다. 각 Chunk(Block Group)마다 독립적인 RAID 프로파일이 적용되며, 데이터와 메타데이터에 서로 다른 프로파일을 혼합할 수 있습니다. 이 섹션에서는 Chunk 기반 RAID의 내부 동작, 리밸런싱, 그리고 장애 복구를 분석합니다.
Chunk 할당 메커니즘
/* fs/btrfs/volumes.c - RAID 프로파일별 stripe 할당 */
static int decide_stripe_size_regular(
struct alloc_chunk_ctl *ctl,
struct btrfs_device_info *devices_info)
{
/* RAID 프로파일에 따른 stripe 계산 */
switch (ctl->type & BTRFS_BLOCK_GROUP_PROFILE_MASK) {
case BTRFS_BLOCK_GROUP_RAID0:
ctl->num_stripes = ctl->devs_max;
ctl->sub_stripes = 0;
break;
case BTRFS_BLOCK_GROUP_RAID1:
ctl->num_stripes = 2;
ctl->ncopies = 2;
break;
case BTRFS_BLOCK_GROUP_RAID1C3:
ctl->num_stripes = 3;
ctl->ncopies = 3;
break;
case BTRFS_BLOCK_GROUP_RAID10:
ctl->num_stripes = ctl->devs_max;
ctl->sub_stripes = 2;
ctl->ncopies = 2;
break;
case BTRFS_BLOCK_GROUP_DUP:
ctl->num_stripes = 2;
ctl->ncopies = 2;
/* 같은 디바이스에 2개 stripe */
break;
}
}
리밸런싱과 프로파일 변환
# 현재 Chunk 배치 상태 확인
$ btrfs filesystem usage -T /mnt
Data Metadata System
Id Path RAID1 RAID1 RAID1
-- ---- ----- ----- -----
1 /dev/sda 240.00G 8.00G 32.00M
2 /dev/sdb 240.00G 8.00G 32.00M
# 디바이스 추가 후 리밸런싱
$ btrfs device add /dev/sdc /dev/sdd /mnt
$ btrfs balance start -dconvert=raid10 -mconvert=raid1c3 /mnt
# 리밸런싱 진행률 모니터링
$ btrfs balance status /mnt
Balance on '/mnt' is running
14 out of 56 chunks balanced (8 considered), 75% left
# 특정 사용률 이하 Chunk만 리밸런싱 (빠른 공간 회수)
$ btrfs balance start -dusage=30,soft -musage=30,soft /mnt
# 비균등 디바이스에서의 데이터 균등 분배
$ btrfs balance start -ddevid=1,limit=10 /mnt # dev1에서 10개 chunk만
Degraded 모드 운영
# 디바이스 장애 시 degraded 마운트
$ mount -o degraded /dev/sda /mnt
# 장애 디바이스 교체
$ btrfs replace start 2 /dev/sdd /mnt # devid 2를 /dev/sdd로 교체
$ btrfs replace status /mnt
# 디바이스 에러 통계 확인
$ btrfs device stats /mnt
[/dev/sda].write_io_errs 0
[/dev/sda].read_io_errs 0
[/dev/sda].flush_io_errs 0
[/dev/sda].corruption_errs 0
[/dev/sda].generation_errs 0
[/dev/sdb].write_io_errs 145 # 장애 징후!
[/dev/sdb].read_io_errs 23
[/dev/sdb].corruption_errs 5
압축 심화
Btrfs의 투명 압축은 extent 단위(최대 128K)로 동작하며, 각 extent에 독립적인 압축 유형이 기록됩니다. 이 섹션에서는 압축 알고리즘별 성능 특성, 인라인 extent, 휴리스틱 기반 자동 판단, 그리고 워크로드별 최적 설정을 분석합니다.
압축 휴리스틱
Btrfs는 compress(non-force) 모드에서 파일의 첫 부분을 샘플링하여 압축 가능성을 판단합니다:
/* fs/btrfs/compression.c - 휴리스틱 판단 */
static int btrfs_compress_heuristic(
struct inode *inode,
u64 start, u64 end)
{
/* 1. 파일의 처음 일부를 샘플링 */
/* 2. 바이트 분포(Shannon entropy) 계산 */
/* 3. 엔트로피가 임계값 이상이면 비압축성으로 판단
* → 해당 범위를 NOCOMPRESS로 마킹
* → 이후 같은 파일의 쓰기는 압축 시도하지 않음 */
sample_end = min(end, start + BTRFS_MAX_UNCOMPRESSED);
heuristic_result = sample_repeated_patterns(input);
if (heuristic_result > ENTROPY_THRESHOLD)
return 0; /* 압축하지 않음 */
return 1; /* 압축 진행 */
}
압축률 측정과 분석
# compsize 도구로 실제 압축률 확인
$ compsize /mnt/data
Processed 25000 files, 18000 regular extents (20000 refs), 5000 inline.
Type Perc Disk Usage Uncompressed Referenced
TOTAL 58% 23.2G 40.0G 42.0G
zstd 52% 18.5G 35.5G 37.0G
none 100% 4.7G 4.5G 5.0G
# 파일별 압축 정보 확인
$ compsize -x /mnt/data/logfile.txt
Processed 1 file, 120 regular extents (120 refs), 0 inline.
Type Perc Disk Usage Uncompressed
zstd 25% 250.0M 1.0G
# 압축 레벨별 테스트 (defrag으로 재압축)
$ btrfs filesystem defragment -czstd:1 /mnt/testfile # 빠른 압축
$ btrfs filesystem defragment -czstd:9 /mnt/testfile # 높은 압축률
$ btrfs filesystem defragment -czlib:9 /mnt/testfile # 최대 압축률
워크로드별 압축 가이드
| 워크로드 | 권장 설정 | 예상 압축률 | 이유 |
|---|---|---|---|
| 소스 코드/텍스트 | compress=zstd:3 | 3~5:1 | 높은 압축률, 적당한 속도 |
| 로그 파일 | compress=zstd:3 | 5~10:1 | 반복 패턴 많아 매우 효과적 |
| 데이터베이스 | nodatacow (압축 비활성) | - | 랜덤 I/O, COW 오버헤드 제거 |
| 가상머신 이미지 | nodatacow | - | 내부 FS가 별도 관리 |
| 미디어 (사진/동영상) | compress=zstd:1 또는 none | 1:1~1.1:1 | 이미 압축된 데이터, 효과 미미 |
| 범용 데스크톱 | compress=zstd:1 | 1.5~2.5:1 | 최소 CPU로 적당한 절약 |
| NAS/아카이브 | compress=zstd:9 | 2.5~4:1 | 공간 절약 우선, I/O 부담 수용 |
중복 제거 (Deduplication)
Btrfs는 ZFS와 달리 인라인(실시간) 중복 제거를 지원하지 않지만, reflink 기반의 오프라인/out-of-band 중복 제거를 효과적으로 수행할 수 있습니다. 이 섹션에서는 reflink, 중복 제거 도구, 그리고 extent 공유 메커니즘을 분석합니다.
Reflink 메커니즘
/* fs/btrfs/reflink.c - reflink(파일 클론) 구현 */
loff_t btrfs_remap_file_range(
struct file *src_file, loff_t src_off,
struct file *dst_file, loff_t dst_off,
loff_t len, unsigned int remap_flags)
{
/* FICLONE / FICLONERANGE ioctl 핸들러
*
* 동작:
* 1. src의 EXTENT_DATA 아이템을 dst의 FS Tree에 복사
* 2. Extent Tree에서 해당 extent의 refcount 증가
* 3. 실제 데이터 복사 없음 → O(수정된 메타데이터) 시간
*
* 이후 src 또는 dst에서 수정 발생 시:
* → COW로 새 extent 할당, refcount 감소
*/
return btrfs_clone_file_range(src_file, src_off,
dst_file, dst_off, len);
}
중복 제거 도구
# ── duperemove: 블록 레벨 중복 제거 ──
$ duperemove -rdh /mnt/data/
# -r: 재귀, -d: 실행(dry-run 아님), -h: human-readable
# 해시 DB를 저장하여 증분 스캔
$ duperemove -rdh --hashfile=/tmp/dedup.hash /mnt/data/
# ── bees (Best-Effort Extent-Same): 온라인 중복 제거 데몬 ──
$ bees /mnt # 백그라운드 데몬으로 실행
$ bees --loadavg-target=2.0 /mnt # 시스템 부하 제한
# ── cp --reflink: 수동 COW 복사 ──
$ cp --reflink=always src.img dst.img # 즉시 완료, 공간 0 추가
# ── FIDEDUPERANGE ioctl: 프로그래밍 방식 ──
# 두 파일의 특정 범위가 동일한지 확인 후 reflink 처리
# ioctl(fd, FIDEDUPERANGE, &args)
커널 내부 심화
이 섹션에서는 Btrfs의 핵심 커널 내부 구조인 btrfs_fs_info, btrfs_root, 트랜잭션 관리자, 그리고 delayed ref 메커니즘을 심층 분석합니다. fs/btrfs/ 디렉토리의 주요 소스 파일 간 관계를 이해하는 것이 목표입니다.
Delayed References
Btrfs의 extent 참조 카운트 갱신은 즉시 수행되지 않고 delayed reference 큐에 쌓였다가 트랜잭션 커밋 시 배치 처리됩니다. 이는 COW 기반 시스템에서 Extent Tree의 반복적인 수정을 줄여 성능을 크게 향상시킵니다:
/* fs/btrfs/delayed-ref.h */
struct btrfs_delayed_ref_root {
struct rb_root_cached href_root; /* head ref RB-tree */
spinlock_t lock;
atomic_t num_entries; /* 대기 중인 ref 수 */
unsigned long num_heads; /* 고유 extent 수 */
int flushing; /* 플러시 진행 중 */
u64 run_delayed_start; /* 처리 시작 오프셋 */
};
struct btrfs_delayed_ref_head {
struct rb_node href_node;
struct list_head ref_add_list; /* 추가 참조 리스트 */
struct list_head ref_del_list; /* 삭제 참조 리스트 */
u64 bytenr; /* extent 논리 주소 */
u64 num_bytes; /* extent 크기 */
int total_ref_mod; /* 순 참조 변경량 (+/-) */
int must_insert_reserved;
};
트랜잭션 생명주기
/* fs/btrfs/transaction.c - 트랜잭션 상태 전이 */
enum btrfs_trans_state {
TRANS_STATE_RUNNING, /* writer 참여 가능 */
TRANS_STATE_COMMIT_START, /* 커밋 시작, 새 writer 차단 */
TRANS_STATE_COMMIT_DOING, /* delayed ref 처리 + 데이터 flush */
TRANS_STATE_UNBLOCKED, /* 새 트랜잭션 시작 가능 */
TRANS_STATE_SUPER_COMMITTED,/* superblock 기록 완료 */
TRANS_STATE_COMPLETED, /* 완전히 종료 */
};
/* 커밋 과정 요약:
* 1. RUNNING → COMMIT_START: 새 writer 차단
* 2. 기존 writer 완료 대기 (num_writers == 0)
* 3. COMMIT_DOING: delayed refs 실행, 더티 페이지 flush
* 4. UNBLOCKED: 새 트랜잭션 시작 허용 (파이프라이닝)
* 5. 모든 I/O 완료 대기
* 6. SUPER_COMMITTED: superblock 기록 (commit point)
* 7. COMPLETED: 이전 트랜잭션 정리
*/
소스 코드 맵
| 파일 | 역할 | 주요 함수/구조체 |
|---|---|---|
fs.h | btrfs_fs_info 정의 | btrfs_fs_info, 마운트 옵션 상수 |
ctree.h / ctree.c | B-tree 핵심 | btrfs_search_slot, split_leaf, btrfs_key |
transaction.c | 트랜잭션 관리 | btrfs_start_transaction, btrfs_commit_transaction |
extent-tree.c | extent 할당/해제 | btrfs_alloc_tree_block, btrfs_free_tree_block |
delayed-ref.c | 지연 참조 처리 | btrfs_run_delayed_refs |
volumes.c | RAID/Chunk/디바이스 | btrfs_map_block, decide_stripe_size |
disk-io.c | 디스크 I/O | btrfs_read_buffer, write_all_supers |
inode.c | VFS inode 연산 | btrfs_lookup_dentry, btrfs_new_inode |
file.c | 파일 연산 | btrfs_file_write_iter, btrfs_sync_file |
send.c | send/receive | btrfs_ioctl_send, changed_cb |
scrub.c | scrub | scrub_stripe, scrub_repair |
compression.c | 투명 압축 | btrfs_submit_compressed_read/write |
ftrace/bpftrace 성능 분석
Btrfs는 커널의 tracepoint 인프라를 활용하여 내부 동작을 추적할 수 있습니다. 이 섹션에서는 ftrace, perf, bpftrace를 사용한 Btrfs 성능 분석과 문제 진단 방법을 다룹니다.
Btrfs Tracepoints
# 사용 가능한 btrfs tracepoint 목록 확인
$ ls /sys/kernel/debug/tracing/events/btrfs/
btrfs_chunk_alloc btrfs_cow_block
btrfs_delayed_data_ref btrfs_delayed_ref_head
btrfs_delayed_tree_ref btrfs_flush_space
btrfs_ordered_extent_add btrfs_ordered_extent_put
btrfs_qgroup_account_extent btrfs_reserved_extent_alloc
btrfs_space_reservation btrfs_transaction_commit
btrfs_trigger_flush btrfs_writepage_end_io_hook
...
# ftrace로 트랜잭션 커밋 추적
$ echo 1 > /sys/kernel/debug/tracing/events/btrfs/btrfs_transaction_commit/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
btrfs-transacti-1234 [002] .... 12345.678: btrfs_transaction_commit:
generation=1000 root=5
# COW 블록 추적
$ echo 1 > /sys/kernel/debug/tracing/events/btrfs/btrfs_cow_block/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
kworker/2:0-567 [002] .... 12345.789: btrfs_cow_block:
root=5 refs=1 orig_buf=0x1a2b3c new_buf=0x4d5e6f
bpftrace 스크립트
# 트랜잭션 커밋 지연 시간 히스토그램
$ bpftrace -e '
kprobe:btrfs_commit_transaction {
@start[tid] = nsecs;
}
kretprobe:btrfs_commit_transaction /@start[tid]/ {
@commit_latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# extent 할당 크기 분포
$ bpftrace -e '
tracepoint:btrfs:btrfs_reserved_extent_alloc {
@alloc_size = hist(args->len);
@alloc_by_root[args->root_objectid] = count();
}'
# COW 빈도 모니터링 (초당)
$ bpftrace -e '
tracepoint:btrfs:btrfs_cow_block {
@cow_per_sec = count();
}
interval:s:1 {
print(@cow_per_sec);
clear(@cow_per_sec);
}'
# 압축 성능 분석
$ bpftrace -e '
kprobe:btrfs_compress_pages {
@start[tid] = nsecs;
}
kretprobe:btrfs_compress_pages /@start[tid]/ {
@compress_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# ENOSPC flush 이벤트 추적
$ bpftrace -e '
tracepoint:btrfs:btrfs_trigger_flush {
printf("%s: flush_action=%d flags=0x%lx\n",
comm, args->flush_action, args->flags);
}'
perf 기반 분석
# btrfs 관련 함수의 CPU 사용률 프로파일링
$ perf top -g --call-graph dwarf -p $(pidof btrfs-transacti)
# btrfs commit 과정의 flame graph 생성
$ perf record -g -p $(pidof btrfs-transacti) -- sleep 30
$ perf script | stackcollapse-perf.pl | flamegraph.pl > btrfs-commit.svg
# btrfs tracepoint 기반 I/O 지연 분석
$ perf stat -e 'btrfs:btrfs_cow_block' \
-e 'btrfs:btrfs_transaction_commit' \
-e 'btrfs:btrfs_ordered_extent_add' \
-- sleep 60
# scrub I/O 패턴 분석
$ perf trace -e btrfs:btrfs_scrub_* -p $(pgrep -f "btrfs scrub")
btrfs_flush_space tracepoint가 빈번히 발생하면 ENOSPC 임박을 의미합니다. btrfs_cow_block의 빈도가 비정상적으로 높다면 과도한 메타데이터 수정을 의심하세요. commit=N 마운트 옵션을 늘려 트랜잭션 커밋 빈도를 줄이면 COW 오버헤드를 완화할 수 있습니다.
성능 튜닝 심화
Btrfs의 성능은 마운트 옵션, 파일시스템 구성, 워크로드 패턴, 그리고 커널 파라미터의 조합에 크게 영향을 받습니다. 이 섹션에서는 실측 데이터 기반의 튜닝 전략, I/O 스케줄러 연동, 그리고 벤치마크 방법론을 분석합니다.
벤치마크 방법론
# ── fio: 랜덤 읽기/쓰기 벤치마크 ──
# 4K 랜덤 읽기 (IOPS 측정)
$ fio --name=randread --directory=/mnt/test \
--ioengine=libaio --direct=1 --bs=4k \
--iodepth=32 --numjobs=4 --size=1G \
--rw=randread --group_reporting
# 4K 랜덤 쓰기 (COW 오버헤드 측정)
$ fio --name=randwrite --directory=/mnt/test \
--ioengine=libaio --direct=1 --bs=4k \
--iodepth=32 --numjobs=4 --size=1G \
--rw=randwrite --group_reporting
# 순차 쓰기 + 압축 효과 측정
$ fio --name=seqwrite --directory=/mnt/test \
--ioengine=libaio --direct=0 --bs=128k \
--iodepth=16 --numjobs=1 --size=10G \
--rw=write --group_reporting
# ── bonnie++: 종합 파일시스템 벤치마크 ──
$ bonnie++ -d /mnt/test -r 4096 -s 8192
# ── 스냅샷 오버헤드 측정 ──
$ # 기준선 측정
$ fio --name=base --directory=/mnt/test --rw=randrw --size=1G --runtime=60
$ # 스냅샷 100개 생성 후 재측정
$ for i in $(seq 1 100); do
btrfs subvolume snapshot /mnt/@data /mnt/@snap-$i
done
$ fio --name=with_snaps --directory=/mnt/test --rw=randrw --size=1G --runtime=60
실전 튜닝 예제
# ── 고성능 NVMe 서버 ──
$ mkfs.btrfs -m dup -d single -L fast-storage /dev/nvme0n1p1
$ mount -o noatime,compress=zstd:1,discard=async,space_cache=v2,\
ssd,commit=60,max_inline=2048 /dev/nvme0n1p1 /data
# ── RAID1 NAS 서버 ──
$ mkfs.btrfs -m raid1 -d raid1 -L nas-pool /dev/sd{a,b,c,d}
$ mount -o noatime,compress=zstd:3,space_cache=v2,\
autodefrag,commit=120 /dev/sda /nas
# ── 컨테이너/CI 호스트 ──
$ mkfs.btrfs -m dup -d single -L containers /dev/nvme0n1p2
$ mount -o noatime,compress=zstd:1,discard=async,space_cache=v2,\
ssd /dev/nvme0n1p2 /var/lib/containers
# ── 주기적 유지보수 크론잡 ──
$ cat /etc/cron.weekly/btrfs-maintenance
#!/bin/bash
# 빈 블록 그룹 회수
btrfs balance start -dusage=0 -musage=0 /data
# 사용률 낮은 블록 그룹 통합
btrfs balance start -dusage=30,soft -musage=30,soft /data
# scrub 실행
btrfs scrub start -B /data
# 에러 통계 확인
btrfs device stats /data
흔한 성능 문제와 해결
| 증상 | 원인 | 해결 방법 |
|---|---|---|
| 쓰기 성능 급락 | 메타데이터 공간 부족 | btrfs balance start -musage=50 |
| ENOSPC (df에 공간 있음) | Chunk 할당 불균형 | btrfs balance start -dusage=0 |
| 마운트 지연 | space_cache v1 로드 | mount -o space_cache=v2 |
| DB 성능 저하 | COW + 체크섬 오버헤드 | nodatacow + chattr +C |
| 스냅샷 삭제 느림 | 대량 extent 참조 해제 | 한번에 적은 수의 스냅샷 삭제 |
| qgroup 활성화 후 느림 | 참조 카운트 추적 오버헤드 | squota(커널 6.1+) 사용 또는 비활성화 |
| 파일 단편화 | COW + 랜덤 쓰기 | autodefrag 또는 주기적 defragment |
| send/receive 느림 | 대량 변경 + 비효율적 비교 | -c clone source 지정, v2 프로토콜 사용 |
autodefrag은 소형 랜덤 쓰기 워크로드에서 유용하지만, 대용량 순차 쓰기 환경에서는 불필요한 I/O를 생성할 수 있습니다. 또한 스냅샷이 많은 환경에서 defrag은 extent 공유를 해제하여 디스크 사용량을 급증시킬 수 있으므로, 항상 사전에 btrfs fi usage로 여유 공간을 확인하세요.
관련 문서
Btrfs와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.