ext4 파일시스템 심화
ext4 디스크 레이아웃, Extent Tree, JBD2 저널링, mballoc, 지연 할당, fscrypt, fsverity, 성능 튜닝 종합 가이드.
개요 & 역사
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
핵심 커널 자료구조
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으로 설정할 수 있습니다.