세그먼트
세그먼트 (Segment)
Apache® Druid는 데이터와 인덱스를 시간 단위로 분할된 *세그먼트 파일(segment files)*에 저장해요. Druid는 데이터가 들어 있는 각 세그먼트 인터벌마다 세그먼트를 하나씩 만들지요. 인터벌이 비어 있으면, 그러니까 행이 하나도 없으면 그 시간 인터벌의 세그먼트는 만들어지지 않아요. 같은 인터벌에 서로 다른 수집 잡으로 데이터를 넣으면 여러 세그먼트가 생길 수도 있어요. 이들을 인터벌당 세그먼트 하나로 합치려는 과정이 Compaction이에요.
본문
시간 인터벌은 granularitySpec의 segmentGranularity 파라미터로 설정할 수 있어요.
Druid가 무거운 쿼리 부하에서 제대로 동작하려면 세그먼트 파일 크기를 권장 범위인 300–700 MB 안으로 유지하는 게 중요해요. 세그먼트 파일이 이 범위보다 커지면 세그먼트 시간 인터벌의 granularity를 바꾸거나 데이터를 파티셔닝하고, partitionsSpec의 targetRowsPerSegment를 조정해 보세요. 이 파라미터의 좋은 시작점은 500만 행이에요. 더 자세한 기준은 아래 Sharding 절과 Batch ingestion 문서의 "Partitioning specification" 절을 참고하세요.
세그먼트 파일 구조
세그먼트 파일은 *컬럼 기반(columnar)*이에요. 각 컬럼의 데이터가 별도의 데이터 구조로 펼쳐져 있죠. 컬럼을 분리해서 저장하면 쿼리에 실제로 필요한 컬럼만 스캔하므로 쿼리 레이턴시를 낮춰요. 컬럼 타입은 크게 세 가지 — timestamp, dimension, metric — 로 나눠요.
timestamp와 metric 타입 컬럼은 정수나 실수 값의 배열이고 LZ4로 압축돼요. 쿼리가 선택할 행을 정하면 그 컬럼들을 압축 해제하고, 관련 행을 뽑아 원하는 집계 연산을 적용하지요. 쿼리가 컬럼을 필요로 하지 않으면 Druid는 그 컬럼의 데이터를 아예 건너뜁니다.
Dimension 컬럼은 필터와 group-by 연산을 지원해야 해서 조금 달라요. 그래서 각 dimension은 다음 세 가지 데이터 구조를 필요로 해요.
- Dictionary: 값(항상 문자열로 취급)을 정수 ID로 매핑해 목록·비트맵 값을 컴팩트하게 표현해요.
- List: dictionary로 인코딩된 컬럼의 값들이에요. GroupBy·TopN 쿼리에 필요해요. 이 연산들은 필터 기반 집계만 하는 쿼리가 값 목록에 접근하지 않아도 되게 해주죠.
- Bitmap: 컬럼의 각 고유 값마다 비트맵 하나가 있어 그 값을 가진 행을 표시해요. AND·OR 연산을 빠르게 적용하기 좋아서 빠른 필터링에 유리해요. 역인덱스(inverted index)라고도 불러요.
이 구조의 감을 잡기 위해 예시 데이터의 "Page" 컬럼을 표현한 아래 구조를 볼게요.
1: Dictionary
{
"Justin Bieber": 0,
"Ke$ha": 1
}
2: List of column data
[0,
0,
1,
1]
3: Bitmaps
value="Justin Bieber": [1,1,0,0]
value="Ke$ha": [0,0,1,1]
여기서 비트맵은 dictionary나 list와 다르다는 점을 주목해 주세요. dictionary와 list는 데이터 크기에 비례해 선형으로 커지지만, 비트맵 구간의 크기는 데이터 크기와 컬럼 기수성의 곱이에요. 즉, 컬럼 값마다 비트맵이 하나씩 있고, 같은 값을 가진 컬럼끼리 같은 비트맵을 공유하지요.
컬럼 데이터 목록의 각 행은 non-zero 항목이 하나뿐인 비트맵을 가져요. 덕분에 고기수성 컬럼의 비트맵은 매우 희소하고, 따라서 압축률이 아주 높아져요. Druid는 이런 특성을 Roaring 비트맵 압축처럼 비트맵에 특화된 압축 알고리즘으로 활용합니다.
null 값 처리
문자열 컬럼은 어떤 행에나 null이 있으면 항상 그 null 값을 id 0, 즉 값 dictionary의 첫 번째 자리로 저장하고, null 값을 필터링하는 데 쓰는 비트맵 값 인덱스 항목도 연결해 둬요. 숫자 컬럼도 null 값 행을 표시하는 null 비트맵 인덱스를 저장해서, 집계의 null 체크와 null 값 매칭 필터에 사용합니다.
스키마가 다른 세그먼트
같은 datasource의 Druid 세그먼트는 서로 다른 스키마를 가질 수 있어요. 문자열 컬럼(dimension)이 한 세그먼트엔 있는데 다른 세그먼트엔 없어도, 두 세그먼트를 모두 포함한 쿼리는 정상 동작해요. 기본 모드에서는 dimension이 없는 세그먼트가 마치 그 dimension이 빈 값만 가진 것처럼 쿼리돼요. SQL 호환 모드에서는 dimension이 없는 세그먼트가 null 값만 가진 것처럼 동작하지요. 비슷하게 숫자 컬럼(metric)이 한 세그먼트엔 있고 다른 세그먼트엔 없어도, metric이 없는 세그먼트 쿼리는 보통 예상대로 동작해요. 빠진 metric에 대한 집계는 그 metric이 없는 것처럼 이루어집니다.
컬럼 형식
각 컬럼은 두 부분으로 저장돼요.
- Jackson 직렬화된
ColumnDescriptor. - 컬럼의 이진 데이터.
ColumnDescriptor는 Druid 내부의 ColumnDescriptor 클래스의 Jackson 직렬화 인스턴스예요. Jackson의 다형적 역직렬화를 활용해 코드 영향은 최소로 하면서 새롭고 유용한 직렬화 방식을 추가할 수 있게 해주죠. 컬럼의 메타데이터 일부(예: 타입, 멀티값 여부)와 나머지 이진 데이터를 역직렬화할 직렬화/역직렬화 로직 목록으로 구성돼요.
멀티값 컬럼 (Multi-value columns)
멀티값 컬럼은 한 행이 컬럼에 여러 문자열을 담을 수 있게 해요. 문자열 배열이라고 생각하면 돼요. datasource가 멀티값 컬럼을 쓰면 세그먼트 파일 안의 데이터 구조가 조금 달라 보입니다. 위 예시에서 두 번째 행이 Ke$ha 와 Justin Bieber 토픽을 모두 달고 있다고 상상해 볼게요.
1: Dictionary
{
"Justin Bieber": 0,
"Ke$ha": 1
}
2: List of column data
[0,
[0,1], <--Row value in a multi-value column can contain an array of values
1,
1]
3: Bitmaps
value="Justin Bieber": [1,1,0,0]
value="Ke$ha": [0,1,1,1]
^
|
|
Multi-value column contains multiple non-zero entries
컬럼 데이터 목록의 두 번째 행과 Ke$ha 비트맵이 바뀐 것에 주목해 주세요. 한 행이 컬럼에 값 여러 개를 가지면 그 항목은 값의 배열이 돼요. 그리고 목록에서 값이 n개인 행은 비트맵에 n개의 non-zero 항목을 가지지요.
압축
Druid는 string, long, float, double 컬럼의 값 블록을 기본적으로 LZ4로 압축해요. 문자열 컬럼과 숫자 null 값의 비트맵은 Roaring으로 압축하지요. 특별히 데이터·쿼리 패턴으로 실험해 보지 않았다면 이 기본값을 쓰는 걸 권장해요. 일반적이지 않은 옵션이 내 경우에 더 좋을 거라 판단될 때만 바꾸는 게 좋습니다.
Druid는 Concise 비트맵 압축도 지원해요. 문자열 컬럼 비트맵에서 Roaring과 Concise의 차이는 고기수성 컬럼에서 가장 두드러져요. 이 경우 Roaring이 많은 값을 매칭하는 필터에서 훨씬 빠르지만, Roaring 형식의 오버헤드 때문에 경우에 따라 Concise가 더 적은 공간을 쓸 수 있어요(그래도 값이 많이 매칭될 때는 여전히 느립니다). 압축은 컬럼 단위가 아니라 세그먼트 단위로 설정해요. 자세한 건 IndexSpec을 참고하세요.
세그먼트 식별
세그먼트 식별자(identifier)는 보통 세그먼트의 datasource, 인터벌 시작 시각(ISO 8601 형식), 인터벌 종료 시각(ISO 8601 형식), 버전 정보를 담아요. 데이터가 시간 범위를 넘어 추가로 shard되면 세그먼트 식별자에 파티션 번호도 들어갑니다.
datasource_intervalStart_intervalEnd_version_partitionNum
세그먼트 ID 예시
다음 세그먼트들의 파티션 번호가 커지는 것은 같은 인터벌에 세그먼트가 여러 개 존재한다는 뜻이에요.
foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-01/2015-01-02_v1_1
foo_2015-01-01/2015-01-02_v1_2
새 스키마로 데이터를 재인덱싱하면 Druid는 새로 만든 세그먼트에 새 버전 ID를 부여해요.
foo_2015-01-01/2015-01-02_v2_0
foo_2015-01-01/2015-01-02_v2_1
foo_2015-01-01/2015-01-02_v2_2
샤딩 (Sharding)
단일 시간 인터벌과 datasource에 대해 세그먼트가 여러 개 존재할 수 있어요. 이 세그먼트들은 인터벌에 대한 block을 형성하지요. 데이터를 샤딩할 때 쓴 shardSpec 유형에 따라, Druid 쿼리는 block이 완전해야만 완료될 수 있어요. 예를 들어 block이 다음 세 세그먼트로 구성돼 있다고 볼게요.
sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_0
sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_1
sampleData_2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z_v1_2
2011-01-01T02:00:00:00Z_2011-01-01T03:00:00:00Z 인터벌 쿼리가 완료되려면 세 세그먼트가 모두 로드되어야 해요.
선형(linear) shard spec은 이 규칙의 예외예요. 선형 shard spec은 "완전성"을 강제하지 않아서 shard가 완전히 로드되지 않아도 쿼리가 완료될 수 있지요.
예를 들어 실시간 수집으로 선형 shard spec으로 샤딩된 세그먼트 세 개를 만들었는데 그중 두 개만 로드됐다면, 쿼리는 그 두 세그먼트에 대한 결과를 반환해요.
세그먼트 구성 요소
세그먼트는 여러 파일을 담아요.
-
version.bin현재 세그먼트 버전을 정수 4바이트로 나타내요. 예를 들어 v9 세그먼트의 버전은 0x0, 0x0, 0x0, 0x9예요.
-
meta.smoosh다른
smoosh파일 내용(파일명·오프셋)에 대한 메타데이터를 담은 파일이에요. -
XXXXX.smooshSmoosh(
.smoosh) 파일은 연결된 이진 데이터를 담아요. 이 파일 통합 덕분에 데이터 접근 시 열어야 하는 파일 디스크립터 수가 줄어들지요. 자바의 메모리 매핑ByteBuffer한도 안에 들도록 파일은 2 GB 이하로 유지돼요. Smoosh 파일은 다음을 포함해요:- 데이터의 각 컬럼 파일 하나씩. 세그먼트 timestamp를 가리키는
__time컬럼용 파일도 포함돼요. - 추가 세그먼트 메타데이터를 담은
index.drd파일.
- 데이터의 각 컬럼 파일 하나씩. 세그먼트 timestamp를 가리키는
코드베이스에서 세그먼트는 내부 형식 버전을 가져요. 현재 세그먼트 형식 버전은 v9예요.
세그먼트 업데이트의 영향
Druid는 버전 관리를 이용해 업데이트를 다루며 일종의 다중 버전 동시성 제어(MVCC)를 만들어요. 이 MVCC 버전은 위에서 설명한 세그먼트 형식 버전과는 별개입니다.
여러 세그먼트 인터벌을 가로지르는 업데이트는 각 인터벌 안에서만 원자적이란 점을 주의하세요. 업데이트 전체에 걸쳐 원자적이진 않아요. 예를 들어 다음 세그먼트를 갖고 있다고 할게요.
foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-02/2015-01-03_v1_1
foo_2015-01-03/2015-01-04_v1_2
v2 세그먼트는 만들어지자마자 클러스터에 로드되어, 겹치는 시간 동안 v1 세그먼트를 대체해요. v2 세그먼트가 완전히 로드되기 전까진 클러스터에 v1과 v2 세그먼트가 섞여 있을 수 있어요.
foo_2015-01-01/2015-01-02_v1_0
foo_2015-01-02/2015-01-03_v2_1
foo_2015-01-03/2015-01-04_v1_2
이 경우 쿼리가 v1과 v2 세그먼트를 섞어 맞닥뜨릴 수 있어요.
더 알아보기
- Compaction — 인터벌당 여러 세그먼트를 하나로 합치는 과정
- Batch ingestion — 파티셔닝·
partitionsSpec가이드 - IndexSpec — 세그먼트 레벨 압축 설정