파일 시스템의 Persistence
우리가 프로그램을 사용할 때 가장 아찔한 순간은 저장하지 않았는데 갑자기 프로세스가 죽어버리는 상황일 것이다. 마찬가지로 OS에서 데이터를 저장하는 도중에 전원 플러그를 뽑으면 어떻게 될까? 메모리(RAM)에 있는 데이터가 날아가는 건 당연하지만, 하드 디스크에 쓰고 있던 데이터마저 깨져버린다면? 파일 시스템은 그 거대한 모순을 어떻게 견뎌내는 걸까.
오늘은 파일 시스템의 영속성(Persistence)을 위협하는 Crash Consistency(충돌 일관성) 문제와, 이를 해결하기 위한 고전적 방법인 FSCK, 그리고 현대적인 해결책인 Journaling(저널링)에 대해 알아보겠다.
1. 문제 상황: 3번의 쓰기와 1번의 크래시
파일 끝에 4KB 데이터를 추가하는 단순한 작업(append)을 생각해 보자. 우리는 코드 한 줄로 처리하지만, 파일 시스템 내부에서는 최소 3번의 물리적인 쓰기 작업(Write)이 필요하다.
아이노드 업데이트 (Inode Update): 파일 크기를 늘리고 새로운 데이터 블록 포인터를 추가해야 한다.
데이터 비트맵 업데이트 (Data Bitmap Update): 새로 할당된 블록을 '사용 중'으로 표시해야 한다.
데이터 블록 쓰기 (Data Block Write): 실제 사용자 데이터를 디스크에 기록해야 한다.
문제는 디스크가 이 3가지 작업을 '동시에(Atomically)' 처리할 수 없다는 점이다. 하나씩 순차적으로 기록해야 하는데, 만약 두 번째 작업까지만 하고 전원이 나가버린다면?
비트맵만 기록된 경우: 아이노드는 그 블록을 가리키지 않는데, 비트맵은 사용 중이라고 한다. -> 공간 누수 (Space Leak)
아이노드만 기록된 경우: 아이노드는 블록을 가리키는데, 그곳엔 쓰다 만 쓰레기 데이터가 있다. -> 데이터 손상 (Garbage Data)
이처럼 메타데이터와 실제 데이터 간의 불일치가 발생하는 것을 Crash Consistency Problem이라고 한다.
2. 사후 수습: FSCK (File System Checker)
초기 유닉스 시스템은 "일단 문제가 생기면 나중에 고치자"라는 방식을 택했다. 그것이 바로 fsck 도구다. 시스템이 부팅될 때 파일 시스템을 검사하여 불일치한 부분을 찾아 수정한다.
작동 방식: 슈퍼블록, 프리 블록, 아이노드 상태, 링크 수 등을 전수 조사한다. 예를 들어, 어떤 아이노드도 가리키지 않는 할당된 블록(공간 누수)을 발견하면 비트맵을 수정하여 다시 사용 가능하게 만든다.
치명적인 단점: 너무 느리다. 디스크 용량이 커질수록 검사 시간은 기하급수적으로 늘어난다. 수 테라바이트의 디스크를 검사하려면 몇 시간이 걸릴 수도 있다.
"FSCK는 훌륭한 도구지만, 현대의 대용량 스토리지 환경에서는 더 이상 유효한 전략이 아니다."
3. 사전 예방: Journaling (Write-Ahead Logging)
데이터베이스 시스템에서 아이디어를 빌려온 Journaling(저널링)은 접근 방식 자체를 바꿨다. "쓰기 전에, 무엇을 할지 먼저 적어두자(Write-Ahead Logging)."
3.1 저널링의 핵심 프로토콜
리눅스 ext3 파일 시스템의 데이터 저널링 모드를 기준으로, 데이터를 쓰는 과정은 다음과 같이 정교해진다.
Journal Write: 트랜잭션의 시작(TxB), 메타데이터, 그리고 실제 데이터를 로그(Journal) 영역에 먼저 쓴다.
Journal Commit: 로그 작성이 완료되었음을 알리는 '종결 블록(TxE)'을 쓴다. 이 단계가 성공해야만 트랜잭션이 유효한 것으로 인정된다.
Checkpoint: 이제 안심하고 실제 디스크 위치(아이노드, 비트맵, 데이터 영역)에 데이터를 덮어쓴다.
Free: 체크포인트가 끝나면, 로그 영역의 해당 트랜잭션은 더 이상 필요 없으므로 삭제(재사용) 표시를 한다. (즉, Circular data structure를 활용한다)
만약 Checkpoint 도중에 전원이 꺼지더라도, 재부팅 시 로그(Journal)만 읽어서 다시 실행(Replay)하면 된다. 전체 디스크를 뒤질 필요가 없어 복구 속도가 획기적으로 빠르다.
| Jounal (이전 리눅스 파일 시스템은 지난 시간 참고) |
3.2 원자성(Atomicity)을 위한 디테일
여기서 흥미로웠던 점은 Journal Write와 Journal Commit을 분리하는 이유다.
디스크는 내부적으로 쓰기 순서를 최적화(Scheduling)하기 때문에, 만약 한 번에 다 보내버리면 TxE(종결 블록)가 데이터보다 먼저 기록될 수 있다. 그 상태에서 크래시가 나면, 쓰레기 데이터가 유효한 트랜잭션인 것처럼 오인될 수 있다.
그래서 반드시 데이터가 로그에 안전하게 박힌 것을 확인한 뒤에야 TxE를 기록한다.
| TxB & TxE |
4. 저널링의 모드와 트레이드오프
모든 데이터를 저널링하면 너무 느리지 않을까? 그래서 ext4 같은 파일 시스템은 성능과 안정성 사이에서 타협점을 제공한다.
Data Journaling (Journal 모드): 데이터와 메타데이터를 모두 로그에 쓴다. 가장 안전하지만 가장 느리다. (데이터를 두 번 쓰는 셈이다)
Ordered Journaling (기본값): 메타데이터만 로그에 기록한다. 단, 메타데이터를 로그에 쓰기 전에 실제 데이터 블록을 먼저 디스크에 쓴다. 순서를 강제함으로써, 아이노드가 쓰레기 데이터를 가리키는 문제를 방지한다. 성능과 안전성의 균형이 가장 좋다.
Writeback Journaling: 메타데이터만 로그에 기록하고, 데이터 쓰기 순서는 신경 쓰지 않는다. 빠르지만, 크래시 발생 시 파일 내용이 깨질 위험이 있다.
마치며
Crash Consistency 문제를 공부하며 신뢰성을 만들기 위한 OS 개발자들의 노력을 확인할 수 있었다. FSCK가 무식하게 전체를 뒤지는 방식이었다면, Journaling은 '미리 기록하는 비용'을 지불하고 '빠른 복구'를 얻는 전략이다. 상황에 맞춰 어떤 모드의 저널링을 사용할지, 혹은 성능을 위해 어디까지 위험을 감수할지 판단할 수 있어야 한다.
결국 시스템 엔지니어링은 일어날 수 있는 최악의 상황(Crash)을 가정하고, 그 속에서도 정합성(Consistency)을 잃지 않으려는 치열한 고민의 산물이다.
추천글:
[운영체제] Operating System 전체 포스팅 모음집
[운영체제] 파일 시스템과 디렉토리 | Unix File System, Inode, Storage Virtualization
[운영체제] 파일 시스템 구현(VSFS) | Index Node(Inode) in deep
[운영체제] Fast File System (FFS) | 디스크의 물리적 구조를 고려한 성능 최적화