ext4 파일시스템 심화
범용 Linux 서버에서 가장 널리 쓰이는 ext4를 대상으로 온디스크 블록 그룹 구조, inode/extent 매핑, JBD2 저널 트랜잭션 상태 전이, mballoc과 delayed allocation의 공간 배치 전략, writeback/commit 지연이 성능과 내구성에 주는 영향, fscrypt·fsverity 보안 기능, 장애 복구 도구(e2fsck/tune2fs) 활용까지 실무 중심으로 상세히 다룹니다.
핵심 요약
- 계층 이해 — VFS, 캐시, 하위 FS 경계를 구분합니다.
- 메타데이터 우선 — inode/dentry 일관성을 먼저 확인합니다.
- 저장 정책 — 저널링/압축/할당 정책 차이를 비교합니다.
- 일관성 모델 — 로컬/원격/합성 FS의 반영 시점을 구분합니다.
- 복구 관점 — 장애 시 재구성 경로를 함께 점검합니다.
단계별 이해
- 경계 계층 파악
요청이 VFS에서 어디로 내려가는지 확인합니다. - 메타/데이터 분리
어느 경로에서 무엇이 갱신되는지 나눠 봅니다. - 동기화/플러시 확인
쓰기 반영 시점과 순서를 검증합니다. - 복구 시나리오 점검
비정상 종료 후 일관성 회복을 확인합니다.
개요 & 역사
ext4(Fourth Extended Filesystem)는 Linux의 사실상 표준 파일시스템으로, ext2/ext3의 직접적인 후속입니다. 2006년 개발이 시작되어 Linux 2.6.28(2008)에서 안정 상태로 포함되었습니다.
ext 계열 진화
| 파일시스템 | 커널 버전 | 주요 변경 |
|---|---|---|
| ext2 (1993) | 0.99 | Block Group 구조, BSD FFS 기반 설계 |
| ext3 (2001) | 2.4.15 | JBD 저널링 추가 (journal / ordered / writeback) |
| ext4 (2008) | 2.6.28 | Extent, 48/64-bit, delalloc, mballoc, 체크섬, fscrypt |
ext4 주요 스펙
| 항목 | 값 |
|---|---|
| 최대 볼륨 크기 | 1 EiB (4K 블록 기준) |
| 최대 파일 크기 | 16 TiB (4K 블록 기준) |
| 최대 파일 수 | 40억 (232) |
| 파일명 길이 | 255 바이트 |
| 블록 크기 | 1K / 2K / 4K (기본 4K) |
| 타임스탬프 범위 | 1901-12-14 ~ 2446-05-10 (나노초 확장) |
| 디렉토리 엔트리 | 무제한 (HTree 인덱싱) |
Feature Flags
ext4는 세 가지 유형의 feature flag로 기능을 관리합니다:
| 유형 | 설명 | 예시 |
|---|---|---|
| COMPAT | 미지원 커널도 읽기/쓰기 가능 | dir_index, resize_inode |
| INCOMPAT | 미지원 커널은 마운트 불가 | extents, flex_bg, 64bit |
| RO_COMPAT | 미지원 커널은 읽기전용만 가능 | metadata_csum, bigalloc |
/* fs/ext4/ext4.h */
#define EXT4_FEATURE_COMPAT_DIR_INDEX 0x0020
#define EXT4_FEATURE_INCOMPAT_EXTENTS 0x0040
#define EXT4_FEATURE_INCOMPAT_64BIT 0x0080
#define EXT4_FEATURE_INCOMPAT_FLEX_BG 0x0200
#define EXT4_FEATURE_RO_COMPAT_METADATA_CSUM 0x0400
#define EXT4_FEATURE_RO_COMPAT_BIGALLOC 0x0200
디스크 레이아웃
ext4 볼륨은 고정 크기 Block Group으로 분할됩니다. 각 Block Group은 최대 8 × block_size 개의 블록을 포함합니다 (4K 블록 기준 32,768 블록 = 128 MiB).
ext4_super_block 주요 필드
/* fs/ext4/ext4.h */
struct ext4_super_block {
__le32 s_inodes_count; /* 전체 inode 수 */
__le32 s_blocks_count_lo; /* 전체 블록 수 (하위 32비트) */
__le32 s_r_blocks_count_lo; /* 예약 블록 수 */
__le32 s_free_blocks_count_lo; /* 미사용 블록 수 */
__le32 s_free_inodes_count; /* 미사용 inode 수 */
__le32 s_first_data_block; /* 첫 데이터 블록 (1K 블록이면 1, 4K면 0) */
__le32 s_log_block_size; /* 블록 크기 = 2^(10+값) */
__le32 s_blocks_per_group; /* 그룹당 블록 수 */
__le32 s_inodes_per_group; /* 그룹당 inode 수 */
__le16 s_magic; /* 매직 넘버: 0xEF53 */
__le16 s_inode_size; /* inode 크기 (기본 256) */
__le32 s_feature_compat; /* 호환 기능 플래그 */
__le32 s_feature_incompat; /* 비호환 기능 플래그 */
__le32 s_feature_ro_compat; /* 읽기전용 호환 기능 플래그 */
__le16 s_desc_size; /* 그룹 디스크립터 크기 (64bit: 64) */
__le32 s_blocks_count_hi; /* 전체 블록 수 (상위 32비트) */
__le32 s_checksum_seed; /* 체크섬 시드 (UUID 기반) */
...
};
ext4_group_desc
struct ext4_group_desc {
__le32 bg_block_bitmap_lo; /* 블록 비트맵 위치 */
__le32 bg_inode_bitmap_lo; /* inode 비트맵 위치 */
__le32 bg_inode_table_lo; /* inode 테이블 시작 위치 */
__le16 bg_free_blocks_count_lo;
__le16 bg_free_inodes_count_lo;
__le16 bg_used_dirs_count_lo;
__le16 bg_flags; /* INODE_UNINIT, BLOCK_UNINIT, INODE_ZEROED */
__le32 bg_checksum; /* CRC32C 체크섬 */
/* 64bit feature 활성 시 상위 32비트 필드들 추가 */
__le32 bg_block_bitmap_hi;
__le32 bg_inode_bitmap_hi;
__le32 bg_inode_table_hi;
...
};
Flex Block Groups
flex_bg feature는 여러 Block Group(기본 16개)의 메타데이터(비트맵 + inode 테이블)를 첫 번째 BG에 연속 배치합니다. 이를 통해:
- 메타데이터 접근 시 디스크 시크 횟수를 크게 감소
- 대용량 파일의 데이터 블록이 더 넓은 연속 영역에 분포
s_log_groups_per_flex필드로 flex 그룹 크기 설정 (2의 거듭제곱)
/* flex_bg 크기 확인 */
$ dumpe2fs /dev/sda1 | grep "Flex block group size"
Flex block group size: 16
Extent Tree
ext4는 ext2/ext3의 간접 블록(indirect block) 방식 대신 Extent Tree를 사용합니다. Extent는 연속된 물리 블록의 범위를 하나의 엔트리로 표현하여, 대용량 파일에서 메타데이터 오버헤드를 크게 줄입니다.
간접 블록 vs Extent 비교
| 항목 | 간접 블록 (ext2/ext3) | Extent (ext4) |
|---|---|---|
| 100 MiB 연속 파일 | 25,600 블록 포인터 + 간접 블록 | 1 extent 엔트리 (12 바이트) |
| 최대 파일 크기 | ~4 TiB (4K 블록) | 16 TiB (4K 블록) |
| 트리 깊이 | 최대 3 (triple indirect) | 최대 5 (실제로 0~2) |
| 단편화 영향 | 높음 (1:1 매핑) | 낮음 (범위 기반) |
Extent 자료구조
/* fs/ext4/ext4_extents.h */
struct ext4_extent_header {
__le16 eh_magic; /* 매직: 0xF30A */
__le16 eh_entries; /* 현재 엔트리 수 */
__le16 eh_max; /* 최대 엔트리 수 */
__le16 eh_depth; /* 트리 깊이 (0 = 리프) */
__le32 eh_generation;
};
/* 내부 노드 (depth > 0) */
struct ext4_extent_idx {
__le32 ei_block; /* 논리 블록 번호 */
__le32 ei_leaf_lo; /* 자식 노드 물리 블록 (하위) */
__le16 ei_leaf_hi; /* 자식 노드 물리 블록 (상위) */
__le16 ei_unused;
};
/* 리프 노드 (depth == 0) */
struct ext4_extent {
__le32 ee_block; /* 논리 블록 시작 */
__le16 ee_len; /* 연속 블록 수 (최대 32768, MSB=1이면 미초기화) */
__le16 ee_start_hi; /* 물리 블록 시작 (상위) */
__le32 ee_start_lo; /* 물리 블록 시작 (하위) */
};
i_block[15] 영역(60 바이트)에 extent header + 최대 4개 extent를 직접 저장합니다. 이 영역이 부족하면 외부 블록에 extent tree를 확장합니다.
Extent Tree 구조
ext4_ext_map_blocks() 흐름
논리 블록 번호를 물리 블록으로 매핑하는 핵심 함수입니다:
/* fs/ext4/extents.c - 간략화 흐름 */
int ext4_ext_map_blocks(handle_t *handle, struct inode *inode,
struct ext4_map_blocks *map, int flags)
{
struct ext4_ext_path *path;
/* 1. extent tree 탐색: root → idx → leaf */
path = ext4_find_extent(inode, map->m_lblk, NULL, 0);
/* 2. 리프에서 논리 블록 포함 extent 검색 */
ex = path[depth].p_ext;
if (ex && in_range(map->m_lblk, ex)) {
/* 매핑 존재 → 물리 블록 반환 */
newblock = ex->ee_start + (map->m_lblk - ex->ee_block);
goto out;
}
/* 3. 매핑 없음 → 새 블록 할당 (CREATE 플래그 시) */
if (flags & EXT4_GET_BLOCKS_CREATE) {
newblock = ext4_mb_new_blocks(handle, ...);
ext4_ext_insert_extent(handle, inode, &path, &newex, flags);
}
out:
map->m_pblk = newblock;
return allocated;
}
블록 할당
Multi-Block Allocator (mballoc)
ext4의 mballoc은 한 번의 요청으로 여러 블록을 동시에 할당하는 정교한 할당기입니다. ext3의 단일 블록 할당기에 비해 연속 할당률이 크게 향상됩니다.
할당 전략
| 전략 | 설명 |
|---|---|
| Per-inode Preallocation | 파일별 사전 할당 공간 유지 (순차 쓰기 최적화) |
| Per-group Preallocation | Block Group별 사전 할당 (소파일 최적화) |
| Buddy Allocator | 2의 거듭제곱 단위 블록 관리, 비트맵 기반 |
| 정규화(Normalization) | 요청 크기를 2의 거듭제곱으로 올림하여 단편화 방지 |
/* fs/ext4/mballoc.c - 할당 흐름 (간략화) */
ext4_fsblk_t ext4_mb_new_blocks(handle_t *handle,
struct ext4_allocation_request *ar, int *errp)
{
/* 1. 정규화: 요청 크기를 적절히 확대 */
ext4_mb_normalize_request(ac, ar);
/* 2. per-inode preallocation에서 검색 */
ext4_mb_use_inode_pa(ac);
/* 3. per-group preallocation에서 검색 */
ext4_mb_use_group_pa(ac);
/* 4. buddy allocator로 새 할당 */
ext4_mb_regular_allocator(ac);
/* 5. 결과에서 미사용 공간은 preallocation으로 저장 */
ext4_mb_put_pa(ac, ...);
return block;
}
지연 할당 (Delayed Allocation)
ext4의 기본 할당 전략인 delalloc은 write() 시점에 블록을 할당하지 않고, writepages 시점(페이지 캐시 → 디스크 플러시)에 실제 할당합니다.
write() 완료 후에도 디스크에 할당되지 않은 데이터가 유실될 수 있습니다. 이를 완화하기 위해 ext4는 ordered 모드와 함께 데이터를 저널 커밋 전에 디스크에 기록합니다.
bigalloc (클러스터 할당)
bigalloc feature는 블록 비트맵의 단위를 단일 블록에서 클러스터(여러 블록의 그룹)로 변경합니다. 대용량 파일이 주로 저장되는 환경에서 비트맵 메모리 사용량을 줄이고 할당 효율을 높입니다.
/* 클러스터 크기 = 블록 크기 × 2^s_log_cluster_size */
/* 예: 4K 블록 + cluster_size=4 → 64K 클러스터 */
$ mkfs.ext4 -O bigalloc -C 65536 /dev/sda1
저널링 (JBD2)
ext4의 저널링은 JBD2(Journaling Block Device 2) 서브시스템이 담당합니다. JBD2는 ext4와 독립적인 커널 모듈로, 메타데이터(및 선택적으로 데이터)의 원자적 갱신을 보장합니다.
JBD2 아키텍처
| 구조체 | 역할 |
|---|---|
journal_t | 저널 디바이스/영역을 대표. 트랜잭션 목록, 버퍼 관리 |
transaction_t | 하나의 트랜잭션. 수정된 버퍼 목록, 상태, 시퀀스 번호 |
handle_t | 개별 파일시스템 연산의 저널 핸들 (트랜잭션의 하위 단위) |
트랜잭션 라이프사이클
저널 모드
| 모드 | 저널 대상 | 성능 | 안전성 |
|---|---|---|---|
| journal | 메타데이터 + 데이터 | 가장 느림 (데이터 이중 기록) | 가장 높음 |
| ordered (기본) | 메타데이터만 | 중간 | 높음 (데이터 먼저 기록 보장) |
| writeback | 메타데이터만 | 가장 빠름 | 낮음 (데이터 순서 미보장) |
/* 마운트 시 저널 모드 설정 */
mount -o data=ordered /dev/sda1 /mnt # 기본값
mount -o data=journal /dev/sda1 /mnt # 최고 안전성
mount -o data=writeback /dev/sda1 /mnt # 최고 성능
Fast Commit
Linux 5.10에서 도입된 Fast Commit은 전체 블록을 저널에 복사하는 대신, 변경 사항만 기록하여 커밋 지연을 크게 줄입니다.
/* Fast Commit 활성화 */
tune2fs -O fast_commit /dev/sda1
/* Fast Commit 태그 유형 (fs/ext4/fast_commit.h) */
#define EXT4_FC_TAG_ADD_RANGE 0x0001 /* extent 추가 */
#define EXT4_FC_TAG_DEL_RANGE 0x0002 /* extent 삭제 */
#define EXT4_FC_TAG_CREAT 0x0003 /* 디렉토리 엔트리 생성 */
#define EXT4_FC_TAG_LINK 0x0004 /* 하드링크 생성 */
#define EXT4_FC_TAG_UNLINK 0x0005 /* 디렉토리 엔트리 삭제 */
#define EXT4_FC_TAG_INODE 0x0006 /* inode 변경 */
저널 복구 과정
비정상 종료 후 마운트 시 JBD2가 수행하는 복구 과정:
- SCAN: 저널 시퀀스 번호를 따라가며 유효한 커밋 블록 탐색
- REVOKE: revoke 레코드를 수집하여 이후 덮어쓴 블록을 복구 대상에서 제외
- REPLAY: 커밋된 블록을 원래 위치에 재기록
디렉토리 구현
선형 디렉토리
엔트리 수가 적은 디렉토리는 선형 리스트로 저장됩니다:
struct ext4_dir_entry_2 {
__le32 inode; /* inode 번호 */
__le16 rec_len; /* 엔트리 전체 길이 (4바이트 정렬) */
__u8 name_len; /* 파일명 길이 */
__u8 file_type; /* 파일 유형 (1=REG, 2=DIR, 7=SYMLINK ...) */
char name[]; /* 파일명 (null 종료 아님) */
};
Hash Tree (HTree) 인덱싱
dir_index feature가 활성화되면(기본), 디렉토리 엔트리 수가 한 블록을 초과할 때 자동으로 HTree(B-tree 변형)로 전환됩니다. 파일명의 해시값을 키로 사용하여 O(1)에 가까운 검색 성능을 제공합니다.
/* fs/ext4/namei.c */
struct dx_root {
struct fake_dirent dot; /* "." 엔트리 */
struct fake_dirent dotdot; /* ".." 엔트리 */
struct dx_root_info {
__le32 reserved_zero;
__u8 hash_version; /* 해시 알고리즘 (half_md4, tea, sip) */
__u8 info_length;
__u8 indirect_levels; /* 트리 깊이 (0 또는 1) */
__u8 unused_flags;
} info;
struct dx_entry entries[]; /* {hash, block} 쌍의 배열 */
};
struct dx_entry {
__le32 hash; /* 파일명 해시값 */
__le32 block; /* 해당 리프 블록 번호 */
};
Inline Directory
inline_data feature가 활성화되면, 소규모 디렉토리의 엔트리를 inode 본체 내에 직접 저장하여 별도의 데이터 블록 할당을 피합니다.
확장 속성 (xattr) & ACL
xattr 저장 위치
ext4는 xattr을 두 곳에 저장합니다:
- inode body: inode 크기가 128보다 클 때 (기본 256), 여유 공간에 저장
- 외부 블록: inode body에 들어가지 않으면 별도 블록에 저장
/* xattr 엔트리 헤더 */
struct ext4_xattr_entry {
__u8 e_name_len; /* 이름 길이 */
__u8 e_name_index; /* 네임스페이스 인덱스 */
__le16 e_value_offs; /* 값의 오프셋 */
__le32 e_value_inum; /* ea_inode 번호 (대용량) */
__le32 e_value_size; /* 값의 크기 */
__le32 e_hash; /* 해시 */
char e_name[]; /* 속성 이름 */
};
xattr 네임스페이스
| 네임스페이스 | 접두어 | 용도 |
|---|---|---|
| user | user. | 일반 사용자 확장 속성 |
| system | system. | POSIX ACL (system.posix_acl_access) |
| security | security. | SELinux 레이블 (security.selinux) |
| trusted | trusted. | CAP_SYS_ADMIN 필요 |
ea_inode (대용량 xattr)
ea_inode feature는 단일 블록(4K)을 초과하는 xattr 값을 별도의 inode에 저장합니다. 이를 통해 최대 수 MiB까지의 xattr 값을 지원합니다.
POSIX ACL
ext4는 POSIX ACL을 system.posix_acl_access와 system.posix_acl_default xattr로 구현합니다:
# ACL 설정 예
setfacl -m u:www-data:rx /var/www/html
getfacl /var/www/html
# file: var/www/html
# owner: root
# group: root
# user::rwx
# user:www-data:r-x
# group::r-x
# mask::r-x
# other::r-x
고급 기능
메타데이터 체크섬
metadata_csum feature는 superblock, group descriptor, inode, extent, 디렉토리 블록, 저널 등 모든 메타데이터에 CRC32C 체크섬을 추가합니다. UUID 기반의 시드(s_checksum_seed)를 사용하여 볼륨 간 블록 혼동을 방지합니다.
/* 체크섬 계산 예 (fs/ext4/super.c) */
static __le32 ext4_superblock_csum(struct ext4_sb_info *sbi,
struct ext4_super_block *es)
{
__u32 csum;
csum = ext4_chksum(sbi, ~0, (char *)es,
offsetof(struct ext4_super_block, s_checksum));
return cpu_to_le32(csum);
}
파일시스템 암호화 (fscrypt)
fscrypt는 파일 내용, 파일명, symlink 대상을 per-file 키로 암호화합니다. 커널 4.1에서 도입되어 ext4, F2FS, UBIFS에서 지원합니다.
| 항목 | 설명 |
|---|---|
| 암호화 대상 | 파일 내용, 파일명, symlink target |
| 암호화 알고리즘 | AES-256-XTS (내용), AES-256-CTS (파일명) |
| 키 파생 | HKDF-SHA512로 마스터 키에서 per-file 키 유도 |
| 키 관리 | Linux 키링 서브시스템 (fscrypt_add_key) |
# fscrypt 사용 예
fscrypt setup /mnt/encrypted
fscrypt encrypt /mnt/encrypted/private
# 잠금 해제
fscrypt unlock /mnt/encrypted/private
무결성 검증 (fsverity)
fsverity는 Merkle tree 기반 무결성 검증을 제공합니다. 파일을 읽기 전용으로 설정하고, 각 블록의 해시를 트리 구조로 저장하여 변조를 감지합니다.
# fsverity 활성화
fsverity enable /path/to/file
# Merkle tree 해시는 EOF 뒤에 저장 (사용자에게 투명)
# 읽기 시 블록 단위로 해시 검증 → 불일치 시 -EIO 반환
# 서명된 fsverity (dm-verity와 유사한 신뢰 체인)
fsverity sign /path/to/file cert.pem --key key.pem
fsverity enable /path/to/file --signature=sig.bin
대소문자 무관 디렉토리 (casefold)
casefold feature는 디렉토리 검색 시 유니코드 대소문자를 구분하지 않습니다. Wine, Samba 등 Windows 호환 환경에서 유용합니다.
/* casefold 활성화 */
mkfs.ext4 -O casefold /dev/sda1
chattr +F /mnt/shared /* 디렉토리별 활성화 */
Inline Data
inline_data feature는 소파일(약 60바이트 이하)의 데이터를 inode의 i_block[] 영역에 직접 저장합니다. 별도 데이터 블록 할당이 필요 없어 공간 효율과 성능이 향상됩니다.
Project Quotas
Project ID 기반 디스크 사용량 제한입니다. uid/gid 쿼터와 달리, 디렉토리 트리 단위로 쿼터를 적용할 수 있어 컨테이너 환경에서 유용합니다.
# Project Quota 설정
mkfs.ext4 -O project,quota /dev/sda1
mount -o prjquota /dev/sda1 /mnt
# 프로젝트 ID 할당
chattr +P -p 1000 /mnt/container1
# 쿼터 제한 설정 (1GB)
repquota -Ps /mnt
setquota -P 1000 0 1048576 0 0 /mnt
Large Folio 지원 (커널 6.16+)
커널 6.16에서 ext4에 large folio 지원이 추가되었습니다. 기존에는 단일 페이지(4KB) 단위로 페이지 캐시를 관리했으나, large folio를 통해 여러 페이지를 하나의 folio로 묶어 관리합니다.
| 항목 | 기존 (4KB 페이지) | Large Folio (v6.16+) |
|---|---|---|
| 페이지 캐시 단위 | 4KB (base page) | 16KB~2MB (order-2 ~ order-9) |
| TLB 효율 | 페이지당 1 TLB 엔트리 | folio당 1 TLB 엔트리 (다수 페이지 커버) |
| I/O 병합 | readahead에 의존 | folio 단위 자연 병합 |
| 메타데이터 오버헤드 | 페이지당 struct page | folio당 1개 (나머지 tail page) |
핵심 커널 자료구조
ext4_sb_info
슈퍼블록의 메모리 내 표현으로, VFS struct super_block의 s_fs_info에 저장됩니다:
/* fs/ext4/ext4.h */
struct ext4_sb_info {
struct ext4_super_block *s_es; /* 디스크 superblock 포인터 */
struct buffer_head *s_sbh; /* superblock 버퍼 헤드 */
struct ext4_group_desc **s_group_desc; /* 그룹 디스크립터 배열 */
struct journal_s *s_journal; /* JBD2 저널 */
struct ext4_group_info ***s_group_info; /* 그룹별 mballoc 정보 */
unsigned long s_desc_per_block; /* 블록당 그룹 디스크립터 수 */
ext4_group_t s_groups_count; /* 전체 블록 그룹 수 */
unsigned long s_overhead; /* 메타데이터 블록 수 */
unsigned int s_cluster_ratio; /* bigalloc 클러스터 비율 */
unsigned int s_inode_size; /* inode 크기 */
...
};
ext4_inode_info
VFS struct inode를 확장하는 ext4 전용 inode 구조체입니다:
struct ext4_inode_info {
__le32 i_data[15]; /* extent tree 또는 간접 블록 */
__u32 i_flags; /* ext4 inode 플래그 */
ext4_lblk_t i_dir_start_lookup; /* HTree 검색 힌트 */
/* extent 상태 트리 (메모리 내) */
struct ext4_es_tree i_es_tree;
/* delalloc 예약 블록 */
unsigned int i_reserved_data_blocks;
/* 저널링 */
tid_t i_sync_tid; /* 마지막 동기화 트랜잭션 ID */
tid_t i_datasync_tid; /* 마지막 데이터 동기화 트랜잭션 ID */
struct inode vfs_inode; /* 내장된 VFS inode */
...
};
헬퍼 매크로
/* VFS inode에서 ext4_inode_info 획득 */
static inline struct ext4_inode_info *EXT4_I(struct inode *inode)
{
return container_of(inode, struct ext4_inode_info, vfs_inode);
}
/* VFS super_block에서 ext4_sb_info 획득 */
static inline struct ext4_sb_info *EXT4_SB(struct super_block *sb)
{
return sb->s_fs_info;
}
성능 튜닝
주요 Mount 옵션
| 옵션 | 설명 | 권장 시나리오 |
|---|---|---|
noatime | 접근 시간 갱신 중지 | 대부분의 워크로드 (메타데이터 쓰기 감소) |
delalloc | 지연 할당 (기본값) | 순차 쓰기가 많은 워크로드 |
discard | TRIM 명령 자동 전송 | SSD (또는 fstrim 크론잡 대체) |
barrier=1 | 쓰기 배리어 활성화 (기본값) | 데이터 무결성 중요 시 |
commit=N | 저널 커밋 간격 (초) | 기본 5초, 배치 성능 시 30~60 |
journal_async_commit | 비동기 커밋 블록 | SSD + 배리어 활성 시 |
max_batch_time=N | 트랜잭션 배치 최대 대기 (us) | 동시 커밋이 많을 때 |
dioread_nolock | Direct I/O 시 inode 잠금 회피 | DB 워크로드 (기본 활성) |
data=ordered | 데이터 → 메타 순서 보장 | 기본값, 안전 + 적절한 성능 |
# 일반적인 성능 최적화 마운트
mount -o noatime,delalloc,barrier=1,commit=5 /dev/sda1 /mnt
# SSD 최적화
mount -o noatime,discard,journal_async_commit /dev/nvme0n1p1 /mnt
# DB 워크로드
mount -o noatime,data=writeback,barrier=1,dioread_nolock /dev/sda1 /var/lib/mysql
mkfs 최적화 옵션
| 옵션 | 설명 |
|---|---|
-T usage-type | 워크로드 프로필: largefile, largefile4, small, news |
-i bytes-per-inode | inode 비율 조정 (작은 파일 많으면 줄임) |
-I inode-size | inode 크기 (기본 256, xattr 많으면 512) |
-J size=N | 저널 크기 (MB) |
-E stride=N,stripe-width=M | RAID 정렬 (stride=청크/블록, stripe-width=stride×디스크수) |
# 대용량 파일 서버 (적은 inode, 큰 저널)
mkfs.ext4 -T largefile -J size=1024 /dev/sda1
# RAID-5 (4디스크, 64K 청크, 4K 블록)
mkfs.ext4 -E stride=16,stripe-width=48 /dev/md0
# 소파일이 매우 많은 메일 서버
mkfs.ext4 -T small -i 4096 /dev/sda1
I/O 스케줄러 선택
| 스케줄러 | 권장 디바이스 | 특징 |
|---|---|---|
none | NVMe SSD | 스케줄링 오버헤드 제거, 디바이스 내부 큐에 위임 |
mq-deadline | SATA SSD, HDD | 지연 시간 보장, 기아 방지 |
bfq | 데스크탑, HDD | 공정성, 저지연 대화형 사용 |
kyber | 고성능 NVMe | 경량, 지연 목표 기반 |
# 현재 스케줄러 확인
cat /sys/block/sda/queue/scheduler
# 스케줄러 변경
echo mq-deadline > /sys/block/sda/queue/scheduler
모니터링
# ext4 통계
cat /proc/fs/ext4/sda1/mb_groups # mballoc 그룹 정보
cat /proc/fs/ext4/sda1/mb_stats # mballoc 통계
cat /sys/fs/ext4/sda1/delayed_allocation_blocks # delalloc 대기 블록
cat /sys/fs/ext4/sda1/lifetime_write_kbytes # 총 기록량
cat /sys/fs/ext4/sda1/session_write_kbytes # 세션 기록량
e2fsprogs 도구
| 도구 | 용도 | 주요 예시 |
|---|---|---|
mkfs.ext4 | 포맷 | mkfs.ext4 -O ^metadata_csum /dev/sda1 |
tune2fs | 기능 플래그/파라미터 변경 | tune2fs -O fast_commit /dev/sda1 |
e2fsck | 파일시스템 검사/복구 | e2fsck -f -y /dev/sda1 |
dumpe2fs | 슈퍼블록/그룹 정보 출력 | dumpe2fs -h /dev/sda1 |
debugfs | 대화형 디버깅 | debugfs /dev/sda1 |
resize2fs | 온라인/오프라인 리사이즈 | resize2fs /dev/sda1 |
debugfs 활용 예
# inode 상세 정보
debugfs -R "stat <12>" /dev/sda1
# extent tree 확인
debugfs -R "dump_extents <12>" /dev/sda1
# Level Entries Logical Physical Length Flags
# 0/0 1/4 0-127 1000-1127 128
# 0/0 2/4 128-255 2048-2175 128
# 디렉토리 HTree 확인
debugfs -R "htree_dump /home" /dev/sda1
# 저널 내용 확인
debugfs -R "logdump -a" /dev/sda1
# 삭제된 inode 복구 (undelete)
debugfs -w /dev/sda1
debugfs: lsdel # 삭제된 inode 목록
debugfs: dump <12> /tmp/recovered_file
tune2fs 활용 예
# 현재 feature 확인
tune2fs -l /dev/sda1 | grep "features"
# feature 활성화/비활성화
tune2fs -O fast_commit /dev/sda1 # Fast Commit 활성화
tune2fs -O ^fast_commit /dev/sda1 # Fast Commit 비활성화
# 예약 블록 비율 변경 (기본 5% → 1%)
tune2fs -m 1 /dev/sda1
# 마운트 횟수 기반 fsck 비활성화
tune2fs -c 0 -i 0 /dev/sda1
ext4 vs 다른 파일시스템 비교
| 항목 | ext4 | XFS | Btrfs | F2FS |
|---|---|---|---|---|
| 최대 볼륨 | 1 EiB | 8 EiB | 16 EiB | 16 TiB |
| 최대 파일 | 16 TiB | 8 EiB | 16 EiB | 3.94 TiB |
| COW | X | reflink 지원 | 기본 COW | X |
| 스냅샷 | X | X | O (subvolume) | X |
| 체크섬(데이터) | X (메타만) | reflink 시 | O (전체) | X |
| 압축 | X | X | O (zstd/lzo) | O (LZO/LZ4/zstd) |
| RAID 내장 | X | X | O (0/1/5/6/10) | X |
| 온라인 축소 | X | X | O | X |
| 온라인 확장 | O | O | O | X |
| fsck 속도 | 느림 (전체 스캔) | 빠름 (병렬) | 느림 | 보통 |
| 성숙도 | 매우 높음 | 매우 높음 | 높음 | 보통 |
| 주 사용처 | 범용, 루트 FS | 대용량, 엔터프라이즈 | NAS, 데스크탑 | Flash/eMMC |
- 범용 서버 / 루트 FS: ext4 (안정성, 호환성, 복구 도구 완비)
- 대용량 스토리지 / DB: XFS (대파일 성능, 병렬 I/O)
- 스냅샷 / 데이터 무결성: Btrfs (COW, 체크섬, 스냅샷)
- 임베디드 / Flash: F2FS (Flash 특성 최적화)
ext4 주요 버그 및 데이터 손실 사례
ext4는 리눅스에서 가장 널리 사용되는 파일시스템이지만, 개발 과정에서 심각한 버그와 데이터 손실 사례가 있었습니다. 이들 사례를 분석하면 파일시스템 설계의 근본적인 어려움과 POSIX 시맨틱의 한계를 이해할 수 있습니다.
1. Delayed Allocation 데이터 손실 (2008-2009)
ext4의 대표적인 초기 버그로, delayed allocation(지연 할당) 기능이 기존 ext3의 동작과 다르게 동작하면서 광범위한 데이터 손실을 유발한 사례입니다. 많은 프로그램이 임시 파일에 데이터를 쓴 뒤 rename()으로 원본 파일을 대체하는 패턴을 사용했는데, ext4에서는 이 패턴이 안전하지 않았습니다.
rename()으로 기존 파일을 대체하는 과정에서 시스템 크래시가 발생하면, 새 파일이 0바이트로 남게 되었습니다. 구 파일은 이미 rename()에 의해 삭제된 상태이므로, 데이터가 완전히 유실되었습니다. GNOME, KDE 등 주요 데스크탑 환경의 설정 파일이 이 방식으로 손실되는 사례가 다수 보고되었습니다.
원인 분석: delayed allocation은 실제 디스크 블록 할당을 fsync() 또는 writeback 시점까지 지연합니다. ext3에서는 data=ordered 모드에서 블록 할당이 비교적 즉시 이루어져 크래시 후에도 데이터가 보존될 가능성이 높았습니다. 그러나 ext4의 delayed allocation은 메타데이터와 데이터 모두 지연시켜, 크래시 시점에 디스크에 아무것도 기록되지 않은 상태가 발생했습니다.
/* 문제가 된 전형적인 프로그램 패턴 */
fd = open("config.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, data, len);
close(fd);
/* fsync() 호출 없이 바로 rename — ext4에서 위험! */
rename("config.tmp", "config");
/* 올바른 패턴: rename 전에 fsync() 필수 */
fd = open("config.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);
write(fd, data, len);
fsync(fd); /* 디스크에 데이터 기록 보장 */
close(fd);
rename("config.tmp", "config");
커널 수정: Theodore Ts'o는 원칙적으로 "프로그램이 fsync()를 호출해야 한다"고 주장했으나, 커뮤니티의 강한 반발로 결국 ext4에 호환성 패치가 적용되었습니다. ext4_da_writepages()에서 rename()이나 truncate()를 감지하면 즉시 블록 할당을 수행하도록 변경되었습니다.
/* fs/ext4/inode.c - delayed allocation 안전 장치 (간략화) */
static int ext4_da_writepages(struct address_space *mapping,
struct writeback_control *wbc)
{
struct inode *inode = mapping->host;
/*
* rename/truncate 감지 시 즉시 할당으로 전환
* EXT4_STATE_DA_ALLOC_CLOSE: close() 시 지연 할당 블록 즉시 기록
*/
if (ext4_test_inode_state(inode, EXT4_STATE_DA_ALLOC_CLOSE)) {
ext4_alloc_da_blocks(inode);
}
/* ... writeback 진행 ... */
}
rename() 후 fsync() 없이도 데이터 영속성을 보장하지 않습니다. 그러나 ext3에서 사실상 보장되었던 동작에 수많은 프로그램이 의존하고 있었습니다. 이 사건은 "사양 vs 실제 동작"의 괴리를 보여주는 대표적 사례이며, 파일시스템 변경이 사용자 공간 프로그램에 미치는 영향을 과소평가해서는 안 된다는 교훈을 남겼습니다.
2. JBD2 저널 손상과 복구
JBD2(Journaling Block Device 2)는 ext4의 저널링 엔진으로, 메타데이터(및 선택적으로 데이터)의 원자적 갱신을 보장합니다. 그러나 전원 손실이나 하드웨어 오류 시 저널 자체가 손상되는 사례가 보고되었습니다.
JBD2_FEATURE_INCOMPAT_CSUM_V3 도입: 이 문제를 해결하기 위해 저널 블록에 CRC32C 체크섬을 추가하는 JBD2_FEATURE_INCOMPAT_CSUM_V3 기능이 도입되었습니다. 이전 버전(CSUM_V2)의 체크섬 범위가 불완전했던 문제를 수정하여, 모든 저널 블록(디스크립터, 커밋, 리보크, 데이터 블록)에 대한 완전한 체크섬을 보장합니다.
/* fs/jbd2/commit.c - 저널 커밋 시 체크섬 삽입 (간략화) */
static void jbd2_descriptor_block_csum_set(
journal_t *j, struct buffer_head *bh)
{
struct jbd2_journal_block_tail *tail;
__u32 csum;
if (!jbd2_journal_has_csum_v2or3(j))
return;
tail = (struct jbd2_journal_block_tail *)
(((char *)bh->b_data) + j->j_blocksize -
sizeof(struct jbd2_journal_block_tail));
tail->t_checksum = 0;
csum = jbd2_chksum(j, j->j_csum_seed, bh->b_data, j->j_blocksize);
tail->t_checksum = cpu_to_be32(csum);
}
data 모드별 안전성 비교:
| 저널 모드 | 메타데이터 보호 | 데이터 보호 | 성능 | 크래시 안전성 |
|---|---|---|---|---|
data=journal | O | O (저널에 기록) | 낮음 | 가장 높음 |
data=ordered | O | 순서 보장 | 중간 (기본값) | 높음 |
data=writeback | O | X | 높음 | 낮음 (stale 데이터 노출 가능) |
e2fsck -f로 강제 복구를 수행할 때 저널이 손상된 상태라면, 저널 재생이 파일시스템을 추가적으로 손상시킬 수 있습니다. 심각한 저널 손상 시에는 e2fsck -f -E journal_only로 저널만 먼저 검증하거나, 최악의 경우 tune2fs -O ^has_journal로 저널을 제거한 뒤 복구를 시도해야 합니다. 반드시 복구 전 디스크 이미지 백업(dd 또는 ddrescue)을 수행하십시오.
3. Extent Tree 손상 사례
ext4의 extent tree는 파일의 물리적 블록 매핑을 B-tree 형태로 관리합니다. 대용량 파일이나 심한 단편화 상태에서 extent tree의 깊이(depth)가 증가하며, 이 과정에서 다양한 손상 사례가 보고되었습니다.
- Depth 오류: 대용량 파일의 extent tree depth가 실제 트리 구조와 불일치하여 파일 접근이 완전히 불가능해지는 사례. 특히 extent 분할(split) 과정에서 전원 손실 시 발생
- Status Tree 캐시 불일치: 메모리의 extent status tree 캐시가 디스크의 실제 extent와 달라 잘못된 블록을 읽거나 덮어쓰는 사례. 장시간 운영 중인 서버에서 간헐적으로 보고됨
- e4defrag 중 크래시: 온라인 조각 모음(
e4defrag) 실행 중 크래시가 발생하면 extent가 이전 위치와 새 위치 모두에서 참조되어 데이터 중복 또는 손상 발생
Extent 유효성 검증 강화: 이러한 문제들을 해결하기 위해 ext4_ext_check()에서 extent의 논리적 일관성을 엄격하게 검증하도록 강화되었습니다.
/* fs/ext4/extents.c - extent 유효성 검증 (간략화) */
static int ext4_ext_check(struct inode *inode,
struct ext4_extent_header *eh,
int depth, ext4_fsblk_t pblk)
{
const char *error_msg;
/* 매직 넘버 검증 */
if (unlikely(eh->eh_magic != EXT4_EXT_MAGIC)) {
error_msg = "invalid magic number";
goto corrupted;
}
/* depth 범위 검증 */
if (unlikely(eh->eh_depth != depth)) {
error_msg = "unexpected eh_depth";
goto corrupted;
}
/* 엔트리 수 상한 검증 */
if (unlikely(eh->eh_entries > eh->eh_max)) {
error_msg = "invalid eh_entries";
goto corrupted;
}
/* metadata_csum 활성 시 체크섬 검증 */
if (ext4_has_metadata_csum(inode->i_sb) &&
!ext4_extent_block_csum_verify(inode, eh)) {
error_msg = "extent block checksum failed";
goto corrupted;
}
return 0;
corrupted:
ext4_error_inode(inode, "ext4_ext_check",
"%s (depth %d, pblk %llu)",
error_msg, depth, pblk);
return -EFSCORRUPTED;
}
metadata_csum 기능을 활성화하면 extent 블록을 포함한 모든 메타데이터 블록에 CRC32C 체크섬이 추가되어 손상을 조기에 탐지할 수 있습니다. 새 파일시스템 생성 시 mkfs.ext4 -O metadata_csum을 권장합니다. 기존 파일시스템은 tune2fs -O metadata_csum으로 활성화할 수 있으나, 반드시 e2fsck -f를 먼저 수행해야 합니다.
4. ext4 마운트 옵션 보안 문제
ext4의 마운트 옵션 설정에 따라 데이터 무결성과 보안에 심각한 영향을 미칠 수 있습니다. 성능 최적화를 위해 안전 장치를 해제하는 설정이 프로덕션 환경에서 사용되어 문제가 된 사례들입니다.
/* 위험한 마운트 옵션 조합 */
# 절대 프로덕션에서 사용하지 마십시오:
mount -o barrier=0,data=writeback,nodelalloc /dev/sda1 /mnt
# 권장 안전 설정:
mount -o barrier=1,data=ordered,delalloc /dev/sda1 /mnt
# 최대 안전 설정 (성능 저하 감수):
mount -o barrier=1,data=journal,journal_checksum /dev/sda1 /mnt
주요 보안 및 안전성 문제:
| 마운트 옵션 | 위험 수준 | 영향 | 권장사항 |
|---|---|---|---|
barrier=0 | 높음 | 쓰기 순서 미보장으로 저널 무효화 가능 | BBU RAID에서만 사용 |
nodelalloc | 낮음 | 성능 저하, 단편화 증가 | delayed alloc 버그 우회 시에만 |
max_dir_size_kb 미설정 | 중간 | 거대 디렉토리의 해시 충돌로 DoS 가능 | 공유 시스템에서 제한 설정 |
errors=continue | 높음 | I/O 에러 발생 시 계속 동작 → 데이터 추가 손상 | errors=remount-ro 권장 |
errors=continue는 파일시스템 에러 발생 시에도 계속 동작합니다. 디스크 불량 섹터나 메타데이터 손상 상태에서 계속 쓰기를 수행하면 손상이 확산됩니다. 프로덕션 환경에서는 반드시 errors=remount-ro(읽기 전용 재마운트) 또는 errors=panic(시스템 중단)으로 설정하여, 에러 감지 즉시 추가 손상을 방지해야 합니다. tune2fs -e remount-ro /dev/sdXN으로 설정할 수 있습니다.
Extent Tree 심화: B+tree 구조와 탐색 알고리즘
ext4의 Extent Tree는 B+tree 변형으로, 파일의 논리 블록 번호(logical block number)를 디스크의 물리 블록 번호(physical block number)로 매핑합니다. 이 섹션에서는 앞서 소개한 기본 구조를 넘어, 트리의 분할·병합·깊이 변화, 커널 내부의 탐색 경로 최적화, 그리고 실제 디버깅 사례를 심층적으로 다룹니다.
트리 깊이와 최대 엔트리 수
extent tree의 깊이는 파일 크기와 단편화 정도에 의존합니다. inode 내부(60바이트)에 root 노드가 저장되며, 외부 블록(4096바이트)은 더 많은 엔트리를 수용합니다.
| 위치 | 가용 크기 | 최대 idx/extent | 비고 |
|---|---|---|---|
| inode i_block[15] | 60 바이트 | 4개 | header(12) + entry(12) × 4 |
| 외부 블록 (4K) | 4096 바이트 | 340개 | header(12) + entry(12) × 340 |
| depth=0 (inode 내장) | — | 4 extents | 최대 ~128K 블록 = 512MB (4K 블록) |
| depth=1 | — | 4 × 340 = 1,360 extents | 약 5.3GB ~ 40GB+ |
| depth=2 | — | 4 × 340² = 462,400 extents | 수 TB 규모 |
EXT4_MAX_EXTENT_DEPTH는 5로 정의되어 있지만, 실제 운영 환경에서 depth 3 이상은 극히 드뭅니다. depth=2면 수백만 개의 extent를 수용할 수 있어, 수 TB의 극도로 단편화된 파일도 처리 가능합니다.
B+tree 탐색 경로: ext4_find_extent()
ext4_find_extent()는 root에서 시작하여 이진 탐색으로 각 깊이의 적절한 인덱스 엔트리를 찾고, 해당 자식 블록을 읽어 리프까지 도달합니다.
/* fs/ext4/extents.c - ext4_find_extent() 핵심 로직 (간략화) */
struct ext4_ext_path *
ext4_find_extent(struct inode *inode, ext4_lblk_t block,
struct ext4_ext_path **orig_path, int flags)
{
struct ext4_extent_header *eh;
struct ext4_ext_path *path;
int depth, i;
eh = ext_inode_hdr(inode); /* inode의 i_block[] → extent header */
depth = ext_depth(inode); /* eh->eh_depth */
/* depth+1 개의 path 엔트리 할당 */
path = kcalloc(depth + 2, sizeof(*path), GFP_NOFS);
path[0].p_hdr = eh;
i = 0;
while (i < depth) {
/* 이진 탐색으로 block을 포함하는 idx 찾기 */
ext4_ext_binsearch_idx(inode, path + i, block);
/* idx가 가리키는 자식 블록 읽기 */
path[i + 1].p_bh = read_extent_tree_block(
inode, path[i].p_idx, depth - i - 1, flags);
path[i + 1].p_hdr = ext_block_hdr(path[i + 1].p_bh);
i++;
}
/* 리프 레벨: extent 이진 탐색 */
ext4_ext_binsearch(inode, path + i, block);
return path;
}
Extent 분할과 병합
새로운 extent를 삽입할 때 리프 노드가 가득 차면 분할(split)이 발생합니다. 반대로 인접한 extent가 물리적으로 연속이면 병합(merge)으로 엔트리를 줄입니다.
/* fs/ext4/extents.c - extent 삽입 흐름 (간략화) */
int ext4_ext_insert_extent(handle_t *handle,
struct inode *inode,
struct ext4_ext_path **ppath,
struct ext4_extent *newext, int gb_flags)
{
struct ext4_ext_path *path = *ppath;
int depth = ext_depth(inode);
/* 1단계: 인접 extent와 병합 시도 */
if (ext4_ext_try_to_merge_right(inode, path, newext))
goto merge;
if (ext4_ext_try_to_merge(handle, inode, path, newext))
goto merge;
/* 2단계: 리프에 공간 있으면 직접 삽입 */
if (le16_to_cpu(path[depth].p_hdr->eh_entries) <
le16_to_cpu(path[depth].p_hdr->eh_max)) {
ext4_ext_insert(handle, inode, path, newext);
goto out;
}
/* 3단계: 공간 부족 → 노드 분할 */
ext4_ext_create_new_leaf(handle, inode, gb_flags,
ppath, newext);
/* 분할은 리프→부모→루트 방향으로 전파될 수 있음
루트 분할 시 트리 깊이 증가 (depth++) */
out:
ext4_ext_dirty(handle, inode, path + depth);
return 0;
merge:
ext4_ext_dirty(handle, inode, path + depth);
return 0;
}
Extent Status Tree (메모리 내 캐시)
커널은 extent tree의 디스크 I/O를 줄이기 위해 extent status tree를 메모리에 유지합니다. 이 red-black tree는 각 inode별로 extent의 상태(written, unwritten, delayed, hole)를 캐싱합니다.
/* fs/ext4/extents_status.h */
struct ext4_es_tree {
struct rb_root root; /* red-black tree 루트 */
struct extent_status *cache_es; /* 마지막 조회 캐시 */
};
struct extent_status {
struct rb_node rb_node;
ext4_lblk_t es_lblk; /* 논리 블록 시작 */
ext4_lblk_t es_len; /* 길이 */
ext4_fsblk_t es_pblk; /* 물리 블록 + 상태 비트 */
};
/* 상태 플래그 (es_pblk 상위 비트) */
#define ES_WRITTEN_B 0 /* 디스크에 기록 완료 */
#define ES_UNWRITTEN_B 1 /* 할당됨, 미기록 (fallocate) */
#define ES_DELAYED_B 2 /* 지연 할당 (아직 디스크 블록 없음) */
#define ES_HOLE_B 3 /* 구멍 (sparse file) */
cache_es 필드는 마지막으로 조회한 extent를 캐싱합니다. 순차 읽기/쓰기에서는 대부분 이 캐시에서 즉시 히트하여 red-black tree 탐색을 생략합니다. /proc/fs/ext4/<dev>/es_stats에서 캐시 히트율을 확인할 수 있습니다.
JBD2 커밋 흐름 심화
JBD2(Journaling Block Device 2)의 커밋 과정은 단순한 "기록 후 플러시"가 아닙니다. 트랜잭션 상태 전이, 체크포인트 메커니즘, 복구 알고리즘까지 이해해야 ext4의 내구성 보장 방식을 정확히 파악할 수 있습니다.
트랜잭션 상태 전이 상세
JBD2 트랜잭션은 다음 8가지 상태를 순차적으로 거칩니다:
jbd2_journal_commit_transaction() 핵심 흐름
/* fs/jbd2/commit.c - 커밋 함수 핵심 단계 (간략화) */
void jbd2_journal_commit_transaction(journal_t *journal)
{
transaction_t *commit_transaction = journal->j_running_transaction;
/* Phase 0: 트랜잭션 잠금 (T_RUNNING → T_LOCKED) */
jbd2_journal_lock_updates(journal);
commit_transaction->t_state = T_LOCKED;
/* Phase 1: 새 트랜잭션 시작 (T_LOCKED → T_SWITCH → T_FLUSH) */
jbd2_journal_start_new_transaction(journal);
commit_transaction->t_state = T_FLUSH;
jbd2_journal_unlock_updates(journal);
/* Phase 2: ordered 모드 데이터 쓰기 */
if (journal->j_flags & JBD2_ORDERED) {
jbd2_journal_submit_inode_data_buffers(commit_transaction);
jbd2_journal_finish_inode_data_buffers(commit_transaction);
}
/* Phase 3: 저널 디스크립터 블록 + 메타데이터 블록 기록 */
while (bufs) {
jbd2_journal_write_metadata_buffer(...);
submit_bh(REQ_OP_WRITE, bh);
}
/* Phase 4: I/O 완료 대기 */
while (!list_empty(&io_bufs))
wait_on_buffer(bh);
/* Phase 5: 커밋 블록 기록 (T_FLUSH → T_COMMIT) */
commit_transaction->t_state = T_COMMIT;
jbd2_write_commit_record(journal, commit_transaction);
/* Phase 6: 디스크 플러시 (T_COMMIT → T_COMMIT_DFLUSH → T_COMMIT_JFLUSH) */
if (journal->j_flags & JBD2_BARRIER)
blkdev_issue_flush(journal->j_dev);
/* Phase 7: 완료 (T_FINISHED) */
commit_transaction->t_state = T_FINISHED;
jbd2_journal_done_transaction(journal, commit_transaction);
}
체크포인트 메커니즘
체크포인트는 커밋된 트랜잭션의 데이터를 원래 디스크 위치에 반영하고, 저널 공간을 재사용 가능하게 만드는 과정입니다.
| 단계 | 설명 | 트리거 조건 |
|---|---|---|
| 1. 체크포인트 대상 선택 | 가장 오래된 커밋된 트랜잭션 선택 | 저널 공간 부족, 주기적 타이머 |
| 2. 더티 버퍼 기록 | 트랜잭션의 수정된 버퍼를 원래 위치에 기록 | — |
| 3. I/O 완료 대기 | 모든 버퍼의 디스크 기록 완료 확인 | — |
| 4. 저널 헤드 전진 | j_checkpoint_transactions에서 제거, 저널 공간 해제 | — |
/* fs/jbd2/checkpoint.c - 체크포인트 (간략화) */
int jbd2_log_do_checkpoint(journal_t *journal)
{
transaction_t *transaction;
tid_t this_tid;
/* 가장 오래된 체크포인트 대상 트랜잭션 */
transaction = journal->j_checkpoint_transactions;
if (!transaction)
return 0;
this_tid = transaction->t_tid;
/* 트랜잭션의 모든 버퍼를 원래 위치에 기록 */
do {
struct buffer_head *bh = transaction->t_checkpoint_list;
if (buffer_dirty(bh))
write_dirty_buffer(bh, REQ_SYNC);
} while (bh != transaction->t_checkpoint_list);
/* I/O 완료 대기 후 트랜잭션 해제 */
__jbd2_journal_drop_transaction(journal, transaction);
return 1; /* 저널 공간 해제됨 */
}
저널 복구 알고리즘
비정상 종료 후 마운트 시 JBD2는 3단계 복구를 수행합니다. 각 단계의 목적과 revoke 레코드의 역할을 이해하는 것이 중요합니다.
/* fs/jbd2/recovery.c */
int jbd2_journal_recover(journal_t *journal)
{
int err;
recovery_info_t info;
/* Pass 1: SCAN - 유효한 트랜잭션 범위 파악
* 저널 superblock의 시퀀스 번호부터 시작하여
* 유효한 커밋 블록이 있는 마지막 트랜잭션까지 스캔 */
err = do_one_pass(journal, &info, PASS_SCAN);
/* Pass 2: REVOKE - 덮어쓴 블록 목록 수집
* revoke 레코드는 "이 블록은 이후에 다시 변경되었으니
* 오래된 저널 데이터로 복구하지 마라"는 의미 */
err = do_one_pass(journal, &info, PASS_REVOKE);
/* Pass 3: REPLAY - 커밋된 블록을 원래 위치에 복원
* revoke 목록에 없는 블록만 디스크에 재기록 */
err = do_one_pass(journal, &info, PASS_REPLAY);
/* 복구 완료: 저널 superblock 갱신 */
jbd2_journal_clear_revoke(journal);
jbd2_journal_update_sb_log_tail(journal, ...);
return err;
}
mballoc 다중 블록 할당기 심화
ext4의 mballoc(Multi-Block Allocator)은 단일 블록 할당기인 ext3에 비해 연속 블록 할당 성능을 크게 향상시킵니다. 이 섹션에서는 buddy bitmap 구조, 그룹 디스크립터, 선할당(preallocation) 메커니즘, 그리고 정규화(normalization) 전략을 심층적으로 분석합니다.
Buddy Bitmap 구조
각 블록 그룹은 블록 비트맵(디스크)과 buddy 비트맵(메모리)을 가집니다. buddy 비트맵은 2의 거듭제곱 크기별로 연속 빈 블록을 추적합니다.
할당 컨텍스트 (ext4_allocation_context)
/* fs/ext4/mballoc.h - 할당 컨텍스트 (간략화) */
struct ext4_allocation_context {
struct inode *ac_inode; /* 할당 요청 inode */
struct super_block *ac_sb; /* 슈퍼블록 */
/* 원래 요청 */
struct ext4_free_extent ac_o_ex; /* original: 요청한 범위 */
/* 정규화된 요청 */
struct ext4_free_extent ac_g_ex; /* goal: 정규화된 범위 */
/* 최적 결과 */
struct ext4_free_extent ac_b_ex; /* best: 찾은 최적 범위 */
/* 선할당 */
struct ext4_prealloc_space *ac_pa; /* 사용된 선할당 */
__u16 ac_criteria; /* 현재 탐색 기준 (CR 0~3) */
__u16 ac_flags; /* 플래그 (EXT4_MB_HINT_*) */
__u8 ac_status; /* 할당 상태 */
};
/* ext4_free_extent - 빈 블록 범위 표현 */
struct ext4_free_extent {
ext4_lblk_t fe_logical; /* 논리 블록 시작 */
ext4_grpblk_t fe_start; /* 그룹 내 블록 오프셋 */
ext4_group_t fe_group; /* 블록 그룹 번호 */
ext4_grpblk_t fe_len; /* 연속 블록 수 */
};
CR(Criteria) 레벨별 탐색 알고리즘
/* fs/ext4/mballoc.c - ext4_mb_regular_allocator() 핵심 (간략화) */
static noinline_for_stack int
ext4_mb_regular_allocator(struct ext4_allocation_context *ac)
{
int cr;
/* 먼저 선할당 공간에서 할당 시도 */
if (ext4_mb_use_preallocated(ac))
return 0;
/* CR 0부터 CR 3까지 점진적으로 완화 */
for (cr = 0; cr < 4; cr++) {
ac->ac_criteria = cr;
/* 모든 블록 그룹을 순회 */
for (group = ac->ac_g_ex.fe_group; ...;) {
struct ext4_group_info *grp;
grp = ext4_get_group_info(sb, group);
/* CR 0: 정확한 order 매칭만 수락 */
if (cr == 0 && grp->bb_largest_free_order < ac->ac_order)
continue;
/* CR 1: 정확한 order, 전체 그룹 검색 */
/* CR 2: 빈 블록이 있으면 수락 (단편 허용) */
/* CR 3: 정규화 해제, 최소 크기만 할당 */
ext4_mb_find_by_goal(ac, grp);
if (ac->ac_status == AC_STATUS_FOUND)
break;
}
if (ac->ac_status == AC_STATUS_FOUND)
break;
}
return 0;
}
/proc/fs/ext4/<dev>/mb_stats에서 각 CR 레벨별 할당 횟수, 선할당 사용 횟수, buddy 캐시 히트율 등을 확인할 수 있습니다. CR 2 이상이 빈번하면 디스크 단편화가 심한 상태입니다.
Fast Commit 심화
Linux 5.10에서 도입된 Fast Commit은 전체 메타데이터 블록을 저널에 복사하는 기존 JBD2 방식 대신, 변경 사항(delta)만 기록하는 경량 커밋 메커니즘입니다. 이를 통해 fsync() 지연을 크게 줄이면서도 크래시 일관성을 유지합니다.
Fast Commit vs Full Commit 비교
Fast Commit 처리 흐름
/* fs/ext4/fast_commit.c - Fast Commit 주요 구조 (간략화) */
/* FC 영역: 저널의 끝에 위치하는 별도 블록들 */
struct ext4_fc_tl { /* Tag-Length 헤더 */
__le16 fc_tag; /* 태그 유형 */
__le16 fc_len; /* 데이터 길이 */
};
/* fsync() → Fast Commit 시도 */
int ext4_fc_commit(journal_t *journal, tid_t commit_tid)
{
/* 1. FC 가능 여부 확인 */
if (!ext4_fc_eligible(sbi))
return jbd2_complete_transaction(journal, commit_tid);
/* FC 불가 → Full Commit으로 폴백 */
/* 2. FC 추적 목록에서 변경된 inode들 수집 */
list_for_each_entry(ei, &sbi->s_fc_q[FC_Q_MAIN], ...) {
/* 3. 각 inode의 변경사항을 FC 태그로 직렬화 */
ext4_fc_write_inode(inode, ...);
ext4_fc_write_inode_data(inode, ...);
}
/* 4. FC TAIL 태그 기록 (CRC32 포함) */
ext4_fc_write_tail(sb, commit_tid);
/* 5. 디스크 플러시 */
blkdev_issue_flush(journal->j_dev);
return 0;
}
/* FC 불가능 조건 (Full Commit으로 폴백) */
/* - 디렉토리 rename 발생
- 지원하지 않는 연산 (truncate, xattr 변경 등)
- FC 블록 공간 부족
- FC 직렬화 중 에러 */
Fast Commit 복구
크래시 후 마운트 시, FC 복구는 일반 JBD2 복구 이후에 수행됩니다. FC 블록을 순차적으로 읽으며 태그별로 재생(replay)합니다.
/* fs/ext4/fast_commit.c - FC 복구 (간략화) */
int ext4_fc_replay(journal_t *journal, ...)
{
/* FC 블록 순회 */
while ((bh = ext4_fc_replay_next_block(journal, ...))) {
struct ext4_fc_tl *tl = buf;
switch (le16_to_cpu(tl->fc_tag)) {
case EXT4_FC_TAG_ADD_RANGE:
ext4_fc_replay_add_range(sb, tl, ...);
break;
case EXT4_FC_TAG_DEL_RANGE:
ext4_fc_replay_del_range(sb, tl, ...);
break;
case EXT4_FC_TAG_CREAT:
ext4_fc_replay_create(sb, tl, ...);
break;
case EXT4_FC_TAG_INODE:
ext4_fc_replay_inode(sb, tl, ...);
break;
case EXT4_FC_TAG_TAIL:
/* CRC32 검증 → 실패 시 이후 FC 데이터 무시 */
if (!ext4_fc_replay_check_crc(...))
return 0;
break;
}
}
return 0;
}
fast_commit feature flag로 활성화됩니다(tune2fs -O fast_commit). 이 기능이 활성화된 볼륨은 이전 버전의 e2fsck(1.46.2 이전)로 검사할 수 없습니다. 또한 data=journal 모드와는 호환되지 않습니다.
인라인 데이터 (Inline Data) 심화
inline_data feature는 소규모 파일의 데이터를 별도 데이터 블록 대신 inode 자체에 직접 저장합니다. 이를 통해 추가 블록 할당과 디스크 I/O를 피하고, 소파일이 많은 워크로드에서 공간 효율과 성능을 크게 향상시킵니다.
인라인 데이터 저장 레이아웃
인라인 데이터 읽기/쓰기 코드
/* fs/ext4/inline.c - 인라인 데이터 읽기 (간략화) */
int ext4_read_inline_data(struct inode *inode,
void *buffer, unsigned int len,
struct ext4_iloc *iloc)
{
struct ext4_inode *raw_inode;
void *inline_start;
int cp_len, inline_size;
raw_inode = ext4_raw_inode(iloc);
/* 1단계: i_block[] 영역에서 읽기 (최대 60바이트) */
inline_start = (void *)raw_inode->i_block;
cp_len = min(len, (unsigned int)EXT4_MIN_INLINE_DATA_SIZE);
memcpy(buffer, inline_start, cp_len);
/* 2단계: xattr 영역의 추가 데이터 읽기 */
inline_size = ext4_get_inline_data_size(inode);
if (inline_size > EXT4_MIN_INLINE_DATA_SIZE) {
ext4_read_inline_data_from_xattr(inode,
buffer + EXT4_MIN_INLINE_DATA_SIZE,
inline_size - EXT4_MIN_INLINE_DATA_SIZE);
}
return inline_size;
}
/* 인라인 → 일반 파일 변환 (파일 크기 증가 시) */
int ext4_da_convert_inline_data_to_extent(
struct address_space *mapping,
struct inode *inode,
unsigned flags, struct folio **foliop)
{
/* 1. 인라인 데이터를 페이지 캐시로 복사 */
/* 2. inode에서 인라인 데이터 제거 */
/* 3. extent tree 초기화 */
/* 4. 지연 할당 예약 */
return 0;
}
| 항목 | 인라인 데이터 | 일반 데이터 블록 |
|---|---|---|
| 최대 크기 | ~160바이트 (inode_size=256 기준) | 16TiB |
| 블록 할당 | 불필요 | 필요 (최소 1 블록) |
| 읽기 I/O | inode 읽기에 포함 | 추가 I/O 필요 |
| 적합한 용도 | 심볼릭 링크, 빈 파일, 설정 파일 | 일반 파일 |
| 활성화 | mkfs.ext4 -O inline_data | 기본 |
inline_data는 inode_size가 256바이트 이상이어야 사용할 수 있습니다(기본값이 256이므로 보통 조건을 만족합니다). 또한 디렉토리의 인라인 데이터도 지원되어, 엔트리가 적은 디렉토리는 별도 데이터 블록 없이 inode 내부에 저장됩니다.
bigalloc (클러스터 기반 할당) 심화
bigalloc feature는 블록 비트맵의 할당 단위를 단일 블록(4KB)에서 클러스터(여러 블록의 그룹)로 변경합니다. 대용량 파일이 주로 저장되는 환경에서 비트맵 관리 오버헤드를 줄이고, 대규모 연속 할당 효율을 높입니다.
bigalloc 아키텍처
bigalloc 커널 내부 구현
/* fs/ext4/ext4.h - bigalloc 관련 매크로/함수 */
/* 블록 번호 → 클러스터 번호 변환 */
static inline ext4_grpblk_t
EXT4_B2C(struct ext4_sb_info *sbi, ext4_lblk_t blk)
{
return blk >> sbi->s_log_cluster_size;
}
/* 클러스터 번호 → 블록 번호 변환 (클러스터 시작) */
static inline ext4_lblk_t
EXT4_C2B(struct ext4_sb_info *sbi, ext4_grpblk_t cluster)
{
return cluster << sbi->s_log_cluster_size;
}
/* 블록 수 → 클러스터 수 변환 (올림) */
static inline ext4_grpblk_t
EXT4_NUM_B2C(struct ext4_sb_info *sbi, ext4_lblk_t blks)
{
return (blks + sbi->s_cluster_ratio - 1) >> sbi->s_log_cluster_size;
}
/* bigalloc에서 delalloc 예약 블록 관리
* 같은 클러스터에 속하는 블록은 중복 예약하지 않음 */
static int ext4_cluster_alloc(struct inode *inode,
ext4_lblk_t lblk, int num)
{
struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
ext4_lblk_t end = lblk + num - 1;
ext4_lblk_t cluster_start = EXT4_C2B(sbi, EXT4_B2C(sbi, lblk));
ext4_lblk_t cluster_end = EXT4_C2B(sbi, EXT4_B2C(sbi, end));
/* 클러스터 경계를 고려한 예약 블록 수 계산 */
return EXT4_B2C(sbi, end) - EXT4_B2C(sbi, lblk) + 1;
}
# bigalloc 파일시스템 생성
mkfs.ext4 -O bigalloc -C 65536 /dev/sda1 # 64KB 클러스터
mkfs.ext4 -O bigalloc -C 262144 /dev/sda1 # 256KB 클러스터
mkfs.ext4 -O bigalloc -C 1048576 /dev/sda1 # 1MB 클러스터
# 클러스터 크기 확인
dumpe2fs /dev/sda1 | grep -i cluster
# Cluster size: 65536
inline_data feature와 함께 사용하면 소파일의 단편화를 완화할 수 있습니다.
casefold (대소문자 무관 파일 이름) 심화
casefold feature는 디렉토리 검색 시 유니코드 대소문자 접기(case folding)를 적용하여, "File.TXT"와 "file.txt"를 동일한 파일로 취급합니다. Wine, Samba, Android 등 Windows/macOS 호환이 필요한 환경에서 핵심적인 기능입니다.
유니코드 정규화와 대소문자 접기
casefold 커널 구현
/* fs/ext4/namei.c - casefold 검색 (간략화) */
static int ext4_ci_compare(
const struct inode *parent,
const struct qstr *name, /* 검색할 이름 */
const struct qstr *entry, /* 디렉토리 엔트리 이름 */
bool quick)
{
const struct super_block *sb = parent->i_sb;
const struct unicode_map *um = sb->s_encoding;
int ret;
/* 유니코드 case-insensitive 비교 */
ret = utf8_strncasecmp(um, name, entry);
if (ret < 0) {
/* 유니코드 처리 실패 시: strict 모드면 에러,
아니면 바이트 단위 비교로 폴백 */
if (sb_has_strict_encoding(sb))
return -EINVAL;
return ext4_ci_compare_fallback(name, entry);
}
return ret; /* 0 = 매칭, 양수 = 불일치 */
}
/* SipHash를 사용한 case-insensitive 해시
* HTree에서 casefold된 이름으로 해시를 계산 */
int ext4_fname_setup_ci_filename(
struct inode *dir,
const struct qstr *iname,
struct ext4_filename *fname)
{
const struct unicode_map *um = dir->i_sb->s_encoding;
/* casefold된 이름 생성 */
fname->cf_name.len = utf8_casefold(um, iname,
fname->cf_name.name, EXT4_NAME_LEN);
return 0;
}
# casefold 파일시스템 생성
mkfs.ext4 -O casefold /dev/sda1
# 특정 디렉토리에 casefold 활성화
chattr +F /mnt/shared
# strict 모드 (잘못된 유니코드 시퀀스 거부)
mkfs.ext4 -O casefold -E encoding_flags=strict /dev/sda1
# 유니코드 버전 확인
dumpe2fs /dev/sda1 | grep -i encoding
# Encoding: utf8-14.0
# 동작 테스트
mkdir /mnt/shared && chattr +F /mnt/shared
echo "hello" > /mnt/shared/Test.TXT
cat /mnt/shared/test.txt # "hello" 출력 (대소문자 무관)
unicode 모듈 버전에 의존합니다. 파일시스템 생성 시의 유니코드 버전이 superblock에 기록되며, 이후 커널 업그레이드로 유니코드 버전이 달라져도 하위 호환성이 유지됩니다.
ext4 커널 내부 구조 심화
ext4 커널 코드는 fs/ext4/ 디렉토리에 약 50개의 소스 파일로 구성됩니다. 이 섹션에서는 핵심 자료구조의 관계, VFS 연결 인터페이스, 그리고 코드 모듈 구조를 심층적으로 분석합니다.
ext4 소스 코드 맵
VFS 연산 테이블
/* fs/ext4/super.c - ext4 VFS 연결 */
static const struct super_operations ext4_sops = {
.alloc_inode = ext4_alloc_inode,
.free_inode = ext4_free_in_core_inode,
.write_inode = ext4_write_inode,
.dirty_inode = ext4_dirty_inode,
.evict_inode = ext4_evict_inode,
.put_super = ext4_put_super,
.sync_fs = ext4_sync_fs,
.statfs = ext4_statfs,
.show_options = ext4_show_options,
};
/* fs/ext4/file.c - 일반 파일 연산 */
const struct file_operations ext4_file_operations = {
.llseek = ext4_llseek,
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
.mmap = ext4_file_mmap,
.open = ext4_file_open,
.release = ext4_release_file,
.fsync = ext4_sync_file,
.splice_read = ext4_file_splice_read,
.fallocate = ext4_fallocate,
};
/* fs/ext4/namei.c - 디렉토리 inode 연산 */
const struct inode_operations ext4_dir_inode_operations = {
.create = ext4_create,
.lookup = ext4_lookup,
.link = ext4_link,
.unlink = ext4_unlink,
.symlink = ext4_symlink,
.mkdir = ext4_mkdir,
.rmdir = ext4_rmdir,
.rename = ext4_rename2,
.tmpfile = ext4_tmpfile,
};
ext4 전용 slab 캐시
| 캐시 이름 | 객체 | 용도 |
|---|---|---|
ext4_inode_cache | ext4_inode_info | inode 할당 (VFS inode 내장) |
ext4_free_data | ext4_free_data | 지연 해제 블록 정보 |
ext4_allocation_context | ext4_allocation_context | mballoc 할당 컨텍스트 |
ext4_io_end | ext4_io_end | 비동기 I/O 완료 추적 |
ext4_extent_status | extent_status | extent status tree 노드 |
ext4_pending_reservation | pending_reservation | bigalloc 예약 블록 |
/* fs/ext4/super.c - slab 캐시 초기화 */
static int __init ext4_init_fs(void)
{
/* ext4_inode_info 전용 캐시 (SLAB_RECLAIM_ACCOUNT 플래그) */
ext4_inode_cachep = kmem_cache_create_usercopy(
"ext4_inode_cache",
sizeof(struct ext4_inode_info), 0,
SLAB_RECLAIM_ACCOUNT | SLAB_ACCOUNT,
offsetof(struct ext4_inode_info, i_data),
sizeof_field(struct ext4_inode_info, i_data),
ext4_inode_init_once);
/* mballoc, extent status 등 다른 캐시도 초기화 */
ext4_init_mballoc();
ext4_init_es();
/* 파일시스템 타입 등록 */
register_filesystem(&ext4_fs_type);
return 0;
}
ftrace/bpftrace ext4 성능 분석
ext4와 JBD2는 커널에 다수의 tracepoint를 제공합니다. ftrace, perf, bpftrace를 활용하면 블록 할당, 저널 커밋, extent 매핑 등의 내부 동작을 실시간으로 관찰하고 성능 병목을 정확히 진단할 수 있습니다.
ext4 주요 Tracepoint
| Tracepoint | 위치 | 용도 |
|---|---|---|
ext4:ext4_da_write_begin | delalloc 쓰기 시작 | 지연 할당 쓰기 빈도/크기 추적 |
ext4:ext4_da_write_end | delalloc 쓰기 완료 | 실제 기록된 바이트 수 |
ext4:ext4_writepages | writeback 시작 | 페이지 플러시 빈도/범위 |
ext4:ext4_mb_new_blocks | mballoc 블록 할당 | 할당 크기, 그룹, CR 레벨 |
ext4:ext4_request_blocks | 블록 할당 요청 | 요청 크기 vs 실제 할당 비교 |
ext4:ext4_allocate_blocks | 블록 할당 완료 | 할당된 물리 블록 위치 |
ext4:ext4_ext_map_blocks_enter | extent 매핑 진입 | 논리→물리 블록 매핑 추적 |
ext4:ext4_sync_file_enter | fsync 시작 | fsync 빈도/대상 파일 |
jbd2:jbd2_commit_logging | JBD2 커밋 시작 | 저널 커밋 빈도 |
jbd2:jbd2_end_commit | JBD2 커밋 완료 | 커밋 지연 시간 측정 |
jbd2:jbd2_checkpoint | 체크포인트 시작 | 체크포인트 빈도 |
ftrace 실전 예제
# ext4 tracepoint 목록 확인
ls /sys/kernel/debug/tracing/events/ext4/
ls /sys/kernel/debug/tracing/events/jbd2/
# mballoc 블록 할당 추적
echo 1 > /sys/kernel/debug/tracing/events/ext4/ext4_request_blocks/enable
echo 1 > /sys/kernel/debug/tracing/events/ext4/ext4_allocate_blocks/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 출력 예: ext4_request_blocks: dev=sda1 ino=12345 flags=0x20 len=8 lblk=1000
# ext4_allocate_blocks: dev=sda1 ino=12345 block=50000 len=8
# fsync 지연 시간 추적
echo 1 > /sys/kernel/debug/tracing/events/ext4/ext4_sync_file_enter/enable
echo 1 > /sys/kernel/debug/tracing/events/ext4/ext4_sync_file_exit/enable
# JBD2 커밋 추적
echo 1 > /sys/kernel/debug/tracing/events/jbd2/jbd2_commit_logging/enable
echo 1 > /sys/kernel/debug/tracing/events/jbd2/jbd2_end_commit/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 출력 예: jbd2_commit_logging: dev=sda1 transaction=42 sync=0
# jbd2_end_commit: dev=sda1 transaction=42 head=100
# 추적 비활성화
echo 0 > /sys/kernel/debug/tracing/events/ext4/enable
echo 0 > /sys/kernel/debug/tracing/events/jbd2/enable
bpftrace 실전 예제
/* bpftrace: ext4 fsync 지연 시간 히스토그램 */
#!/usr/bin/env bpftrace
tracepoint:ext4:ext4_sync_file_enter
{
@start[tid] = nsecs;
@file[tid] = args->pathname;
}
tracepoint:ext4:ext4_sync_file_exit
/@start[tid]/
{
$latency_us = (nsecs - @start[tid]) / 1000;
@fsync_lat_us = hist($latency_us);
@slow_fsync[str(@file[tid])] = count();
delete(@start[tid]);
delete(@file[tid]);
}
END
{
printf("\n=== fsync 지연 분포 (us) ===\n");
print(@fsync_lat_us);
printf("\n=== 느린 fsync 파일 Top 10 ===\n");
print(@slow_fsync, 10);
}
/* bpftrace: mballoc CR 레벨별 할당 통계 */
#!/usr/bin/env bpftrace
tracepoint:ext4:ext4_mballoc_alloc
{
@cr_level[args->cr] = count();
@alloc_size = hist(args->result_len);
@group_dist[args->result_group] = count();
}
interval:s:10
{
printf("\n--- 10초간 mballoc 통계 ---\n");
printf("CR 레벨별 할당:\n");
print(@cr_level);
printf("할당 크기 분포:\n");
print(@alloc_size);
clear(@cr_level);
clear(@alloc_size);
}
/* bpftrace: JBD2 커밋 지연과 배치 크기 추적 */
#!/usr/bin/env bpftrace
tracepoint:jbd2:jbd2_commit_logging
{
@commit_start[args->transaction] = nsecs;
}
tracepoint:jbd2:jbd2_end_commit
/@commit_start[args->transaction]/
{
$lat_ms = (nsecs - @commit_start[args->transaction]) / 1000000;
@commit_lat_ms = hist($lat_ms);
delete(@commit_start[args->transaction]);
}
tracepoint:jbd2:jbd2_checkpoint
{
@checkpoint_count = count();
}
perf trace -e 'ext4:*' -e 'jbd2:*' -- dd if=/dev/zero of=/mnt/test bs=4K count=1000 명령으로 파일 쓰기 시 ext4/JBD2의 모든 tracepoint를 한 번에 관찰할 수 있습니다. 또한 perf stat -e 'ext4:ext4_mb_new_blocks'으로 특정 이벤트의 발생 횟수만 빠르게 확인할 수도 있습니다.
성능 튜닝 심화: 워크로드별 최적화
ext4의 성능은 mkfs 옵션, 마운트 옵션, 저널 모드, 커널 파라미터의 조합에 의해 결정됩니다. 이 섹션에서는 워크로드별 최적 설정과 벤치마크 비교, 그리고 실제 튜닝 절차를 상세히 다룹니다.
워크로드별 최적 설정
저널 모드별 성능 특성
| 항목 | data=journal | data=ordered (기본) | data=writeback |
|---|---|---|---|
| 저널 대상 | 메타데이터 + 데이터 | 메타데이터만 | 메타데이터만 |
| 데이터 순서 | 저널에서 보장 | 커밋 전 데이터 기록 | 순서 미보장 |
| 쓰기 증폭 | 2x (데이터 이중 기록) | 1x | 1x |
| fsync 지연 | 높음 | 중간 | 낮음 |
| 순차 쓰기 | 가장 느림 | 중간 | 가장 빠름 |
| 랜덤 쓰기 | 느림 (data 저널링) | 중간 | 빠름 |
| 크래시 안전성 | 최고 (데이터 복구 가능) | 높음 (stale 데이터 방지) | 낮음 (stale 데이터 노출 가능) |
| 권장 용도 | 회계/금융 DB | 범용 서버 | 빌드 서버, 임시 데이터 |
벤치마크 비교 가이드
# fio: 저널 모드별 랜덤 쓰기 성능 비교
# data=ordered
mount -o remount,data=ordered /mnt/test
fio --name=rw_test --directory=/mnt/test \
--rw=randwrite --bs=4k --size=1G \
--numjobs=4 --runtime=60 --time_based \
--ioengine=libaio --iodepth=32 \
--group_reporting --output=ordered.json --output-format=json
# data=writeback
mount -o remount,data=writeback /mnt/test
fio --name=rw_test --directory=/mnt/test \
--rw=randwrite --bs=4k --size=1G \
--numjobs=4 --runtime=60 --time_based \
--ioengine=libaio --iodepth=32 \
--group_reporting --output=writeback.json --output-format=json
# fsync 지연 시간 측정
fio --name=fsync_test --directory=/mnt/test \
--rw=write --bs=4k --size=100M \
--fsync=1 --ioengine=sync \
--group_reporting
# dbench (파일서버 시뮬레이션)
dbench -D /mnt/test 8 -t 60
# 결과 비교: IOPS, BW, lat(avg/p99/p999)
커널 파라미터 튜닝
| 파라미터 | 기본값 | 권장값 | 설명 |
|---|---|---|---|
vm.dirty_ratio | 20 | 10~40 | 전체 메모리 중 더티 페이지 비율 상한 (동기 쓰기 시작) |
vm.dirty_background_ratio | 10 | 5~20 | 백그라운드 writeback 시작 비율 |
vm.dirty_expire_centisecs | 3000 | 1500~6000 | 더티 페이지 만료 시간 (cs) |
vm.dirty_writeback_centisecs | 500 | 100~1000 | writeback 스레드 깨우기 간격 (cs) |
vm.vfs_cache_pressure | 100 | 50~200 | inode/dentry 캐시 회수 압력 |
# DB 서버: 더티 페이지 적게 유지 (빠른 플러시)
sysctl -w vm.dirty_ratio=5
sysctl -w vm.dirty_background_ratio=2
sysctl -w vm.dirty_expire_centisecs=1500
# 파일 서버: 더티 페이지 많이 허용 (배치 효율)
sysctl -w vm.dirty_ratio=40
sysctl -w vm.dirty_background_ratio=10
sysctl -w vm.dirty_expire_centisecs=6000
# ext4 전용 sysfs 튜닝
# 예약 블록 비율 변경 (기본 5%)
tune2fs -m 1 /dev/sda1 # 대용량 볼륨에서 공간 절약
# mballoc 통계 확인
cat /proc/fs/ext4/sda1/mb_stats
# mballoc:
# reqs: 12345 (groups_scanned: 678)
# CR 0 hits: 11000 (89.1%)
# CR 1 hits: 800 (6.5%)
# CR 2 hits: 500 (4.1%)
# CR 3 hits: 45 (0.4%)
# 실시간 모니터링
watch -n 1 "cat /sys/fs/ext4/sda1/delayed_allocation_blocks"
data=writeback과 barrier=0는 성능을 크게 높이지만 크래시 안전성을 희생합니다. 프로덕션 환경에서는 항상 barrier=1을 유지하고, 데이터 안전성이 보장되는 범위 내에서만 튜닝하세요.
관련 문서
ext4와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.