[OSTEP][영속성] CH42 크래시 일관성 - FSCK와 저널링
발표 날짜 2024.12.04 WED 10시a.m.
CH42 크래시 일관성 - FSCK 와 저널링
파일 시스템은 그 기본 개념들을 구현하는 데 필요한 파일, 디렉터리, 각종 메타데이터들을 관리한다
파일 시스템의 자료 구조는 장시간 사용 후에도 유지되어야 하며 전력 손실에도 손상 없이 안전하게 저장되어야 한다
전력 손실이나 크래시가 발생할 때 파일 시스템은 크래시 일관성 Crash-consistency 이라는 문제에 직면하게 된다
디스크는 한번에 하나의 요청만 처리할 수 있기 때문에 두 개의 자료 구조를 갱신하려고 할 때 하나의 쓰기 작업만 완료한 상태에서 시스템 전원이 나간 경우, 디스크 상의 자료 구조는 일관성이 깨지게 Inconsistent 된다
핵심 질문 - 크래시에도 불구하고 디스크 갱신하기
두 쓰기 동작 사이에서 시스템은 크래시되거나 전력이 끊어질 수도 있기 때문에 디스크 상의 상태는 부분적으로만 갱신이 될 수 있다 크래시 이후에 시스템이 재구동되면 파일 접근 등의 동작을 위해서 파일 시스템을 다시 마운트하려고 할 것이다, 임의의 시간에 크래시가 발생할 수 있다고 하면 파일 시스템이 어떻게 해야 디스크 상의 자료를 올바른 상태로 유지할 수 있을까?
🐣 무조건 갱신해야 할 필요는 없고 원자성을 유지하면 된다 🐣
42.1 예제
저널링을 이해하기 위해 디스크 상에서 여러 개의 자료 구조를 갱신하는 연산을 예로 든다
워크로드는 기존 파일에 블럭을 하나 추가하는 연산이고, 아이노드 2번이 할당되어 있고 4번째 데이터 블럭을 사용하고 있다
파일의 끝에 내용을 추가한다는 것은 새로운 데이터 블럭을 추가하는 것이고, 이 과정에서 세 개의 디스크 자료를 갱신해야 한다
1) 아이노드 (버전 2 또는 I[v2]) 2) 새로운 데이터 블럭 Db 3) 데이터 비트맵 B[v2]
write() 의 결과는 디스크에 즉시 반영되지 않고, 일정 기간 동안 페이지 캐시나 버퍼 캐시에 존재하다가
파일 시스템이 실제로 디스크를 실행할 때 기록된다, 그 과정에서 크래시가 발생하면 디스크 기록 과정이 엉망이 될 수 있다
세 개의 작업 중 하나 또는 두 개만 작업이 성공한 경우 일관성 문제가 발생할 수도, 안 할 수도 있다 (예제는 책 P576 참고)
크래시 일관성 문제
크래시 때문에 파일 시스템 자료 구조 간의 불일치, 공간 누수 발생, 의미 없는 데이터 전달 등 여러 문제가 발생할 수 있다
연산 이전의 상태에서 연산 이후의 상태로 이동할 때, 중간의 임시 상태 (Transient State) 로 파일 시스템이 남아 있는 경우를 크래시 일관성 문제 또는 일관된 갱신 문제라고 부른다
42.2 해법 1 - 파일 시스템 검사기
초기의 파일 시스템은 일관성이 없더라도 그대로 두었다가 리부팅 시에 일관성 문제를 해결하는 간단한 방법을 사용하였다
fsck 라고 하는 이 UNIX 도구로 일관성 불일치를 발견하고 수정할 수 있다
fsck 는 파일 시스템이 마운트 직전에 실행되고 fsck 실행 시에는 파일 시스템이 어떤 동작도 실행하지 않는다
fsck 는 아래와 같은 작업을 진행한다
* 슈퍼블럭 - 슈퍼블럭 내용에 오류가 없는지 검사한다, 손상이 의심되는 슈퍼블럭을 발견하면 사본으로 대체할지를 결정한다
* 프리 블럭 - 아이노드, 간접 블럭, 이중 간접 블럭들이 할당되었는지에 대한 정보를 생성하고 얻은 정보를 토대로 할당 비트맵을 재구성한다
* 아이노드 상태 - 아이노드가 손상되었는지 다른 문제는 없는지 검사하고, 의심 대상이 있으면 아이노드 초기화 및 아이노드 비트맵도 갱신한다
* 아이노드 링크 - 각 할당된 아이노드의 링크 개수를 확인한다 (링크의 개수는 특정 파일에 대한 참조를 포함하고 있는 디렉터리들의 수)
* 중복 - 같은 블럭을 가리키는 서로 다른 아이노드가 있는지를 검사한다
* 배드 블럭 - 모든 포인터 목록을 검사하면서 유효하지 않는 공간을 참조하는 "배드" 블럭 포인터들을 검사한다
* 디렉터리 검사 - 디렉터리의 각 아이노드가 실제 할당되어 있는지, 디렉터리가 두 번 이상 연결된 경우가 없는지를 검사한다
fsck 는 너무 느리다, 약간의 작업 때문에 생긴 문제를 해결하기 위해 디스크 전체를 다 읽어본다는 것은 엄청난 비용이다
42.3 해법 2 - 저널링 or Write-Ahead Logging
WAL 또는 저널링 Journaling 이라고 부르는 Write-Ahead Logging 은 , 많은 시스템들이 이 개념을 사용하고 있다
디스크 내용을 갱신할 때, 해당 자료 구조를 갱신하기 전에, 먼저 수행하고자 하는 작업을 요약해서 기록해두고
해당 디스크 페이지들을 새값으로 갱신하는 과정에서 크래시가 발생하면, 로그를 확인하여 다시 갱신한다, 이를 redo 라고 한다
데이터 저널링
트랙잭션의 시작 블럭 TxB 은 갱신될 블럭들에 대한 정보와 트랙잭션 식별자 TID 등 연산에 대한 정보들을 기록한다
갱신해야 할 물리적 내용을 저널에 기록하기 때문에 물리 로깅이라고 부른다 (갱신에 명령어 자체를 저장하는 논리 로깅도 있다)
트랜잭션 종료 블럭 TxE 는 트랙잭션의 종료를 알리며 TID 를 포함하고, TxE 가 로그에 기록되면 트랜잭션은 Commit 되었다고 한다
저널에 기록된 내용을 실제 위치에 반영하는 과정을 체크포인팅이라고 하며, 파일 시스템 체크포인트 시에 갱신된 메타데이터와 데이터 블럭들을 해당 위치에 반영한다
저널에 기록하는 도중 크래시가 발생하면 일이 복잡해지기 때문에, 여러 개의 요청을 한꺼번에 전송해서 순차 쓰기를 하는 방법도 있겠지만, 디스크가 스케줄링을 통해 이들을 작은 단위로 나누어 기록할 경우 파일 시스템이 의도했던 순서와 완전히 달라질 수 있는 문제가 생긴다
데이터가 잘못되어도 잘못된 블럭인지 아닌지 알 수 있는 문제를 방지하기 위해서, 파일 시스템은 트랜잭션을 두 단계로 기록할 수 있다
1) TxE 를 제외한 모든 블럭을 한 번의 쓰기 요청으로 저널에 쓴다
2) 이 쓰기가 완료되면 TxE 블럭에 대한 쓰기를 요청하여 저널을 최종적이고 안전한 상태로 만든다
여기서 제일 중요한 점은 TxE 는 무조건 원자적으로 기록되어야 한다는 것이다
저널링 과정을 다시 정리하면 아래와 같다
1. 저널 쓰기 - 트랜잭션의 내용을 로그에 쓴다 (TxB, 메타데이터, 데이터) 그리고 이 쓰기가 완료되길 기다린다
2. 저널 커밋 - TxE 를 포함한 트랙잭션 커밋 블럭을 로그에 쓴다, 그리고 트랜잭션은 커밋됨이라고 한다
3. 체크포인트 - 갱신 (메타데이터와 데이터) 한 내용을 디스크 상의 최종 위치에 쓴다
여담 - 로그 쓰기의 최적화
트랜잭션 시작 블럭을 쓰고, 트랙잭션의 내용을 다 쓴 후에 트랜잭션 끝 블럭을 쓰는 동작은 한 번의 추가 회전이 발생한다
이것을 해결하기 위해, 트랙잭션의 시작과 끝 블럭에 저널 내용에 대한 체크섬을 포함하도록 하여 한 번에 쓸 수 있는 방법이 나왔다
이 방법으로 파일 시스템이 전체 트랜잭션을 기다리지 않고 한 번에 쓸 수 있고, 복구 시에 체크섬을 비교하여 트랜잭션 기록 중에 크래시가 발생하였는지 검사할 수 있게 되었다, 또한 빠른 성능을 얻고, 저널의 내용을 읽을 때도 체크섬으로 보호를 받기 때문에 파일 시스템은 신뢰성을 가지게 되었다
복구
트랜잭션이 로그에 기록되기 전에 크래시가 발생한다면, 복구 시에 아무것도 안 하면 된다
🐣 로그에 기록되지 않은 작업은 시작되지 않은 것으로 간주되기 때문에 복구 과정에서 처리할 필요가 없다 🐣
트랜잭션이 로그에 기록되었지만 체크포인트가 완료되기 전에 크래시가 발생한다면, 시스템이 부팅할 때 로그를 탐색해서 디스크에 커밋된 트랜잭션이 있는지 파악한다, 그리고 커밋된 트랜잭션의 블럭들을 디스크 상의 원래 위치에 쓰는데 이 과정을 재생 Replayed 또는 Redo Logging 이라고 한다
🐣 로그에 기록된 트랜잭션은 commit 여부에 따라 복구 과정을 결정한다, 시스템 부팅 시 로그를 탐색해서 커밋된 트랜잭션을 찾고,
커밋되지 않은 트랜잭션은 무시한다, 커밋된 트랜잭션이 있다면 디스크에 반영하여 일관성을 복구한다 🐣
체크포인트 중에는 어느 시점에서든 크래시가 발생해도 문제가 없다
최악의 경우라고 해봐야 복구 시에 해당 갱신 작업을 다시 수행하는 것 뿐이다
🐣 체크포인팅은 이미 저널에 안전하게 기록된 데이터를 디스크로 옮기는 작업이기 때문에 이 과정에서 크래시가 발생해도, 로그에는 여전히 데이터가 안전하게 남아 있어서 복구 시 다시 재생 Replayed 또는 Redo 하면 된다
로그 기록을 일괄 처리 방식으로
데이터 저널링 방식은 디스크에 엄청나게 많은 트래픽을 유발한다, 파일을 하나 생성하려면 디스크 상의 여러 자료 구조를 갱신해야 한다
아이노드 비트맵 (새로운 아이노드 할당), 생성된 파일의 아이노드, 새로운 디렉터리 항목을 포함하는 부모 디렉터리의 데이터 블럭 등 -
같은 디렉터리 내에 두 파일을 생성했다면, 파일 아이노드들도 같은 아이노드 블럭에 존재할 가능성이 크다
이런 상황을 개선하기 위해서 Linux ex3 을 비롯한 어떤 파일 시스템은 여러 개의 저널 로그를 모아서 한 번에 디스크에 커밋한다
로깅해야 할 모든 파일 시스템 갱신 내용을 트랜잭션 버퍼 또는 저널 버퍼라는 자료 구조에 보관하고, 트랜잭션 버퍼의 내용들은 파일 시스템이 저널을 커밋할 때 디스크에 기록된다
파일 시스템은 메모리에 있는 갱신 내용들을 정기적으로 (저널 타임아웃) 또는 fsync() 나 sync() 함수가 호출되면, 트랜잭션 버퍼의 내용들을 저널에 기록한다
🐣 1. 트랜잭션 버퍼 - 파일 시스템 갱신 내용 (메타데이터와 관련 데이터) 을 메모리 상의 트랜잭션 버퍼 또는 저널 버퍼에 임시 저장
2. 커밋 - 저널 버퍼의 내용을 디스크의 저널 영역에 기록, 저널 영역에 기록이 완료된 이후, 실제 데이터 블럭과 메타데이터를 디스크의 파일 시스템 데이터 영역에 적재
3. 커밋 시점 - 저널 타임아웃 (일정 시간 간격) 마다 또는 fsync() 나 sync() 함수 호출 시 트랜잭션 버퍼의 내용을 디스크에 기록 🐣
저널에 기록하는 블럭들을 버퍼링함으로써 많은 양의 쓰기 트래픽을 줄일 수 있다
로그 공간의 관리
트랜잭션 버퍼, 로그 공간이 가득 차면 1) 로그가 커질수록 복구 소요 시간이 길어지고 2) 더 이상의 트랜잭션을 커밋할 수 없게 된다
이 문제를 해결하기 위해서 저널링 파일 시스템은 로그 형식으로 환형 자료 구조를 사용한다, 로그 영역을 끝까지 다 쓰면 앞에서부터 다시 쓰기 때문에 저널을 환형 로그 Circular Log 라고도 부른다
저널 슈퍼블럭은 (파일 시스템의 슈퍼블럭 아님) 체크포인트가 안 된 트랜잭션들을 구분할 수 있을 만큼의 충분한 정보를 가지고 있어서, 이 정보를 통해 복구 시간을 단축시키고, 로그를 재사용한다
1. 저널 쓰기 - TxB 와 갱신에 대한 내용을 포함하여 트랜잭션의 내용을 로그에 쓴다, 이 쓰기가 완료될 때까지 기다린다
2. 저널 커밋 - TxE 를 포함하여 트랜잭션의 커밋 블럭을 로그에 쓴다, 쓰기가 완료되기를 기다린다 (트랜잭션은 커밋된 상태)
3. 체크포인트 - 갱신에 대한 내용을 파일 시스템 내의 원래 위치에 쓴다
4. 프리 - 일정 시간 이후에 저널 슈퍼블럭을 갱신하여 저널의 트랜잭션을 해제한다
데이터 저널링 프로토콜은 상기 내용과 같다, 하지만 크래시는 자주 일어나지 않는 것에 비해 복구를 위해 모든 데이터 블럭을 디스크에 두 번씩 기록하는 것은 부담스러운 방법이다
메타데이터 저널링
이제까지 다룬 저널링 모드는 데이터 저널링이라고 부르고 (ex. Linux ext3),
간단하고 대중적인 저널링의 형태는 Ordered Journaling 또는 메타데이터 저널링이라고 한다
저널에 데이터 블럭을 기록하지 않는다는 것을 제외하면 거의 대부분이 동일하다
갱신 작업이 일어날 때, 데이터 저널링에서는 데이터 블럭을 로그에 기록한 것과 달리
Ordered 모드 저널링에서는 파일 시스템의 원래 위치에 데이터 블럭을 기록한다
두 번씩 쓰지 않는 것만으로도 저널링으로 내려가는 I/O 의 오버헤드 정도를 상당히 감소시킬 수 있다
메타데이터만 저널링하는 경우 데이터 블럭을 디스크에 내려 보내는 시점에 매우 중요한데, 일관성을 유지시키기 위해
일반적인 파일 시스템에서는 메타데이터를 저널에 기록하기 전에 반드시 관련 데이터 블럭들을 디스크에 먼저 쓴다
1. 데이터 쓰기 - 데이터를 최종 위치에 쓴다 (완료될 때까지 대기할 필요는 없다)
2. 저널 메타데이터 쓰기 - 시작 블럭과 메타데이터를 로그에 쓴다, 완료될 때까지 기다린다
3. 저널 커밋 - TxE 를 포함한 트랜잭션 커밋 블럭을 로그에 쓴다, 쓰기가 완료될 때까지 기다린다, 이제 트랜잭션은 커밋된 상태이다
4. 체크포인트 메타데이터 - 갱신된 메타데이터의 내용을 파일 시스템 상에 있어야 할 최종 위치에 갱신한다
5. 해제 - 저널 슈퍼블럭에 해당 트랜잭션이 해제되었다고 표기한다
데이터가 먼저 기록되는 것을 강제하여 아이노드 포인터가 쓰레기 데이터를 가리키지 않는다는 것을 보장한다
크래시 일관성의 핵심 법칙은 "포인터의 대상이 되는 객체를 그것을 가리키는 객체보다 먼저 써라"는 것이다
Ordered 모드와 Unordered 모드 둘 다 메타데이터 일관성은 보장하지만, Unordered 모드의 경우 데이터의 일관성은 보장되지 않는다
까다로운 사례 - 블럭 재활용
데이터 블럭들은 저널링을 안 한다
임의 디렉터리가 존재하고 (디렉터리는 메타데이터로 간주된다) 해당 디렉터리의 데이터 블럭과 디렉터리를 삭제한 상황에서
새로운 파일을 생성하였을 때 이전 디렉터리가 사용하던 블럭을 할당받을 수 있다, 데이터와 함께 해당 파일의 아이노드는 디스크에 저장되지만, 메타데이터 저널링이 사용되고 있기 때문에 아이노드"만" 저널에 커밋되고 데이터 블럭은 저널에 기록되지 않는다
그래서 크래시가 발생하고, 복구 시 로그에 있는 모든 정보를 순서에 따라 다시 실행한다, 해당 블럭에 있었던 디렉터리 데이터 정보를 체크포인트하면서 복구 과정에서 현재 생성한 파일은 이전의 디렉터리 정보로 덮어써지게 되는 문제가 발생한다
이 문제의 해결책 중 하나는, 지워진 블럭이 체크포인트될 때까지 절대로 재사용하지 않는 것이다
Linux ext3 은 저널에 철회 레코드 (Revoke Record) 라는 새로운 항목을 추가하여, 디렉터리를 삭제하면 저널에 철회 레코드를 기록하도록 만들었다, 그래서 저널을 재실행하면 철회 레코드의 존재 여부를 먼저 탐색하고, 철회된 내용은 재실행하지 않게 문제를 회피하였다
마치며 - 흐름도
이 부분 다시 읽어보기
42.4 해법 3 - 그 외 방법
Soft Update
파일 시스템의 모든 쓰기들의 순서를 잘 정해서 디스크 상의 자료 구조가 절대로 불일치 상태가 되지 않도록 하는 기법
ex. 데이터 블럭을 가리키는 아이노드가 기록되기 전에 아이노드가 가리키는 대상 데이터 블럭을 먼저 기록하여 아이노드가 쓰레기 값을 가진 블럭을 가리키지 않게 한다
파일 시스템 자료 구조에 대한 심도 있는 지식이 필요하여 구현이 쉽지 않다
Copy-On-Write, COW
파일이나 디렉터리를 절대로 원래 위치에 덮어쓰지 않고, 디스크에서 사용되지 않은 위치에 갱신 내용을 저장한다
갱신 작업이 완료되면 루트 자료 구조에 새롭게 갱신된 자료 구조를 가리키는 "포인터"를 포함하도록 변경한다
백포인터 기반 일관성 Backpointer-Based Consistency, BBC
쓰기 사이에 어떤 순서도 강요하지 않으며, 시스템의 모든 블럭에 백포인터를 추가한다
ex. 각 데이터 블럭은 자신이 속해 있는 아이노드에 대한 참조를 가지고 있다
Optimistics Crash Consistency
트랜잭션 체크섬 Transaction Checksum 을 사용하여 요청할 수 있는 가장 큰 크기의 쓰기를 디스크로 내려 보내며
다른 몇 가지 기술들을 적용하여 일관성을 보장한다
42.5 요약
저널링은 복구 시간을 디스크 볼륨 크기에서 로그의 크기로 줄이기 때문에 복구에 드는 소요 시간을 상당히 줄일 수 있다
Ordered 모드 저널링은 메타데이터 뿐 아니라 사용자 데이터까지 적절한 수준의 일관성을 유지하도록 보장하면서
저널로 내려가는 트래픽 양도 줄이는 기법이다 - 결국 사용자 데이터에 대한 강력한 일관성 보장이 가장 중요하다