스토리지 개요
스토리지 개요 (Storage overview)
Druid는 전통적 RDBMS의 테이블과 유사한 데이터소스에 데이터를 저장해요. 각 데이터소스는 시간으로 파티셔닝되고, 선택적으로 다른 속성으로 더 파티셔닝돼요. 데이터는 segment로 저장돼요.
출처: 문서
본문
Druid는 전통적 RDBMS의 테이블과 유사한 데이터소스에 데이터를 저장해요. 각 데이터소스는 시간으로 파티셔닝되고, 선택적으로 다른 속성으로 더 파티셔닝돼요. 각 시간 범위는 chunk라고 불러요(예: 데이터소스가 일 단위로 파티셔닝되면 하루). chunk 안에서 데이터는 하나 이상의 segment로 파티셔닝돼요. 각 segment는 단일 파일이며, 보통 수백만 행 이하의 데이터로 구성돼요. segment는 시간 chunk로 구성되므로, segment를 다음처럼 타임라인 위에 살아 있는 것으로 생각하는 것이 도움이 될 때가 있어요.
데이터소스는 몇 개의 segment에서 수십만, 심지어 수백만 개까지 어떤 수든 가질 수 있어요. 각 segment는 Middle Manager가 변경 가능하고 커밋되지 않은 상태로 생성해요. 데이터는 커밋되지 않은 segment에 추가되는 즉시 쿼리할 수 있어요. segment 생성 과정은 컴팩트하고 색인된 데이터 파일을 만들어 이후 쿼리를 가속화해요:
-
컬럼형(columnar) 형식으로 변환
-
비트맵 색인으로 인덱싱
-
압축
- String 컬럼에 대한 ID 저장을 최소화하는 Dictionary encoding
- 비트맵 색인용 비트맵 압축
- 모든 컬럼에 대한 타입 인지형 압축
주기적으로 segment는 커밋되어 deep storage에 게시되고, 불변 상태가 되며, Middle Manager에서 Historical 서비스로 이동해요. segment에 대한 항목도 metadata store에 기록돼요. 이 항목은 segment의 스키마, 크기, deep storage에서의 위치 같은 segment에 대한 자기 기술적(self-describing) 메타데이터 비트예요. 이 항목들은 Coordinator에 클러스터에서 어떤 데이터가 이용 가능한지 알려줘요.
segment 파일 형식에 대한 자세한 내용은 segment files를 참고하세요.
Druid에서 데이터를 모델링하는 자세한 방법은 schema design을 참고하세요.
Indexing과 handoff
Indexing은 새 segment가 생성되는 메커니즘이고, handoff는 segment가 게시되어 Historical 서비스가 서비스하는 메커니즘이에요.
Indexing 쪽에서:
- indexing task가 시작되어 새 segment를 만들기 시작해요. segment를 만들기 시작하기 전에 segment의 식별자를 결정해야 해요. append하는 task(Kafka task나 append 모드의 index task 같은)의 경우, 기존 segment 집합에 파티션을 추가할 수 있는지 Overlord의 "allocate" API를 호출해서 결정해요. 덮어쓰는 task(append 모드가 아닌 index task)의 경우 interval을 잠그고 새 버전 번호와 새 segment 집합을 만들어서 결정해요.
- indexing task가 realtime task(예: Kafka task)라면 segment는 이 시점에 즉시 쿼리 가능해요. 이용 가능하지만 게시되지 않은 상태예요.
- indexing task가 segment에 대한 데이터 읽기를 끝내면, deep storage로 밀어 넣고 metadata store에 레코드를 써서 게시해요.
- indexing task가 realtime task라면 데이터가 쿼리에 계속 이용 가능하도록 Historical 서비스가 segment를 로드할 때까지 기다려요. realtime task가 아니라면 즉시 종료해요.
Coordinator / Historical 쪽에서:
- Coordinator는 주기적으로(기본적으로 1분마다) metadata store에서 새로 게시된 segment를 폴링해요.
- Coordinator가 게시되고 사용되지만 이용 불가능한 segment를 찾으면, 그 segment를 로드할 Historical 서비스를 선택해 로드하라고 지시해요.
- Historical이 segment를 로드하고 서비스를 시작해요.
- 이 시점에 indexing task가 handoff를 기다리고 있었다면 종료해요.
Segment 식별자
모든 segment는 다음 컴포넌트로 구성된 네 부분 식별자를 가져요:
- 데이터소스 이름
- segment를 포함한 시간 chunk의 시간 interval. 이는 ingestion 시 지정한
segmentGranularity에 해당하며 query granularity와 같은 형식을 사용해요. - 버전 번호(일반적으로 segment 집합이 처음 시작된 시점에 해당하는 ISO8601 타임스탬프)
- 파티션 번호(데이터소스+interval+버전 내에서 고유한 정수. 연속적이지 않을 수 있어요.)
예를 들어, 데이터소스 clarity-cloud0, 시간 chunk 2018-05-21T16:00:00.000Z/2018-05-21T17:00:00.000Z, 버전 2018-05-21T15:56:09.909Z, 파티션 번호 1인 segment의 식별자는 다음과 같아요:
clarity-cloud0_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T15:56:09.909Z_1
파티션 번호 0인 segment(chunk의 첫 번째 파티션)는 파티션 번호를 생략해요. 다음 예시는 앞선 예시와 같은 시간 chunk의 segment이지만 파티션 번호가 1이 아닌 0인 경우예요:
clarity-cloud0_2018-05-21T16:00:00.000Z_2018-05-21T17:00:00.000Z_2018-05-21T15:56:09.909Z
Segment 버전 관리
버전 번호는 배치 모드 덮어쓰기를 지원하는 multi-version concurrency control (MVCC) 형태를 제공해요. 데이터를 append만 한다면 각 시간 chunk마다 버전은 하나뿐이에요. 하지만 데이터를 덮어쓰면 Druid는 기존 버전을 쿼리하는 것에서 새로 업데이트된 버전을 쿼리하는 것으로 끊김 없이 전환해요. 구체적으로, 같은 데이터소스·같은 시간 interval이지만 더 높은 버전 번호를 가진 새 segment 집합이 생성돼요. 이것은 나머지 Druid 시스템에 기존 버전은 클러스터에서 제거되어야 하고 새 버전이 이를 대체해야 한다는 신호예요.
Druid는 먼저 새 데이터를 로드하고(쿼리되지는 않게), 새 데이터가 모두 로드되는 즉시 모든 새 쿼리가 그 새 segment를 사용하도록 전환해서 처리하기 때문에, 사용자 관점에서 전환이 순간적으로 일어나는 것처럼 보여요. 그런 다음 몇 분 후 기존 segment를 버려요.
Segment 수명주기
각 segment에는 다음 세 가지 주요 영역과 관련된 수명주기가 있어요:
- Metadata store: segment가 구축을 완료하면 segment 메타데이터(보통 몇 KB 이하의 작은 JSON 페이로드)가 metadata store에 저장돼요. segment에 대한 레코드를 metadata store에 삽입하는 행위를 publishing이라고 해요. 이 메타데이터 레코드에는
used라는 부울 플래그가 있는데, segment를 쿼리 가능하게 할지 여부를 제어해요. realtime task가 만든 segment는 게시되기 전에 이용 가능한데, segment가 완료되고 추가 행을 받지 않을 때만 게시되기 때문이에요. - Deep storage: segment 데이터 파일은 구축이 완료되면 deep storage로 밀어 넣어져요. 이는 metadata store에 메타데이터를 게시하기 바로 직전에 일어나요.
- 쿼리 가능 여부: segment는 realtime task, deep storage에서 직접, 또는 Historical 서비스 같은 일부 Druid 데이터 서버에서 쿼리 가능해요.
현재 활성 segment의 상태는 Druid SQL sys.segments table로 검사할 수 있어요. 다음 플래그를 포함해요:
is_published: segment 메타데이터가 metadata store에 게시되었고used가 true이면 true.is_available: segment가 현재 realtime task나 Historical 서비스에서 쿼리 가능하면 true.is_realtime: segment가 realtime task에서만 이용 가능하면 true. realtime ingestion을 사용하는 데이터소스의 경우, 일반적으로 처음에는true였다가 segment가 게시·handoff되면서false가 돼요.is_overshadowed: segment가 게시되고(used가 true) 다른 일부 게시된 segment에 완전히 overshadow되면 true. 일반적으로 일시적인 상태이며, 이 상태의 segment는 곧used플래그가 자동으로 false로 설정돼요.
가용성과 일관성
Druid는 ingestion과 querying 사이에 아키텍처적 분리가 있어요. 이는 Druid의 가용성·일관성 속성을 이해할 때 각 기능을 별도로 봐야 한다는 뜻이에요.
ingestion 쪽에서 Druid의 주요 ingestion 방법은 모두 pull 기반이고 트랜잭션 보장을 제공해요. 즉, 이 방법들을 사용한 ingestion은 all-or-nothing 방식으로 게시된다는 보장이 있어요:
- Kafka와 Kinesis 같은 supervised "seekable-stream" ingestion 방법. 이 방법들로 Druid는 같은 트랜잭션에서 segment 메타데이터와 함께 스트림 offset을 metadata store에 커밋해요. 아직 게시되지 않은 데이터의 ingestion은 ingestion task가 실패하면 롤백될 수 있다는 점에 유의하세요. 이 경우 부분적으로 ingestion된 데이터는 폐기되고, Druid는 마지막으로 커밋된 스트림 offset 집합부터 ingestion을 재개해요. 이는 exactly-once publishing 동작을 보장해요.
- SQL REPLACE 문. 컨트롤러 task가 문 실행이 끝나면 모든 segment 메타데이터를 게시해요.
- Native batch ingestion. 병렬 모드에서는 supervisor task가 하위 task가 끝난 후 단일 트랜잭션으로 모든 segment 메타데이터를 게시해요. 단순(단일 task) 모드에서는 단일 task가 완료된 후 단일 트랜잭션으로 모든 segment 메타데이터를 게시해요.
또한 일부 ingestion 방법은 idempotency 보장을 제공해요. 즉 같은 ingestion을 반복 실행해도 중복 데이터가 ingestion되지 않아요:
- Kafka와 Kinesis 같은 supervised "seekable-stream" ingestion 방법은 스트림 offset과 segment 메타데이터가 함께 저장되고 lock-step으로 업데이트되기 때문에 idempotent해요.
- SQL REPLACE 문은 ingestion 대상과 같은 Druid 데이터소스에서 읽지 않는 한 idempotent해요. 이 경우 같은 task를 두 번 실행하면 non-idempotent한데, 덮어쓰는 것이 아니라 기존 데이터에 추가하기 때문이에요.
- Native batch ingestion은
appendToExisting이 true이거나 입력 소스 중 하나가 ingestion 대상과 같은 Druid 데이터소스인 경우가 아니면 idempotent해요. 이 두 경우 중 하나라도 해당하면 같은 task를 두 번 실행하면 non-idempotent한데, 덮어쓰는 것이 아니라 기존 데이터에 추가하기 때문이에요.
쿼리 쪽에서 Druid Broker는 주어진 쿼리에 일관된 segment 집합이 관여하도록 보장하는 역할을 담당해요. 쿼리가 시작될 때 현재 이용 가능한 것에 기반해 사용할 적절한 segment 버전 집합을 선택해요. 이것은 원자적 교체(atomic replacement)라는 기능으로 뒷받침되는데, 이 기능은 사용자 관점에서 쿼리가 기존 데이터 버전에서 새 데이터 집합으로 일관성·성능 영향 없이 순간적으로 전환되도록 보장해요.
이것은 SQL REPLACE 문, appendToExisting이 false인 native batch ingestion, 그리고 컴팩션에 사용돼요.
원자적 교체는 각 시간 chunk마다 개별적으로 일어난다는 점에 유의하세요. 배치 ingestion task나 컴팩션이 여러 시간 chunk를 포함하면, 각 시간 chunk는 task가 끝난 직후 원자적 교체를 겪지만 교체들이 모두 동시에 일어나지는 않아요.
일반적으로 Druid의 원자적 교체는 segment 버전과 함께 동작하는 core set 개념에 기반해요. 시간 chunk가 덮어써질 때 더 높은 버전 번호를 가진 새 core set의 segment가 생성돼요. Broker가 기존 집합 대신 사용하려면 core set이 모두 이용 가능해야 해요. 또한 시간 chunk당 버전당 core set은 하나만 있을 수 있어요. Druid는 시간 chunk당 한 번에 단일 버전만 사용해요. 이 속성들이 함께 Druid의 원자적 교체 보장을 제공해요.
Druid는 또한 deprecated된 segment locking 모드를 지원해요. ingestion task의 context에서 forceTimeChunkLock을 false로 설정하면 활성화돼요. 이 경우 Druid는 새 버전 번호로 새 core set을 만드는 대신 시간 chunk의 기존 버전을 사용해 원자적 업데이트 그룹(atomic update group)을 만들어요. 같은 시간 chunk에 같은 버전 번호를 가진 원자적 업데이트 그룹이 여러 개 있을 수 있어요. 각 그룹은 같은 시간 chunk에 있고 같은 버전 번호를 가진 특정 이전 segment 집합을 교체해요. Druid는 완전히 이용 가능한 최신 것을 쿼리해요. 이것은 core set 개념보다 더 강력한 버전으로, 시간 chunk의 데이터 하위 집합을 원자적으로 교체하고, 원자적 교체와 append를 동시에 하는 것을 가능하게 해요.
여러 Historical이 동시에 오프라인이 되어(복제 팩터를 초과해) segment가 이용 불가능해지면, Druid 쿼리는 여전히 이용 가능한 segment만 포함해요. 백그라운드에서 Druid는 가능한 한 빨리 이 이용 불가능한 segment를 다른 Historical에 다시 로드하며, 그 시점에 다시 쿼리에 포함돼요.
더 알아보기 (Learn more)
- Segments — segment 파일 형식과 구조를 알아봐요.
- Deep storage — segment가 저장되는 곳을 살펴봐요.
- Metadata storage — segment 메타데이터를 보관하는 저장소를 이해해요.