[OS] 35. Log-structured File Systems

Park Yeongseo·2024년 3월 8일
0

OS

목록 보기
44/54
post-thumbnail

Operating Systems : Three Easy Pieces를 보고 번역 및 정리한 내용들입니다.

90년대 초반 버클리에서 로그-기반 파일 시스템이 개발되었는데, 다음과 같은 관찰들에 기반한다.

  • 시스템 메모리가 커지고 있음
    + 메모리가 커지면서, 더 많은 데이터들이 메모리에 캐시될 수 있게 됐다.
    + 더 많은 데이터들이 캐시되면서 디스크 트래픽에는 많은 쓰기 작업들이 포함된다(읽기의 경우 캐시 처리되기 때문에).
    + 그러므로 파일 시스템의 성능은 쓰기 성능에 더욱 더 많은 영향을 받게 된다.
  • 랜덤 I/O와 순차 I/O 성능 사이에 큰 차이가 있음
    + 드라이브 표면에 더 많은 비트가 압축될 수 있게 됨에 따라, 하드 드라이브 전송 대역폭은 크게 증가해왔다.
    + 하지만 탐색 및 회전 지연의 비용은 천천히 감소해왔다. 플래터를 더 빨리 돌리고, 디스크 암을 더 빨리 움직이는, 싸고 작은 모터를 만드는 일은 어렵기 때문이다.
    + 따라서 디스크를 순차적인 방식으로 사용하는 것이 랜덤 방식보다 훨씬 더 성능적 이점을 가지게 된다. 더 적은 탐색 시간 및 회전 지연을 가지기 때문이다.
  • 현존하는 파일 시스템들은 많은 흔한 워크로드들에 대해 낮은 성능을 보임
    + 예를 들어 FFS는 한 블럭 크기의 새 파일을 만드는 데에도 많은 수의 쓰기를 수행해야 한다. (아이녿, 아이노드 비트맵, 디렉토리 데이터 블럭, 디렉토리 아이노드, 새 파일의 데이터 블럭과 데이터 비트맵)
    + FFS가 이 모든 블럭들을 같은 블럭 그룹에 놓는다 해도, 짧은 탐색 및 회전 지연이 많이 발생하게 된다.
  • 파일 시스템이 RAID-aware가 아님
    + RAID-4와 RAID-5는 small-write problem(단일 블럭으로의 하나의 논리적 쓰기가 네 번의 물리 I/O를 일으키는 문제)를 가지고 있다. (당시) 현존하던 파일 시스템들은 이러한 RAID의 최악의 경우를 해결하지 못한다.

이상적인 파일 시스템은 쓰기 성능에 집중하고, 되도록이면 디스크의 순차 대역폭을 사용할 수 있게 해야 한다. 또한 데이터를 쓸 뿐만 아니라 디스크 상 메타데이터 구조도 자주 업데이트하는 흔한 워크로드들에 대해서도 잘 동작해야한다. RAID에 대해서도 단일 디스크인 것처럼 잘 작동해야 한다.

새로운 타입의 파일 시스템인 로그-기반 파일 시스템(Log-structured File System, LFS)이 도입된 이유는 위와 같다. 디스크에 쓸 때, LFS는 모든 업데이트들을 일단은 메모리 내 세그먼트에 버퍼링한다. 세그먼트가 꽉 차면, 이는 디스크의 쓰이지 않은 부분에 하나의 길고 순차적인 전송으로 쓰인다. LFS는 기존에 있던 데이터를 절대로 덮어 쓰지 않으며, 항상 세그먼트들을 가용 위치에 쓴다. 세그먼트가 크므로, 디스크는 효율적으로 쓰이고, 파일 시스템의 성능 또한 정점에 도달할 수 있게 된다.

1. Writing To Disk Sequentially

