
저널링 파일 시스템: 안전한 쓰기
파일 복사 중에 코드가 뽑히면 어떻게 될까? 데이터 깨짐을 막기 위한 OS의 로그 기록 습관.

파일 복사 중에 코드가 뽑히면 어떻게 될까? 데이터 깨짐을 막기 위한 OS의 로그 기록 습관.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

물리 서버 환경에서는 예상치 못한 정전이 치명적이다. 데이터센터 사례를 찾아보면 공통된 패턴이 있다. UPS 배터리도 다 떨어지면서 서버가 그냥 꺼지고, 재부팅하니 파일 시스템이 깨져서 부팅이 안 되는 것이다. 백업은 있어도, 정전 직전까지 진행하던 작업 데이터는 날아간다.
그 이유가 궁금해졌다. 파일을 저장하는 건 그냥 디스크에 쓰기만 하면 되는 거 아닌가? 왜 정전이 되면 파일이 깨질까? 알고 보니, 파일 하나를 저장하는 것도 여러 단계를 거치고, 그 중간에 문제가 생기면 일관성이 깨진다는 것이었다. 이걸 해결하기 위해 현대 파일 시스템들이 사용하는 게 바로 저널링(Journaling)이었습니다.
저널링 파일 시스템은 데이터베이스의 WAL(Write-Ahead Logging) 개념을 파일 시스템에 가져온 겁니다. 실제 작업을 하기 전에 "이런 작업을 할 거야"라고 로그를 먼저 남기는 거죠. 마치 중요한 계약서에 서명하기 전에 복사본을 만들어두는 것과 비슷합니다.
처음에 저는 파일 저장이 단순한 작업이라고 생각했습니다. write() 시스템 콜 한 번이면 끝이라고요. 하지만 실제로는 전혀 그렇지 않았습니다.
파일 하나를 생성하고 데이터를 쓰는 과정은 대략 이렇습니다:
이 과정은 여러 디스크 쓰기 작업으로 나뉘어 있고, 각 쓰기는 독립적으로 일어납니다. 디스크는 섹터 단위로 쓰기가 원자적이지만, 파일 생성은 여러 섹터를 건드리니까요. 만약 3번 단계 중간에 전원이 나가면? 디렉토리는 새 파일을 가리키는데, inode는 잘못된 데이터 블록을 가리키고, 블록 비트맵은 업데이트가 안 돼서 서로 모순되는 상태가 됩니다.
옛날 파일 시스템(ext2, FAT32)은 이 문제를 해결하는 방법이 하나밖에 없었습니다. 바로 fsck(File System Check)를 실행하는 겁니다. 이건 디스크 전체를 읽어가면서 inode, 블록 비트맵, 디렉토리 구조의 일관성을 검사하고 수정하는 도구입니다. 문제는 이게 엄청 느리다는 거죠. 1TB 디스크면 부팅할 때마다 몇 시간씩 걸릴 수도 있습니다.
# ext2 파일 시스템에 fsck 실행 (부팅 시 자동 실행)
# 디스크 크기에 비례해서 시간이 오래 걸림
fsck.ext2 /dev/sda1
저는 이 문제를 처음 겪었을 때 좌절했습니다. 파일 시스템이 이렇게 취약한 구조였다니. 그런데 ext4나 NTFS 같은 현대 파일 시스템은 부팅이 몇 초 안에 됩니다. 어떻게 이게 가능할까요?
어느 날 데이터베이스 책을 읽다가 WAL(Write-Ahead Logging)이라는 개념을 봤습니다. 데이터베이스가 트랜잭션을 처리할 때, 실제 데이터를 바꾸기 전에 "무엇을 어떻게 바꿀지"를 로그에 먼저 기록한다는 겁니다. 그러면 중간에 크래시가 나도 로그를 읽어서 복구할 수 있죠.
파일 시스템도 똑같은 원리를 쓰면 되는 거였습니다. 파일 쓰기 작업을 하기 전에, 그 작업 내용을 저널(Journal) 영역에 먼저 기록하는 겁니다. 저널은 디스크의 특별한 영역으로, 마치 일기장처럼 "앞으로 할 작업"을 순서대로 적어둡니다.
저널링의 핵심 아이디어는 이겁니다:
만약 3번 단계까지 완료하고 4번 도중에 정전이 나면? 재부팅할 때 파일 시스템은 저널을 읽어봅니다. "아, 트랜잭션 #1234가 커밋됐는데 실제 적용은 안 됐네?" 하고 저널에 있는 내용을 다시 실행(Redo)합니다. 만약 2번 단계 중간에 죽었다면? 커밋 레코드가 없으니까 그냥 무시(Undo)하면 됩니다.
이 방식의 천재성은 복구 시간이 디스크 크기와 무관하다는 겁니다. fsck는 전체 디스크를 스캔하지만, 저널링은 저널 영역(보통 수백 MB)만 읽으면 되니까 몇 초면 끝납니다.
# ext4 파일 시스템의 저널 정보 확인
sudo dumpe2fs /dev/sda1 | grep -i journal
# 출력 예:
# Journal inode: 8
# Journal backup: inode blocks
# Journal size: 128M
저널링 파일 시스템을 더 깊이 파고들면서, 저는 여러 가지 변형이 있다는 걸 배웠습니다. ext4는 세 가지 저널링 모드를 지원합니다:
메타데이터뿐만 아니라 실제 데이터까지 저널에 기록합니다. 데이터를 두 번 쓰는 셈이죠 (저널에 한 번, 실제 위치에 한 번). 가장 안전하지만 성능이 떨어집니다.
# ext4를 journal 모드로 마운트
sudo mount -o data=journal /dev/sda1 /mnt
메타데이터만 저널에 기록하고, 데이터는 저널 없이 직접 씁니다. 단, 데이터를 먼저 쓰고 나서 메타데이터를 저널에 기록합니다. 이렇게 하면 메타데이터가 가리키는 블록에는 항상 올바른 데이터가 있습니다. 크래시 후 메타데이터가 복구되면, 파일 내용도 일관성이 보장됩니다.
# ext4를 ordered 모드로 마운트 (기본값)
sudo mount -o data=ordered /dev/sda1 /mnt
메타데이터만 저널에 기록하고, 데이터는 순서 보장 없이 씁니다. 메타데이터보다 데이터가 나중에 써질 수도 있어서, 크래시 후 메타데이터는 복구됐는데 파일 내용은 쓰레기 데이터일 수 있습니다. 성능은 가장 좋지만 안전성은 떨어집니다.
# ext4를 writeback 모드로 마운트
sudo mount -o data=writeback /dev/sda1 /mnt
저널은 순환 버퍼처럼 동작합니다. 저널 영역이 가득 차면 가장 오래된 엔트리를 덮어씁니다 (이미 체크포인트가 완료된 것들만). 각 저널 엔트리는 이런 구조를 가집니다:
// 저널 트랜잭션의 구조 (간단화한 버전)
struct journal_transaction {
uint32_t transaction_id; // 트랜잭션 고유 번호
uint32_t sequence_num; // 저널 내 순서 번호
uint32_t num_blocks; // 이 트랜잭션이 수정할 블록 개수
block_update blocks[]; // 실제 변경 사항들
uint32_t commit_record; // 커밋 완료 마커 (있으면 완료)
};
struct block_update {
uint32_t block_number; // 수정할 블록 번호
char data[4096]; // 새로운 블록 내용 (전체 복사)
};
NTFS도 비슷한 저널링을 사용하는데, $LogFile이라는 특수 파일에 로그를 기록합니다. NTFS는 항상 메타데이터 저널링만 하고, 데이터 저널링은 지원하지 않습니다.
저널링에는 두 가지 복구 전략이 있습니다:
대부분의 저널링 파일 시스템은 Redo 로깅을 사용합니다. 왜냐하면 커밋 레코드가 있는지만 확인하면 되니까 단순하거든요.
ZFS나 Btrfs 같은 최신 파일 시스템은 저널링 대신 Copy-on-Write(COW)를 사용합니다. 데이터를 제자리에서 수정하는 게 아니라, 새로운 위치에 복사본을 쓰고, 포인터를 원자적으로 바꾸는 겁니다. 이렇게 하면 저널이 필요 없습니다. 포인터가 바뀌기 전까지는 항상 이전 버전이 유효하니까요.
# ZFS 파일 시스템 생성 (COW 방식)
zpool create mypool /dev/sdb
zfs create mypool/data
COW는 스냅샷과 복제가 공짜로 되는 장점이 있지만, 단편화가 심해질 수 있다는 단점도 있습니다.
저는 이제 프로덕션 서버를 세팅할 때 항상 저널링을 고려합니다. 대부분의 리눅스 배포판은 ext4를 ordered 모드로 사용하지만, 데이터베이스 서버 같은 critical한 용도라면 journal 모드도 고려합니다.
데이터베이스는 자체적으로 WAL을 가지고 있어서, 파일 시스템 저널링과 이중으로 로깅하게 됩니다. 약간의 성능 손실이 있지만, 안전성이 최우선이면 이게 맞습니다. PostgreSQL의 pg_wal 디렉토리나 MySQL의 ib_logfile이 바로 데이터베이스의 WAL입니다.
# PostgreSQL의 WAL 디렉토리 확인
ls -lh /var/lib/postgresql/14/main/pg_wal/
# 여기에 16MB 단위의 WAL 세그먼트 파일들이 있음
# MySQL의 redo log 확인
ls -lh /var/lib/mysql/ib_logfile*
클라우드 환경에서는 EBS 같은 블록 스토리지도 내부적으로 저널링을 사용합니다. 그래서 현대 시스템은 여러 레이어에서 저널링이 중첩되어 있는 셈이죠.
fsck와 저널 복구의 속도 차이는 실제로 엄청납니다. 1TB ext2 디스크가 깨진 경우 fsck로 6시간이 걸린다는 사례가 있다. ext4로 바꾸면 크래시 후 부팅이 10초면 끝난다.
저널링 파일 시스템은 제가 배운 시스템 설계 원칙 중 하나입니다. "중요한 작업을 하기 전에 일기를 쓴다." 이 단순한 아이디어가 파일 시스템의 안정성을 혁명적으로 바꿨습니다.
창업자로서 배운 교훈은, 시스템의 복잡도를 늘리더라도 안정성이 중요하다는 겁니다. 저널을 쓰면 디스크 쓰기가 약간 느려지지만, 크래시 복구 시간은 몇 시간에서 몇 초로 줄어듭니다. 이건 사업 연속성 측면에서 엄청난 가치죠.
지금은 클라우드를 주로 쓰지만, 저널링 개념은 여전히 중요합니다. 데이터베이스 설계, 분산 시스템 로깅, 심지어 애플리케이션 레벨에서도 "작업 전에 로그를 남기고, 실패하면 로그를 보고 복구한다"는 패턴은 계속 쓰입니다. 파일 시스템에서 배운 이 원칙이 제 시스템 설계 철학의 기초가 됐습니다.