[250425] LevelDB SSTable

Table of Contents

LSM Tree 에 대해 알아보고 있었는데 자연스럽게 LevelDB 의 구조를 아는 것 자체가 큰 도움이 될 것 같았음.

이미 LSM Tree 관련 정리에서 SkipList 에 대해 알아봄.

Memtable 에서 사용하는 구조이며, SkipList의 임계값이 높아지면 불변구조로 변경 후에 디스크에 flush 함. 이 flush를 할 때 SSTable 이라는 파일 형식을 사용함.

Level DB는 이 SSTable 을 사용하는 대표적인 데이터베이스임. (정확히는 LSM Tree 를 사용하는 것임)

1 SSTable 정의와 역할

SSTable(Sorted String Table)은 LevelDB에서 디스크에 저장되는 기본 데이터 단위(파일 하나), MemTable의 키-값 쌍을 정렬된 상태로 저장함. SSTable은 불변(immutable) 파일로, 한 번 작성된 후에는 수정되지 않고, 새로운 데이터는 새로운 SSTable로 작성함. (LevelDB Table Format, 뭐 SSTable 의 일반적인 형식이라고 볼 수 있음)

주요 특징 정리:

  • 키-값 쌍은 키 기준으로 정렬되어 저장함.
  • 파일은 여러 블록으로 나뉘며, 데이터, 인덱스, 메타데이터를 포함함.
  • Bloom 필터와 같은 메타데이터를 활용해 읽기 성능을 최적화함.
  • 파일명은 고유한 6자리 숫자와 ".sst" 확장자로 구성됨.

SSTable은 LevelDB의 LSM Tree 구조에서 Level 0 이상의 데이터를 저장하며, 압축(compaction) 과정에서 상위 레벨로 이동하거나 병합됨. (구현체마다 다른 방식으로 최적화하는데 Level DB는 Leveling 이라는 방식을 쓰는듯?)

2 SSTable의 구조

사실 공식문서만 봐도 쉽게 알 수 있음. 공식문서 맨 위에 있는 구조를 보자.

<beginning_of_file>
[data block 1]
[data block 2]
...
[data block N]
[meta block 1]
...
[meta block K]
[metaindex block]
[index block]
[Footer]        (fixed size; starts at file_size - sizeof(Footer))
<end_of_file>

SSTable은 여러 구성 요소로 나뉘어 있고, 각 요소는 특정 역할을 수행함. 아래 표는 SSTable의 구조를 요약한 것:

구성 요소 설명 크기/포맷
데이터 블록(Data Blocks) 정렬된 키-값 쌍을 저장. 각 블록은 일반적으로 4KB이며, Snappy 압축 가능. 4KB (기본값), 압축 가능. 체크섬과 압축 유형 포함.
메타 블록(Meta Blocks) 추가 메타데이터 저장. 필터(Bloom 필터)와 통계(Stats) 블록 포함. 압축 가능. 위치: 데이터 블록 다음.
메타인덱스 블록(Metaindex Block) 메타 블록의 이름과 BlockHandle 매핑(예: "filter." → 필터 블록 위치). 위치: 메타 블록 다음.
인덱스 블록(Index Block) 데이터 블록의 키 범위와 위치 저장. 각 항목은 (키 범위, BlockHandle)로 구성. 위치: 메타인덱스 블록 다음.
푸터(Footer) 메타인덱스와 인덱스 블록의 BlockHandle, 패딩, 마직 번호 포함. 파일 끝에 위치. 고정 크기. 파일 끝에 위치.

2.1 데이터 블록

SSTable 의 핵심이며, 데이터가 저장되는 곳. MemTable 에서 플러시된 데이터가 저장되는 곳임. 기본적으로 4KB 크기(Memtable 임계 크기와 같음) Snappy 압축을 지원하는데 구글 자체 제작 압축 알고리즘이며, 데이터 압축 및 압축 해제 속도가 빠름. 그에 따라 압축률이 떨어짐. 블록 끝에 체크섬과 압축유형이 포함되어 있음 (무결성을 위함인듯)

blockbuilder.cc 에서 데이터 블록을 생성하는 과정을 볼 수 있음.

2.2 메타 블록

메타 블록은 두 가지가 존재함

  • 필터 블록 : 블룸필터를 저장, 저장된 키는 다음에 있는 메타인덱스 블록**에 저장됨. 메타인덱스 블록에서 **filter.FilterPolicy::Name() 에 키로 매핑함. 포맷은 다음과 같음
    • 필터 데이터 : 각 필터는 2KB 범위의 키를 커버함.
    • 오프셋 배열: 각 필터의 시작 위치(4바이트씩).
    • 오프셋 배열 시작 위치: 4바이트.
    • lg(base): 1바이트 (기본값: 2KB).
  • 통계 블록 : SSTable의 통계 정보를 저장하며, 키-값 쌍으로 구성됨. 예시 통계는 다음과 같음:
    • 데이터 크기
    • 인덱스 크기
    • 압축되지 않은 키 크기
    • 압축되지 않은 값 크기
    • 항목 수
    • 데이터 블록 수