다뤄야 할 첫 번째 문제는 다음과 같다. 어떻게 모든 파일 시스템 상태 업데이트를 디스크에의 순차적 쓰기 작업으로 바꿀 수 있을까? 간단한 예를 통해 살펴 보자. 데이터 블럭 DD를 파일에 쓴다고 하자. 데이터 블럭을 디스크에 쓰는 것은 다음과 같은 디스크 상 레이아웃을 가진다. DD는 디스크 주소 A0A0에 쓰인다 하자.

![[Pasted image 20240306232215.png]]

하지만 사용자가 데이터 블럭을 쓸 때, 오직 데이터만이 디스크에 쓰이는 것은 아니고, 업데이트되어야 할 메타데이터들도 있다. 이 경우 파일의 아이노드(II)도 디스크에 쓰고, 이것이 데이터 블럭 DD를 가리킨다고 하자. 디스크에 쓰일 때, 데이터 블럭과 아이노드는 다음과 같이 생겼을 것이다.

![[Pasted image 20240306232404.png]]

이렇게 단순히 모든 업데이트들을 디스크에 순차적으로 쓰는 것이 LFS의 핵심 아이디어다.

2.Writing Sequentially And Effectively

하지만 디스크에 순차적으로 쓰는 것만으로 효율적 쓰기 작업을 보장할 수는 없다. 예를 들어 시간 TT에 주소 AA에 있는 한 블럭에 쓰고, T+δT + \delta에 주소 A+1A + 1에 쓴다고 해보자. 그런데 첫 번째와 두 번째 쓰기 도중에 디스크는 회전해 있고, 따라서 두 번째 쓰기 요청이 일어났을 때에는 거의 한 번의 회전에 필요한 만큼의 시간을 대기해야 하게 된다. 따라서 단순히 쓰기를 순차적으로 수행한다고 해서 최고 성능에 도달할 수 있는 것은 아니다. 좋은 쓰기 성능을 얻기 위해서는 드라이브에 많은 수의 연속적인 쓰기가 일어나야 한다.

