[CS300 #117] 저널링 파일 시스템 — 정전에도 구조가 깨지지 않는 이유
컴퓨터공학 300 주제 시리즈의 117번째 글이다. 전체 지도는 여기.
한 줄 요약
저널링은 여러 블록에 걸친 파일 시스템 갱신을 먼저 별도 로그(저널)에 기록하고 커밋 표시를 남긴 뒤에야 제자리에 반영하는 방식으로, 정전이 언제 일어나도 “완전히 적용” 또는 “전혀 안 적용” 둘 중 하나로 복구되게 만드는 기법이다.
왜 필요한가
파일 끝에 블록 하나를 덧붙이는 단순한 작업도 디스크의 세 곳을 바꿔야 한다.
- 새 데이터 블록에 내용을 쓴다.
- 데이터 비트맵에서 그 블록을 “사용 중”으로 표시한다.
- 아이노드의 크기와 블록 목록을 갱신한다.
디스크는 이 세 쓰기를 한 번에 할 수 없고, 성능을 위해 순서를 바꾸기도 한다. 중간에 전원이 나가면 이상한 상태가 남는다. 아이노드는 블록을 쓴다고 하는데 비트맵은 비어 있다고 하면, 나중에 그 블록이 다른 파일에 또 할당되어 두 파일이 서로의 내용을 덮는다.
저널링 이전에는 재부팅할 때마다 fsck 가 파일 시스템 전체를 훑어 이런 불일치를 찾아 고쳤다. 디스크가 커질수록 이 검사는 몇 시간까지 걸렸다. 저널링은 “최근에 하던 일만” 확인하면 되게 해서 복구 시간을 디스크 크기와 무관하게 만든다.
핵심 개념
크래시 일관성 문제
세 블록 중 일부만 디스크에 닿았을 때의 결과다.
| 디스크에 닿은 쓰기 | 결과 |
|---|---|
| 데이터만 | 문제없음. 아무도 가리키지 않는 블록에 쓴 것뿐(작업은 사라짐) |
| 비트맵만 | 공간 누수. 사용 중으로 표시됐지만 아무도 안 쓴다 |
| 아이노드만 | 아이노드는 블록을 가리키지만 비트맵은 빈칸 → 이중 할당 위험, 내용은 쓰레기 |
| 아이노드 + 비트맵 | 메타데이터는 일관적이지만 파일에 쓰레기 내용 |
| 아이노드 + 데이터 | 비트맵 불일치 |
| 비트맵 + 데이터 | 비트맵 불일치(누수) |
쓰기 전 로그(write-ahead logging)
데이터베이스의 WAL 과 같은 아이디어다.
1. 저널 기록 (journal write)
[TxB | 아이노드 새 내용 | 비트맵 새 내용 | 데이터 새 내용] -> 저널 영역에 순차 기록
2. 저널 커밋 (journal commit)
[TxE] 커밋 블록 하나를 쓴다. 이 블록이 디스크에 닿는 순간 트랜잭션이 "확정"
3. 체크포인트 (checkpoint)
아이노드, 비트맵, 데이터를 각자 제자리에 쓴다
4. 해제
체크포인트가 끝난 트랜잭션의 저널 공간을 재사용 가능으로 표시
재부팅 후 복구 규칙은 단순하다.
- 저널에 커밋 블록까지 있는 트랜잭션은 다시 재생(replay)한다. 이미 체크포인트를 일부 했어도 같은 블록 내용을 다시 쓰는 것이라 안전하다(멱등).
- 커밋 블록이 없는 트랜잭션은 버린다. 제자리에는 아직 아무것도 쓰지 않았으므로 옛 상태 그대로다.
커밋 블록을 따로 쓰는 이유는 2단계를 1단계와 한 번에 쓰면 디스크가 순서를 바꿔 커밋 표시가 먼저 닿을 수 있기 때문이다. 커밋 전에 저널 내용이 디스크에 확실히 닿도록 쓰기 배리어(캐시 플러시)를 사이에 둔다. ext4 의 journal_async_commit 옵션은 커밋 블록에 체크섬을 넣고 이 대기 없이 커밋 블록을 쓰게 한다.
무엇을 저널에 넣나: ext4 의 세 모드
데이터까지 저널에 쓰면 모든 데이터를 두 번 쓰게 된다. 그래서 대부분은 메타데이터만 저널링한다. 리눅스 커널의 ext4 문서는 세 가지 모드를 설명한다.
| 모드 | 동작 | 특징 |
|---|---|---|
data=journal |
데이터와 메타데이터를 모두 저널에 쓴 뒤 제자리에 쓴다 | 가장 안전, 쓰기 두 배 |
data=ordered (기본) |
데이터는 저널에 넣지 않지만, 메타데이터를 커밋하기 전에 데이터를 제자리에 먼저 써 둔다 | 아이노드가 쓰레기 블록을 가리키는 일이 없다 |
data=writeback |
데이터 순서를 보장하지 않는다 | 빠르지만, 크래시 후 최근 파일에 옛 내용이 드러날 수 있다 |
ordered 모드의 순서는 “데이터 → 메타데이터 저널 → 커밋 → 메타데이터 체크포인트”다. 데이터가 가리켜지기 전에 먼저 디스크에 있게 해서, 위 표의 “쓰레기 내용” 문제를 막는다.
ext4 문서에 따르면 commit= 옵션의 기본값은 5초다. 즉 전원이 나가면 최대 몇 초 분량의 최근 변경이 사라질 수 있다. 일관성은 지키지만 최신성까지 지키는 것은 아니다. 최신성이 필요하면 애플리케이션이 fsync 를 불러야 한다.
다른 접근
- 소프트 업데이트: 쓰기 순서를 엄격히 조정해 어떤 순간에도 불일치가 무해하도록 한다(FreeBSD UFS).
- Copy-on-Write 파일 시스템: 블록을 제자리에 덮어쓰지 않고 새 위치에 쓴 뒤 루트 포인터 하나를 원자적으로 바꾼다(btrfs, ZFS). 스냅숏이 거의 공짜가 된다.
- 로그 구조 파일 시스템: 모든 쓰기를 로그처럼 순차로 붙인다. SSD 친화적인 F2FS 가 이 계열이다.
직접 해 보기
파일 덧붙이기에 필요한 세 블록 쓰기를 흉내 내고, 정전 시점을 바꿔 가며 결과를 본다. 저널 없이는 디스크가 순서를 바꿀 수 있으므로 모든 순서를 시험한다.
from itertools import permutations
# 파일에 블록 하나를 덧붙이려면 디스크의 세 블록을 함께 바꿔야 한다.
# 각 쓰기는 "블록 전체를 새 내용으로 덮어쓰기"(물리 저널링처럼 멱등)로 표현한다.
def fresh_disk():
return {"bitmap": "블록5=빈칸", "inode": "크기1,[4]", "data5": "쓰레기"}
NEW = {"bitmap": "블록5=사용", "inode": "크기2,[4,5]", "data5": "B"}
def state(d):
ino, bmp, dat = d["inode"] == NEW["inode"], d["bitmap"] == NEW["bitmap"], d["data5"] == "B"
if ino == bmp and (not ino or dat): return "정상" + (" (새 상태)" if ino else " (옛 상태)")
if ino and not bmp: return "불일치: 아이노드는 블록5를 쓰는데 비트맵은 빈칸 -> 이중 할당 위험"
if bmp and not ino: return "불일치: 비트맵은 사용 중인데 아무도 안 씀 -> 공간 누수"
return "불일치: 아이노드·비트맵은 맞지만 데이터가 쓰레기 -> 파일에 엉뚱한 내용"
print("[저널 없음] 디스크는 쓰기 순서를 바꿀 수 있으므로 모든 순서 x 정전 시점을 본다")
seen = set()
for order in permutations(NEW):
for k in (1, 2): # 1개 또는 2개만 디스크에 닿고 정전
d = fresh_disk()
for blk in order[:k]: d[blk] = NEW[blk]
seen.add(state(d))
for s in sorted(seen): print(" ", s)
print("[저널 있음] 저널 기록 -> 커밋 -> 체크포인트, 어느 단계에서 정전되든")
steps = [("log", b) for b in NEW] + [("commit", None)] + [("ckpt", b) for b in NEW]
for cp in range(len(steps) + 1):
d, journal, committed = fresh_disk(), {}, False
for kind, b in steps[:cp]: # cp 단계까지만 디스크에 닿았다
if kind == "log": journal[b] = NEW[b]
elif kind == "commit": committed = True
else: d[b] = journal[b]
if committed: # 복구: 커밋된 트랜잭션은 다시 재생(멱등)
for b, v in journal.items(): d[b] = v
print(f" {cp}단계 후 정전 -> 커밋 {str(committed):5s} -> {state(d)}")
[저널 없음] 디스크는 쓰기 순서를 바꿀 수 있으므로 모든 순서 x 정전 시점을 본다
불일치: 비트맵은 사용 중인데 아무도 안 씀 -> 공간 누수
불일치: 아이노드·비트맵은 맞지만 데이터가 쓰레기 -> 파일에 엉뚱한 내용
불일치: 아이노드는 블록5를 쓰는데 비트맵은 빈칸 -> 이중 할당 위험
정상 (옛 상태)
[저널 있음] 저널 기록 -> 커밋 -> 체크포인트, 어느 단계에서 정전되든
0단계 후 정전 -> 커밋 False -> 정상 (옛 상태)
1단계 후 정전 -> 커밋 False -> 정상 (옛 상태)
2단계 후 정전 -> 커밋 False -> 정상 (옛 상태)
3단계 후 정전 -> 커밋 False -> 정상 (옛 상태)
4단계 후 정전 -> 커밋 True -> 정상 (새 상태)
5단계 후 정전 -> 커밋 True -> 정상 (새 상태)
6단계 후 정전 -> 커밋 True -> 정상 (새 상태)
7단계 후 정전 -> 커밋 True -> 정상 (새 상태)
저널이 없으면 세 종류의 불일치가 모두 나타난다. 저널이 있으면 경계는 정확히 커밋 블록(4단계)이다. 그 전에 정전되면 옛 상태, 그 뒤면 새 상태이고, 중간 상태는 존재하지 않는다. 체크포인트가 반쯤 진행된 5~6단계에서도 재생이 나머지를 채운다.
현업에서는
- 저널은 메타데이터를 지킨다, 내 데이터는 내가 지킨다: 파일 시스템이 일관적이라고 애플리케이션 데이터가 안전한 것은 아니다. DB 는 자체 WAL 과
fsync로 내구성을 보장한다. PostgreSQL 문서가fsync설정을 끄면 정전이나 크래시 때 복구 불가능한 손상이 날 수 있다고 경고하는 이유다. - 안전한 파일 교체: “임시 파일 쓰기 →
fsync→rename” 패턴과, ext4 ordered 모드가 덮어쓰기용rename에 대해 새 파일 데이터 블록을 먼저 내보내는 동작은 커널 ext4 문서에 함께 설명되어 있다. 그래도 이식성 있는 코드는 명시적fsync에 의존한다. - etcd 와 fsync 지연: etcd 는 모든 쓰기를 WAL 에
fsync한다. 디스크의 fsync 지연이 길면 리더 선출 실패와 API 서버 지연으로 번진다. 쿠버네티스 컨트롤 플레인 노드에 빠른 SSD 를 권하는 이유이고, etcd 메트릭의 WAL fsync 지연 히스토그램이 첫 번째 확인 지점이다. - 마운트 후 메시지: 비정상 종료 뒤 부팅하면 커널 로그에 ext4 가 저널을 복구했다는 메시지가 남는다. 몇 초 만에 끝나는 이것이 저널링이
fsck전체 검사를 대신한 순간이다.
확인 문제
- 파일에 블록 하나를 덧붙일 때 바뀌어야 하는 디스크 구조 세 가지는?
- 저널링에서 커밋 블록을 저널 내용과 따로, 그리고 나중에 쓰는 이유는?
- 복구 시 커밋된 트랜잭션을 재생해도 안전한 이유는?
- ext4 의
data=ordered모드가data=writeback보다 안전한 점은 무엇인가? - 저널링 파일 시스템을 쓰는데도 애플리케이션이
fsync를 불러야 하는 이유는?
풀이
- 데이터 블록, 데이터 비트맵, 아이노드.
- 함께 쓰면 디스크가 순서를 바꿔 커밋 표시가 먼저 닿을 수 있고, 그 상태로 정전되면 내용이 불완전한 트랜잭션을 확정된 것으로 오인해 재생하게 되기 때문이다.
- 저널에는 블록의 최종 내용이 들어 있어서, 같은 내용을 몇 번 다시 써도 결과가 같기(멱등) 때문이다.
- 메타데이터를 커밋하기 전에 데이터를 먼저 제자리에 써 두므로, 크래시 후 아이노드가 아직 쓰이지 않은 블록(옛 쓰레기 내용)을 가리키는 일이 없다.
- 저널링은 파일 시스템 구조의 일관성을 보장할 뿐, 최근 쓰기가 디스크에 도달했음을 보장하지 않는다. 커밋 주기 사이의 변경은 정전 시 사라질 수 있다.
더 읽을거리 (References)
- OSTEP, Crash Consistency: FSCK and Journaling (PDF)
- Linux kernel documentation, ext4 General Information — 저널 모드와
commit= - Linux kernel documentation, ext4 Journal (jbd2)
- PostgreSQL 문서, Write-Ahead Logging (WAL)