2.3 메타인덱스 블록

메타인덱스 블록은 메타 블록의 위치를 매핑함 각 항목은 메타 블록의 이름(예: "filter.leveldb.BuiltinBloomFilter2")과 해당 BlockHandle로 구성됨. LevelDB가 메타데이터를 빠르게 찾을 수 있게 함.

2.4 인덱스 블록

인덱스 블록은 데이터 블록의 위치를 매핑하여 빠른 접근을 지원함. 각 항목은 다음과 같은 정보를 포함함.

  • 키 범위: 이전 데이터 블록의 마지막 키와 다음 데이터 블록의 첫 번째 키 사이의 문자열.
  • BlockHandle: 데이터 블록의 오프셋과 크기(varint64 포맷).

인덱스 블록은 이진 탐색을 통해 데이터 블록을 효율적으로 찾을 수 있게 설계됨.

2.5 푸터

푸터는 SSTable 파일의 끝에 위치하며, 고정 크기 40바이트로 구성됨. 포맷은 다음과 같음:

  • 메타인덱스 BlockHandle: 메타인덱스 블록의 오프셋과 크기(varint64).
  • 인덱스 BlockHandle: 인덱스 블록의 오프셋과 크기(varint64).
  • 패딩: 40바이트를 채우기 위한 0으로 채워진 바이트.
  • 매직 번호(상수): 0xdb4775248b80fb57 (리틀 엔디언, 8바이트)로, 파일 무결성을 확인함.

푸터는 파일 끝에서 읽히고, LevelDB가 SSTable의 구조를 파악하는 데 사용됨.

2.6 BlockHandle

BlockHandle은 특정 블록의 위치를 가리키는 포인터로, 다음과 같은 포맷을 가짐:

  • 오프셋(offset): varint64로 인코딩된 파일 내 위치.
  • 크기(size): varint64로 인코딩된 블록 크기.
  • varint64는 Protocol Buffers의 가변 길이 정수 인코딩 방식으로, 효율적인 저장을 지원함 (Protocol Buffers Varints).

3 SSTable 저장 과정

  1. Memtable 순회
    • MemTable은 Skip List로 구현되어 키 기준으로 정렬된 상태임.
    • LevelDB는 MemTable을 순회하여 키-값 쌍을 순서대로 읽음.
  2. 데이터 블록 생성
    • 키-값 쌍은 BlockBuilder를 통해 데이터 블록에 추가함.
    • 블록 크기가 4KB(기본값)에 도달하면, 블록은 체크섬과 압축 유형을 추가하여 SSTable 파일에 기록됨.
    • 새 블록이 시작되고, 이 과정은 모든 키-값 쌍이 기록될 때까지 반복됨.
  3. 메타 블록 생성
    • 데이터 블록 생성 후, 메타 블록을 생성함.
    • 필터 블록과 통계 블록을 생성하여 메타 블록에 추가함.
  4. 인덱스 블록 생성:
    • 각 데이터 블록에 대해 인덱스 블록에 항목이 추가됨.
    • 항목은 키 범위(이전 블록의 마지막 키와 다음 블록의 첫 키 사이)와 BlockHandle(오프셋, 크기)을 포함함.
  5. 필터 블록 생성:
    • FilterPolicy가 지정된 경우, Bloom 필터가 생성되어 필터 메타 블록에 저장됨.
    • 필터는 2KB 단위로 키를 커버하며, 오프셋 배열과 함께 기록됨.
  6. 메타인덱스 블록 생성:
    • 메타 블록(필터, 통계)의 이름과 BlockHandle이 메타인덱스 블록에 기록됨.
  7. 푸터 작성:
    • 메타인덱스와 인덱스 블록의 BlockHandle, 패딩, 매직넘버가 푸터에 기록됨.
    • 푸터는 파일 끝에 고정 크기(40바이트)로 저장됨.
  8. 파일명 지정:
    • SSTable 파일은 데이터베이스 디렉토리 내에 고유한 6자리 숫자와 ".sst" 확장자로 명명됨(예: 000001.sst).
    • 파일 번호는 TableFileName 함수에 의해 생성되며, 시퀀스 번호를 기반으로 증가함.

이 과정은 tablebuilder.cc와 blockbuilder.cc에서 구현되고, SSTable은 Level 0에 저장됨.

4 SSTable 파일명 포맷

  • 형식: <six-digit-file-number>.sst
  • 예시: 000001.sst, 000002.sst, 000003.sst
  • 위치: 데이터베이스 디렉토리(예: /var/data/mydb/000001.sst)