이를 위해 LFS는 쓰기 버퍼링(write buffering이라는 오래된 테크닉을 사용한다. 디스크에 쓰기 전에 LFS는 업데이트 내역을 메모리에 보관하고, 충분한 수의 업데이트가 모이게 되면 이들을 디스크에 모두 한 번에 씀으로써 디스크를 효율적으로 쓸 수 있게 한다.

이렇게 LFS가 한 번에 쓰는 업데이트의 덩어리를 세그먼트(segment)라 부른다. LFS는 업데이트들을 메모리 내 세그먼트에 버퍼링하고, 한 번에 세그먼트를 모두 디스크에 쓴다. 세그먼트가 충분히 크다고 한다면, 이러한 쓰기 작업은 효율적이 된다.

3. How Much To Buffer

그렇다면 다음과 같은 물음이 생길 수 있다. LFS는 얼마나 많은 업데이트를 버퍼링해야 할까? 물론 그 답은 디스크에, 구체적으로는 전송률 대비 접근 시간이 얼마나 걸리느냐 하는 데에 달려 있다.

예를 들어 각 쓰기 전의 접근 시간이 약 TpositionT_{position}초 걸린다고 하자. 또한 디스크의 전송 속도는 RpeakR_{peak}MB/S라하자. 이러한 디스크를 가정했을 때 LFS는 얼마나 많이 버퍼링해야 할까?

DD MB의 데이터를 디스크에 쓰려고 한다고 해보자. 이 데이터를 디스크에 쓰는 데에 걸리는 시간은 TwriteT_{write}라 하자. 즉 TwriteT_{write}는 접근 시간 + 전송 시간이다.

Twrite=Tposiion+DRpeakT_{write} = T_{posiion} + \frac{D}{R_{peak}}

이제 쓰기의 효과적인 속도(rate)를 ReffectiveR_{effective}라 하고 구해보자. 쓰기 속도는 데이터의 양을 전체 쓰기 시간으로 나눈 것과 같다.

Reffective=DTwrite=DTposition+DRpeakR_{effective} = \frac{D}{T_write} = \frac{D}{T_{position} + \frac{D}{R_{peak}}}

ReffectiveR_{effective}를 최대한 최고 속도에 가깝게 만드는 것이 목표다. 구체적으로, 최고 속도의 F(0<F<1)F(0<F<1) 배 한 만큼의 속도를 얻고 싶다 해보자. 즉 Reffective=DTposition+DRpeak=F×RpeakR_{effective} = \frac{D}{T_{position} + \frac{D}{R_{peak}}} = F \times R_{peak}다. 이제 이 등식을 DD에 대해서 풀면

D=F1F×Rpeak×TpositionD = \frac{F}{1-F} \times R_{peak} \times T_{position}

를 얻을 수 있다.

4. Problem: Finding Inodes

LFS에서 어떻게 아이노드를 찾는지를 알기 위해, 전형적인 UNIX 파일 시스템에서는 어떻게 아이노드를 찾는지를 간단히 복습해보자. FFS 등의 전형적인 UNIX 파일 시스템에서 아이노드를 찾는 일은 쉽다. 왜냐하면 그것들은 배열로, 디스크의 고정된 위치에 위치하기 때문이다.

예를 들어, 오래된 UNIX 파일 시스템은 모든 아이노드들을 디스크의 고정된 부분에 보관한다. 그렇기 때문에 아이노드 번호와 시작 주소로 특정 아이노드를 찾기 위해서는, 그냥 아이노드 번호를 아이노드의 크기로 곱하고 이를 디스크 상 배열의 시작 주소에 더해주기만 하면 된다.

FFS에서 주어진 아이노드 번호의 아이노드를 찾는 일은 좀 더 복잡한데, FFS는 아이노드 테이블을 여러 청크로 나누고, 아이노드의 그룹들을 각 실린더 그룹에 위치시키기 때문이다. 그렇기 때문에 아이노드를 찾기 위해서는 각 실린더 그룹의 아이노드 청크의 크기와 그 시작 주소도 알아야 한다. 이것들을 안다면 계산은 이전과 비슷하고 쉽게 마칠 수 있다.

문제는 LFS가 기존에 정보들을 덮어 쓰지 않는다는 것이다. 새로운 버전의 아이노드 또한 다른 위치에 계속 갱신되며 이동하게 된다.

5. Solution Through Indirection: The Inode Map

이 문제를 해결하기 위해 LFS 설계자들은 아이노드 맵(inode map, imap)이라는 자료 구조를 통해, 아이노드 번호와 아이노드 사이에 간접층(level of indirection)이라는 개념을 추가했다. imap은 아이노드 번호를 인풋으로, 그에 해당하는 아이노드의 최신 버전의 주소를 아웃풋으로 가지는 자료 구조다. 아이노드가 새로 디스크에 쓰일 때마다 imap 또한 업데이트 된다.

하지만 imap은 디스크에 영구적으로 저장되어야 한다. 왜냐하면 이렇게 해야만, 충돌이 일어나더라도 아이노드의 위치를 추적하고 원하는 작업을 할 수 있기 때문이다. 그렇다면 imap은 디스크의 어디에 위치해야 할까?

물론 디스크의 고정된 부분을 차지하게 할 수도 있다. 하지만 이 경우 파일 구조를 바꾼 후 imap의 내용을 바꾸기 위해서는 긴 디스크 탐색 시간이 걸리게 된다.

LFS는 그 대신 아이노드 맵의 청크들을 모든 다른 새 정보들을 쓰는 곳 바로 옆에다 위치시킨다. 따라서 데이터 블럭을 파일 kk에 추가할 때, LFS는 그 새 데이터 블럭, 아이노드, 그리고 아이노드맵을 모두 디스크에 함께 쓴다.

위 그림에서 imap은 아이노드 kk가 디스크 주소 A1A1에 있음을 표시하고, 아이노드에서는 데이터 블럭 DD가 주소 A0A0에 있음을 표시한다.

6. Completing The Solution: The Checkpoint Region

그런데 아이노드 맵 자체는 어떻게 찾을 수 있을까? 이것도 디스크 여기저기에 흩어져 있는데 말이다. 결국에 파일 시스템은 파일 검색을 시작하기 위해 어떤 고정된, 잘 알려진 디스크 상 위치를 필요로 한다.

LFS는 이를 위해 디스크 상 고정된 장소를 마련해놓으며, 이를 체크포인트 영역(checkpoint region, CR)이라 부른다. 체크포인트 영역은 최신 아이노드 맵을 가리키는 포인터를 포함하고 있다. 따라서 아이노드 맵의 원소들을 찾기 위해서는 우선 CR을 읽어야 한다. 체크포인트 영역은 주기적으로만 업데이트되며, 따라서 성능에 큰 악영향을 미치지 않는다.

7. Reading A File From Disk: A Recap

이제 디스크로부터 파일을 읽을 때 무슨 일이 일어나는지를 살펴보자. 우선은 체크포인터 영역을 읽어야 한다. 체크포인트 영역은 전체 아이노드 맵을 가리키는 포인터들이 들어있으므로, LFS는 이 아이노드 맵을 읽고 메모리에 캐시한다. 이후 파일의 아이노드 번호가 주어지면, LFS는 아이노드 번호를 가지고 imap에 매핑된 아이노드의 디스크 내 주소를 찾고 아이노드의 가장 최신 버전을 읽는다. 이 시점에서 LFS는 파일로부터 블럭을 읽기 위해, 직간접 포인터 등을 이용하는 전형적인 UNIX 파일 시스템과 동일하게 동작한다. 대부분의 경우 LFS는 파일을 디스크로부터 불러올 때 전형적인 파일 시스템과 같은 수의 I/O를 수행한다. 전체 imap은 캐싱되어 있고, 읽기 작업에서 LFS가 추가적으로 해야할 일은 아이노드의 주소를 imap에서 찾는 일 밖에 없다.

8. What About Directories

그런데 파일 시스템 내의 파일에 접근하기 위해서는 어떤 디렉토리들도 접근되어야 한다. 어떻게 LFS는 디렉토리 데이터를 저장할까?

다행히도, 디렉토리 구조는 클래식한 UNIX 파일 시스템과 기본적으로 동일하다. 디렉토리가 그저 이름과 아이노드 번호의 매핑 컬렉션이라는 점에서 바로 그렇다. 예를 들어 디스크에 파일을 생성할 때, LFS는 새 아이노드와 데이터도 쓰고, 디렉토리 데이터와 해당 파일을 참조하는 아이노드도 쓴다. LFS가 이러한 작업들을 디스크 상에서 순차적으로 수행한다는 것을 유의하자. 따라서 파일 foo의 생성은 다음과 같은 디스크 상 구조를 가지게 된다.

아이노드 맵 원소에는 디렉토리 파일 dirdir과 새로 만들어진 파일 ff의 위치 정보가 모두 들어가있다. 그러므로 아이노드 번호 kk를 갖는 파일 foofoo에 접근할 때에는 우선 메모리에 캐시된 아이노드 맵을 보고 디렉토리 dirdir의 아이노드의 위치를 찾아야 한다. 그 위치를 찾아 디렉토리 아이노드를 읽으면 디렉토리 데이터의 위치를 찾을 수 있다. 이 데이터 블럭에는 이름-아이노드 번호의 매핑이 있다. 해당 아이노드 번호를 가지고 다시 아이노드 맵에서 해당 아이노드의 위치를 찾고, 그 아이노드를 읽음으로써 찾고자 했던 데이터 블럭에 접근한다.

아이노드 맵은 재귀 업데이트 문제(recursive update problem)라 불리는, LFS의 심각한 문제를 해결해준다. 이 문제는 LFS와 같이 그 자리에 바로 업데이트 하지 않고, 디스크의 새 위치로 업데이트를 이동하는 어떤 파일 시스템에서든지 일어날 수 있다.

아이노드가 업데이트 될 때 해당 아이노드의 디스크 상 위치는 계속해서 바뀐다. 그런데 디렉토리가 파일의 아이노드를 (직접) 가리킨다면, 해당 아이노드의 위치가 바뀔 때 디렉토리의 데이터 블럭도 바뀌어야 한다. 해당 디렉토리 데이터 블럭의 업데이트가 일어나면 그 디스크 상 위치도 바뀌므로, 이러한 변화는 그 부모 디렉토리에도 반영이 되어야 하고, 재귀적으로 파일 시스템 트리의 루트까지 이어지게 된다.

LFS는 이 문제를 아이노드 맵을 통해 해결한다. 아이노드의 위치는 바뀔 수 있지만, 이 변화는 디렉토리 자체에 반영되지 않는다. imap은 디렉토리가 같은 이름-아이노드 번호 매핑을 유지하게 하면서 업데이트될 수 있다. 이런 간접적인 접근을 통해 LFS는 재귀 업데이트 문제를 해결한다.

9. A New Problem: Garbage Collection

LFS가 반복적으로 파일의 최신 버전을 디스크 상의 새 위치에 씀으로 인해 생길 수 있는 문제가 있다. 이러한 과정은 쓰기 작업을 효율적으로 만드는 한편, 파일의 오래된 버전들을 디스크 여기저기에 흩어 놓는다. 이러한 오래된 버전들을 가비지(garbage)라 부른다.

예를 들어 아이노드 번호 kk를 가지는 파일이 있고, 이것이 하나의 데이터 블럭 D0D0을 가리킨다고 하자. 블럭을 업데이트 하면, 새 아이노드와 데이터 블럭이 생겨난다. 결과로 나오는 레이아웃은 다음과 같다.

다이어그램을 보면, 디스크 상에 아이노드와 데이터 블럭의 두 버전이 있음을 볼 수 있다. 데이터 블럭 하나를 업데이트하는 간단한 행동으로 디스크 상에 많은 수의 새 구조들이 남아있게 되는 것이다. 이 경우 새 버전이 현재 파일 시스템의 부분으로 살아있어야 한다.

다른 예로, 이번에는 원래 파일 kk에 새 블럭을 추가한다고 해보자. 이 경우, 아이노드의 새 버전이 생기지만 이 아이노드는 아직도 오래된 데이터 블럭을 가리킨다. 이 경우 오래된 데이터 블럭도 현재 파일 시스템의 부분으로 살아있어야 한다.

그럼 이런 아이노드, 데이터 블럭 등등의 오래된 버전들은 어떻게 해야할까? 그냥 디스크에 남겨두고 나중에 어떤 사고가 일어났을 때 복구하기 위한 용도로 사용할 수도 있다. 이러한 파일 시스템을 가리켜 버저닝 파일 시스템(versioning file system)이라 부르는데, 파일의 여러 다른 버전들을 계속해서 쌓아나가기 때문이다.

하지만 LFS는 파일의 최신 버전만을 남겨둔다. 따라서 LFS는 파일 데이터, 아이노드 등등의 오래된, 죽은 버전들을 찾아 정리(clean)해야 한다. 이러한 작업은 디스크 상의 블럭들을 순차적인 쓰기 작업에 사용하기 위해 다시 가용 상태로 만든다.

이전에 세그먼트가 LFS의 대용량 디스크 쓰기를 가능케 하는 중요한 메커니즘이라 말한 적이 있다. 그런데 세그먼트는 이러한 효율적인 정리 작업에 있어서도 중요하다. LFS 클리너가 그저 전체 디스크를 돌면서 하나의 데이터 블럭, 아이노드 등을 해제한다고 해보자. 만약 그렇다면 파일 시스템은 디스크 상 할당된 공간들 사이에 여러 가용 홀들을 가진 상태가 될 것이고, 이 경우 쓰기 성능은 상당히 떨어지게 된다. LFS가 디스크에 순차적으로, 높은 성능을 가지고 쓸 수 있는 큰 연속 영역을 찾을 수 없게 되기 때문이다.

대신 LFS 클리너는 세그먼트 단위로 작업을 함으로써 이후의 쓰기 작업에서 쓸 수 있는 큰 공간 청크들을 비워낸다. 기본적인 정리 프로세스는 다음과 같다. 주기적으로 LFS 클리너는 여러 오래된 세그먼트들을 읽고, 해당 세그먼트 중에 살아있는 블럭이 어떤 것인지를 결정한다. 이후 그렇게 살아있는 블럭들을 모아 새로운 세그먼트들을 만들고, 오래된 것들은 모두 비운다. 구체적으로 클리너는 MM개의 세그먼트들을 읽어 그 중 살아있는 것들을 모아 NN개의 새 세그먼트(N<MN < M)를 만들고, 이것들을 디스크의 새 위치에다 쓴다. 오래된 MM개의 세그먼트는 비워지고후속 쓰기들을 위해 사용된다.

그렇다면 LFS는 세그먼트 내에 어떤 블럭이 살아있는지 어떻게 알 수 있을까? 그리고 클리너는 얼마나 자주 실행되어야 하고, 정리하기 위해서는 어떤 세그먼트를 골라야 할까?

10. Determining Block Liveness

디스크 상 세그먼트 SS에 있는 데이터 블럭 DD가 주어졌을 때, LFS는 해당 블럭이 살아있는지의 여부를 결정할 수 있어야 한다. 이를 위해 LFS는 각 세그먼트에 약간의 정보들을 추가한다. 구체적으로, LFS는 각 데이터 블럭 DD에 대해, 그 아이노드 번호와 오프셋을 기록하며, 이 정보들은 세그먼트의 헤드에 있는, 세그먼트 요약 블럭(segment summary block)에 기록된다.

이러한 정보가 주어진다 했을 때, 해당 블럭이 살아있는지 그렇지 않은지를 결정하는 일은 쉽다. 디스크 주소 AA에 저장된 블럭 DD에 대해, 세그먼트 요약 블럭을 읽고 그 아이노드 번호 NN과 오프셋 TT를 찾는다. 다음으로는 NN이 있는 imap을 보고, NN을 디스크로부터 읽는다. 다음으로는 오프셋 TT를 이용해 아이노드를 보고, 해당 파일의 TT번째 블럭이 디스크 상 어디에 있는지를 확인한다. 만약 이것이 정확히 주소 AA를 가리키고 있다면 해당 블럭은 살아있고, 만약 그렇지 않다면 사용되지 않아 필요없는 버전으로 판단한다.

위 다이어그램은 이 메커니즘을 그린 것으로, 세그먼트 요약 블럭 SSSS는 주소 A0A0에 있는 데이터 블럭이 정확히 파일 kk의 오프셋 00에 해당하는 것임을 기록한다. kk의 imap을 체크함으로써 아이노드를 찾고, 그것이 실제로 해당 위치를 가리키는지를 볼 수 있다.

LFS가 이러한 프로세스를 더 효율적으로 만들기 위해 사용하는 방법도 있다. 예를 들어 파일이 잘리거나 삭제될 때, LFS는 그 버전 번호를 올리고 새 버전 번호를 imap에 기록한다. 버전 번호를 디스크 상 세그먼트에 기록함으로써, LFS는 위에서 언급된 긴 과정 없이, 단순히 디스크 상의 버전 번호와 imap의 버전 번호를 비교함으로써 문제를 해결할 수 있게 된다.

11. A Policy Question: Which Blocks To Clean, And When?

이제는 언제, 그리고 어떤 블럭을 정리해야 할지를 정해야 한다. 언제 정리할지를 정하는 일은 쉽다. 주기적으로, 혹은 한가할 때, 혹은 디스크가 꽉 찼을 때 하면 될 것이다.

어떤 블럭을 정리할지를 정하는 것은 더 어렵다. LFS와 관련된 원래 논문에서 저자들은 뜨거운(hot) 세그먼트와 차가운(cold) 세그먼트를 분리하려 하는 방식을 설명했다. 뜨거운 세그먼트는 그 내용이 갱신되는 것이다. 이러한 세그먼트들에 대한 최고의 정책은 정리 전에 오랫동안 대기하다, 점점 더 많은 블럭들이 새 세그먼트에 쓰이게 되면 그제서야 비워지게 된다. 이와 반대로 차가운 세그먼트는, 몇 개의 죽은 블럭들을 가지고는 있지만, 전체적으로는 내용 갱신이 상대적으로 드문 것을 말한다. 저자들은 차가운 세그먼트들을 우선적으로, 뜨거운 세그먼트들을 나중에 정리하는 것이 좋다고 결론 짓고, 해당 작업을 위한 휴리스틱을 개발했다. 하지만 다른 것들이 그렇듯 완벽한 정책은 아니었고, 이후 더 나은 다른 방식들이 개발되었다.

12. Crash Recovery And The Log

마지막은 LFS가 디스크에 쓰고 있을 때 시스템 충돌이 일어나면 어떤 일이 일어나는지에 대한 것이다.

정상 작동의 경우 LFS는 쓰기 작업을 세그먼트에 버퍼링하고, 이 세그먼트들을 나중에 디스크에 쓴다. LFS는 이러한 쓰기를 로그로 구성한다. 즉 체크포인트 영역은 헤드, 테일 세그먼트를 가리키고, 각 세그먼트는 다음으로 쓰일 세그먼트를 가리킨다. LFS는 또한 주기적으로 체크포인트 영역을 갱신한다. 시스템 충돌은 이러한 과정 내 어디에서든 일어날 수 있다. 그렇다면 LFS는 어떻게 이러한 충돌을 처리할 수 있을까?

우선은 두 번째 경우, 체크포인트 영역 업데이트에서 시스템 충돌이 일어나는 경우부터 살펴보자. CR 업데이트가 원자적으로 일어남을 보장하기 위해 LFS는 디스크의 각 끝에 하나 씩 두 개의 CR을 두고, 그 둘에 번갈아 쓴다. 또한 LFS는 CR을 아이노드 맵 및 다른 정보들을 가리키는 최신 포인터로 업데이트하기 위한 프로토콜도 사용한다. 구체적으로, 우선은 헤더에 타임스탬프를 담아 쓰고, 바디를 쓴 후, 마지막 블럭도 타임스탬프를 담아 쓴다. 만약 CR 업데이트 중 시스템 충돌이 일어나면 LFS는 이를 타임스탬프 쌍이 서로 일관적인지를 비교해 봄으로써 확인할 수 있다. LFS는 가장 최신의, 일관적인 타임스탬프를 갖는 CR을 사용하므로 CR의 일관적 업데이트가 가능해진다.

이제 첫 번째 경우를 다뤄보자. LFS가 CR을 약 30초 간격으로 쓰기 때문에, 현재 파일 시스템 내에 있는, 최신 정보도 상당히 오래된 것일 수 있다. 시스템 충돌 후 재부팅을 할 때, LFS는 이러한 정보를 바탕으로 쉽게 복구할 수 있을 것이지만, 그렇게 복구된 정보들은 상당히 오래된 것일 수 있고, 몇 초 이내의 정말 최신의 정보들은 손실될 수 있다.

이러한 문제를 개선하기 위해 LFS는 롤 포워드(roll forward)라 불리는, 데이터베이스에서 쓰이던 테크닉을 이용한다. 기본 아이디어는 최신 체크포인트 영역부터 시작해, CR 내에 있는 로그의 끝을 찾고, 이를 가지고 다음 세그먼트들을 탐색해나가며 아직 반영되지 않은 업데이트 내용이 있는지를 확인한다. 만약 그런 내용이 있으면 LFS는 그에 맞게 파일 시스템을 업데이트하고, 마지막 체크포인트 이후에 쓰인 대부분의 데이터 및 메타데이터를 복구할 수 있게 된다.

0개의 댓글