시계열DB를 밑바닥부터 만들어보기
(nakabonne.dev)- Go로 작성되어 있지만 거의 언어 무관
- 시계열 데이터는 타임스탬프가 붙은 여러 값의 컬렉션. 각 항목은 데이터 포인트
ㅤ→ 양이 많음. 많아야 의미를 가짐. 초당 백만번씩 캡쳐링 되는 경우도 많음
ㅤ→ Append-only, 시간순 정렬, 최근 데이터 우선
ㅤ→ 특정 시간단위 벌크 리딩
ㅤ→ High Cardinality (집합의 단위가 매우 큼)
ㅤ→ 대부분 최근 데이터를 읽어서 사용
- 시계열 데이터는 주로 쓰기가 많은 것을 반영한 TStorage DB엔진 라이브러리를 Go 언어로 개발
- 데이터 모델
ㅤ→ 선형 데이터 모델
ㅤ→ 데이터 포인트를 시간단위로 파티셔닝
ㅤ→ 각 파티션은 해당 시간내의 모든 데이터를 가진 별도의 독립적인 DB처럼 동작
ㅤ→ 헤드 와 그 다음 파티션만 Heap에 저장되는 메모리 파티션으로 수정 가능
ㅤ→ 데이터 손실을 막기 위해 쓰기전에 WAL(Write Ahead Log)에 작성
ㅤ→ 그 이전 파티션 데이터들은 디스크에 싱글 파일로 저장. 디스크 파티션들은 읽기 전용
- 메모리 파티션
ㅤ→ 데이터 포인트들의 리스트가 힙상에 배열로 표시 (Go 의 Slice 와 비슷)
ㅤ→ 레이턴시 및 동기화 때문에 Out-of-order 가 자주 발생. 같은 파티션 내라면 버퍼링을 통해서 저장할때 재 정렬, 다른 파티션이라면 헤드가 아닌 이전 파티션 뒤에 추가하는 것으로 가능
ㅤ→ WAL 에 실제로 기록되는 것과 똑같은 데이터를 저장해서, 오류시에도 복구 가능하게
- 디스크 파티션
ㅤ→ 파티션당 한개의 디렉토리에 메타 데이터와 압축된 실제 데이터를 저장 (Prometheus V3 Storage 의 축소판)
ㅤ→ Memory-Mapped 데이터 형식(커널에서 mmap 으로 캐쉬가능)
ㅤ→ 메타데이터는 JSON 형식으로 인덱스를 형성
- 타임스탬프와 밸류 튜플로 표현되는 데이터 인코딩은 페이스북의 Gorilla 논문에서 제안된 인코딩 방식을 사용
ㅤ→ 타임스탬프와 밸류를 서로 다른 메소드로 인코딩
ㅤ→ timestamp 는 unsigned 64-bit integer 값으로 Delta-of-delta 인코딩을 이용
ㅤㅤ✓ Delta 인코딩 : 기존값과 현재값의 차이만 기록하는 방식
ㅤㅤ✓ Delta-of-Delta 인코딩 : 일반적으로 특정 시간당 생기므로 델타의 델타만 기록
ㅤㅤ✓ 가변 길이로 인코딩 되므로 Delta-of-Delta 가 가장 작은 공간을 사용
ㅤ→ values 는 signed 64-bit floating-point 값으로 XOR 인코딩 을 사용
ㅤㅤ✓ 처음 값은 그냥 저장
ㅤㅤ✓ 다음 값을 XOR 해서 0이면 기존 값과 같으므로 0 비트 하나만 저장
ㅤㅤ✓ 0이 아니면 다른 비트들 기반으로 계산(Meaningful Bit)
ㅤㅤ✓ 앞/뒤의 0들을 계산해서, 0의 갯수가 같다면 0과 의미있는 비트만 저장, 다르면 리딩 제로 갯수, Meaningful Bit의 갯수과 그 자체를 저장