파일 번호는 데이터베이스의 시퀀스 번호를 기반으로 증가하고, TableFileName 함수**에 의해 생성됨. 이 형식은 SSTable의 고유성을 보장하고, **MANIFEST 파일에서 각 SSTable의 레벨과 파일 번호를 추적함 (LevelDB Architecture).

다른 파일 유형도 데이터베이스 디렉토리에 존재하지만, SSTable은 ".sst" 확장자로 구분됨:

  • MANIFEST: 데이터베이스 구조 변경 로그(예: MANIFEST-000001).
  • CURRENT: 현재 MANIFEST 파일의 이름.
  • LOG: Write-Ahead Log(예: 000001.log).

5 SSTable 복원 과정

LevelDB에서 "복원(restore)"은 데이터베이스를 열 때 디스크에 저장된 상태를 재구성하는 과정을 의미함. LevelDB 는 데이터가 로그로 순차저장되어있음. 이 로그를 통해 데이터를 다시 복원해야 조회가 가능함.

이는 SSTable, MANIFEST, WAL을 활용하여 수행됨. 복원 과정은 다음과 같음:

  1. CURRENT 파일 읽기
  2. CURRENT 파일은 현재 MANIFEST 파일의 이름을 저장함. (예: MANIFEST-000001)
  3. MANIFEST 파일 읽기
  4. MANIFEST 파일은 데이터베이스 구조 변경 로그임.
  5. 각 레벨에 속한 SSTable 목록, 파일 번호, 시퀀스 번호 등을 포함함.
  6. LevelDB는 MANIFEST를 파싱하여 현재 버전(Version)을 재구성함. 이는 각 레벨(0~6)의 SSTable 목록을 포함함.
  7. WAL 재생
  8. Write-Ahead Log(WAL)이 존재하면, LevelDB는 이를 재생하여 MemTable(Skip List)을 재구성함.
  9. WAL은 플러시되지 않은 쓰기 작업을 기록하며, logreader.cc를 통해 읽힘.
  10. 재생 후, MemTable은 최신 키-값 쌍을 포함하며, 이후 플러시될 수 있음.
  11. SSTable 메타데이터 로드
  12. LevelDB는 각 SSTable의 푸터를 읽어 메타인덱스와 인덱스 블록의 위치를 확인함.
  13. 인덱스 블록과 필터 블록(Bloom 필터)은 메모리에 로드되거나 캐싱되어 빠른 접근을 지원함.
  14. 데이터 블록은 디스크에 남아 있고, 필요 시 tablereader.cc를 통해 읽힘.
  15. 캐싱 및 최적화
  16. LevelDB는 TableCache를 사용해 자주 접근하는 SSTable의 메타데이터를 캐싱함.
  17. ShardedLRUCache(16개 샤드, LRU 알고리즘)를 활용해 캐시 효율성을 높임.
  18. 읽기 작업 준비:
  19. 복원 후, LevelDB는 MemTable, 불변 MemTable, 그리고 SSTable을 순차적으로 확인하여 읽기 요청을 처리함.
  20. Bloom 필터는 키가 SSTable에 없는 경우 디스크 읽기를 건너뛰어 성능을 향상시킴.

이 과정은 dbimpl.cc와 versionset.cc에서 구현되고, 데이터베이스 상태를 빠르게 복원하여 읽기/쓰기 작업을 지원함.

6 SSTable 읽기 과정

복원을 완료하면 SSTable 파일을 읽어 데이터를 조회함.

  1. 푸터 읽기:
    • 파일 끝에서 40바이트 푸터를 읽어 메타인덱스와 인덱스 블록의 BlockHandle을 확인함.
  2. 인덱스 블록 로드:
    • 인덱스 블록을 읽어 이진 탐색으로 목표 키가 포함될 가능성이 있는 데이터 블록을 식별함.
  3. 필터 블록 확인:
    • Bloom 필터를 통해 키가 SSTable에 존재할 가능성을 확인함.
  4. 데이터 블록 읽기:
    • 식별된 데이터 블록을 읽어 목표 키-값 쌍을 검색함.
    • 블록이 압축된 경우, block.cc를 통해 압축 해제됨.
    • 키가 없으면 디스크 읽기를 건너뜀.

데이터 블록 읽기: 식별된 데이터 블록을 읽어 목표 키-값 쌍을 검색함. 블록이 압축된 경우, block.cc를 통해 압축 해제됨.

이 과정은 tablereader.cc와 block.cc에서 구현되고, 캐싱과 Bloom 필터를 활용해 효율성을 극대화함.

7 참고

Date: 2025-04-25 Fri 00:00

Author: Younghwan Nam

Created: 2025-04-26 Sat 03:48

Emacs 27.2 (Org mode 9.4.4)

Validate