디스크의 물리적 한계를 극복하는 순차 쓰기 전략
지난번에 FFS(Fast File System)를 통해 디스크의 물리적 구조(Cylinder Group)를 고려한 배치가 성능을 어떻게 개선하는지 확인했다. 하지만 여전히 해결되지 않은 문제가 있다. "데이터를 덮어쓴다(Update-in-place)"는 기존의 방식은 필연적으로 탐색 시간(Seek Time)과 회전 지연(Rotational Latency)을 유발한다. 특히 작은 파일들을 무작위로 쓸 때(Random Write), 디스크 헤드는 쉴 새 없이 움직여야 한다.
운영체제 수업의 파일 시스템 파트 마지막 주제인 Log-structured File System (LFS)은 이 문제에 대해 다음과 같은 새로운 질문을 던진다. "그냥 모든 데이터를 로그(Log)처럼 끝에 갖다 붙이면 안 될까?"
오늘은 하드웨어의 물리적 제약을 소프트웨어 아키텍처의 전환으로 극복하려 했던 LFS의 설계 철학과 핵심 메커니즘을 정리해 본다.
1. 발상의 전환: 디스크를 거대한 로그로 취급하다
기존 파일 시스템은 파일 내용이 바뀌면 원래 있던 위치를 찾아가서 덮어썼다. 하지만 LFS는 모든 업데이트(데이터 블록, 아이노드 등)를 디스크의 끝에 순차적으로 기록(Sequential Write)한다.
쓰기 버퍼링 (Write Buffering)과 세그먼트 (Segment)
물론 4KB 블록 하나가 생성될 때마다 매번 디스크 끝에 쓴다면 효율이 나지 않는다. 그래서 LFS는 메모리에 쓰기 버퍼(Write Buffer)를 두고 업데이트를 모은다. 어느 정도 데이터가 쌓여 거대한 덩어리, 즉 세그먼트(Segment)가 되면 그때 한 번에 디스크로 플러시(Flush)한다. 이렇게 하면 작은 쓰기 요청들이 모여 하나의 거대한 순차 쓰기(Sequential Write)가 되므로, 디스크 대역폭을 100%에 가깝게 활용할 수 있다.
| Segment |
"덮어쓰기를 포기하고 순차 쓰기를 선택함으로써, 기계적인 디스크 헤드의 움직임을 최소화했다."
2. 인덱싱의 난제: 흩어진 아이노드를 찾아라
여기서 심각한 문제가 발생한다. 기존 유닉스 파일 시스템에서 아이노드(inode)는 고정된 위치(Inode Table)에 있었다. 아이노드 번호만 알면 위치를 계산할 수 있었다. 하지만 LFS에서는 데이터를 수정할 때마다 새로운 아이노드를 로그 끝에 새로 쓴다. 즉, 아이노드가 디스크 여기저기에 흩어져 있고, 계속 위치가 바뀐다.
| Inode in LFS |
아이노드 맵 (Inode Map, imap)
파일 시스템은 아이노드 번호를 받아 최신 아이노드의 디스크 주소를 반환해 주는 새로운 자료구조, 아이노드 맵(imap)을 도입했다.
imap[k]: k번 아이노드가 현재 디스크의 어느 주소에 있는지 저장한다.
그런데 이 imap은 어디에 저장해야 할까? imap도 결국 데이터다. LFS는 imap 또한 로그의 일부로 간주하여 세그먼트에 같이 기록해 버린다.
체크포인트 영역 (Checkpoint Region, CR)
문제가 꼬리에 꼬리를 문다. imap이 로그에 계속 기록된다면, 최신 imap은 어디서 찾는가?
이를 위해 LFS는 디스크의 고정된 위치(보통 시작 부분)에 체크포인트 영역(CR)을 두었다. CR은 주기적으로(예: 30초마다) 업데이트되며, 최신 imap 블록들의 위치를 가리킨다.
결국 파일을 읽는 과정(Lookup)은 다음과 같이 재귀적인 구조를 띤다.
CR 읽기: 고정된 위치에서 최신
imap의 위치를 파악한다.imap 읽기: 메모리에 캐싱된 맵을 통해 원하는 파일의 아이노드 위치를 찾는다.
Inode 읽기: 아이노드를 읽어 데이터 블록의 위치를 찾는다.
데이터 읽기: 실제 파일 내용을 읽는다.
3. 필연적인 숙제: 가비지 컬렉션 (Garbage Collection)
로그 구조의 치명적인 단점은 "옛날 버전"의 쓰레기 데이터(Garbage)가 디스크에 남는다는 것이다. 파일을 덮어쓰지 않고 새로 썼으니, 이전 위치에 있던 데이터와 아이노드는 무효가 된다. 디스크 공간은 무한하지 않으므로, 언젠가는 이 쓰레기를 치워야 한다.
| Garbage |
LFS는 Cleaner라는 백그라운드 프로세스를 통해 가비지 컬렉션을 수행한다.
세그먼트 읽기: 디스크에서 오래된 세그먼트들을 읽어들인다.
생존 여부 확인 (Liveness Check): 각 블록이 여전히 유효한지 확인한다. 이를 위해 세그먼트 헤더에 있는 Segment Summary Block을 활용한다. (이 블록이 현재 파일 시스템의 최신 아이노드가 가리키는 블록과 일치하는가?)
데이터 복사: 살아있는(Live) 블록들만 골라내어 새로운 세그먼트로 모아서 쓴다.
공간 해제: 기존의 오래된 세그먼트 공간을 비운다.
이 과정은 프로그래밍 언어의 메모리 GC와 매우 유사하다는 생각이 들었다. 다만 대상이 메모리가 아니라 디스크일 뿐이다.
마치며
LFS는 하드 디스크의 물리적 한계(Seek Time)를 소프트웨어 아키텍처로 극복하려 했던 시도였다. 비록 가비지 컬렉션 오버헤드라는 단점이 있었지만, LFS가 제시한 "Log-structured" 철학은 오늘날 더욱 중요해졌다.
SSD와 FTL: 덮어쓰기가 불가능하고 지우기(Erase) 단위가 큰 플래시 메모리(SSD)의 제어 소프트웨어(FTL)가 바로 이 LFS 방식을 차용하고 있다. (지난 시간 복습)
Modern FS: ZFS, Btrfs 같은 현대적인 파일 시스템도 COW(Copy-On-Write) 방식을 채택하여 데이터 일관성과 스냅샷 기능을 제공하는데, 이는 LFS의 로그 구조 철학이 진화한 형태라고 볼 수 있다. (마찬가지로 지난 시간 복습)
추천글:
[운영체제] Operating System 전체 포스팅 모음집
[운영체제] Fast File System (FFS) | 디스크의 물리적 구조를 고려한 성능 최적화
[운영체제] SSD와 FTL | NAND Flash, Garbage Collection, Wear Leveling
[운영체제] 페이징(Paging) | Page Table, Copy-on-Write, TLB