Time series OLAP – Druid Segment(4)
Druid - 새로운 희망
서투른 장인은 항상 연장 나무란다.
A bad workman always blame his tools. (속담)
앞선 시리즈에서 자주 언급되었던 Druid의 저장 단위인 Segment와 Druid에서 지원하는 Query에 대해 다루려고 한다. Segment는 Druid의 time기준으로 저장되는 indexing 파일 단위를 의미하며 저장 단위는 사용자 쿼리에 따라 최적화가 가능하다.
저장하려고 하는 데이터의 수집시 indexing spec에 정의된 granularitySepc에 segmentGranularity에 따라 저장단위가 결정된다. 예를 들면 segmentGranulality가 HOUR 라면 인덱싱은 시간별로 segment file(shard)이 생성된다. Druid에서 권장되는 segment size는 300MB~700MB사이이다. 이 범위보다 segment file이 더 커지는 경우 sharding이 필요하다. segment파일이 큰 경우라면 Druid적재시 ingestion spec에 partitioningSpec에 해당하는 targetPartitionSize등을 고려해야 한다.
segment file의 데이터 구조는 columnar이다. 위의 그림에서 보듯이 타임스탬프, 디멘전, 메트릭이 druid의 기본 컬럼 타입이다.
1. Timestamp column
- 시간 기준 축으로 질의가 가능하며 druid는 하나의 timestamp 필드만 가질 수 있다.
2. Dimensions columns
- 이벤트의 문자열 속성(현재까지는 문자만 지원) 하며 필터 조건 및 groupBy의 집계 조건으로 사용 될 수 있다. 위의 데이터에서 Page별 Username별, Gender별, City별 집계의 기준이 될 수 있다.
3. Metrics columns
- 집계와 계산에 사용되는 컬럼으로 숫자형 데이터만 허용하며 min/max/sum/count등의 연산이 가능하다.
Segment File의 해부
druid의 segment는 deep storage설정에 따라 hdfs혹은 s3에 ingestion시 지정한 segmentGranularity 단위로 저장된다. 가령 deep storage의 path가 /druid/storage라고 지정되어 있는경우 해당 디렉토리 아래로 데이터 소스이름으로 폴더가 생성된다. 하나의 segment에 대해 파일은 다음과 같다.
[참고 : http://druid.io/docs/latest/design/segments.html]
descriptor.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
{ "dataSource":"pageView", "interval":"2016-01-01T00:00:00.000Z/2016-01-02T00:00:00.000Z", "version":"2016-08-26T09:12:03.309Z", "loadSpec":{ "type":"hdfs", "path":"hdfs://nmcluster/druid/storage/pageView/20160101T000000.000Z_20160102T000000.000Z/2016-08-26T09_12_03.309Z/0/index.zip" }, "dimensions":"timestamp,userid, username,group,communityid", "metrics":"count,sum,min,max", "shardSpec":{ "type":"none" }, "binaryVersion":9, "size":211116494, "identifier":"fpageView_2016-01-01T00:00:00.000Z_2016-01-02T00:00:00.000Z_2016-08-26T09:12:03.309Z" }
해당 segment에 대한 정보를 담고 있으며 dimension, metric field에 대한 내용을 포함하고 있다.
version.bin : integer형으로 segment의 버전을 나타내는 4byte이다.
meta.smoosh : 다른 smoosh file의 컨텐츠에 대한 메타데이터(컬럼 이름과 offset)파일이다.
meta.smoosh
1 2 3 4 5 6 7 8 9 10 11 12
v1,2147483647,1 __time,0,0,1255 user_id,0,210018645,210019145 locale,0,209412931,209528395 count,0,1255,18361105 create_date,0,210018023,210018645 date,0,209152116,209152625 end_date,0,209576508,209973743 escrow,0,209153127,209153632 country,0,210019145,211113711 device,0,209340145,209412931 gender,0,209528395,209576508
XXXX.smoosh
indexing 된 바이너리 파일이다. 각 파일은 최대 2GB(자바에서의 ByteBuffer의 메모리 제한)이다.
Druid Query
HTTP REST방식으로 broker에게 json format으로 요청하게 된다.
curl -X POST '<queryable_host>:<port>/druid/v2/?pretty' -H 'Content-Type:application/json' -d @<query_json_file>
query_json_file에 질의와 관련된 json spec을 생성하면 된다.
druid에서 지원하는 쿼리 type은 다음과 같다.
[참고 : http://druid.io/docs/latest/querying/querying.html]
Aggregation Queries
- Timeseries
- TopN
- GroupBy
Metadata Queries
- Time Boundary
- Segment Metadata
- Datasource Metadata
Search Queries
- Search
timeseries 쿼리의 경우 dimension의 grouping을 필요로 하지 않는 집계 쿼리의 경우 groupBy 쿼리 보다 더 빠르다. 또한, 단일 dimension의 grouping과 sorting만 지원해도 되는 경우라면 groupBy 쿼리 보다 topN 쿼리가 더 최적화 되어있다.
- Aggregation Queries
Timeseries
- 요청된 time의 granularity로 aggregation되어 결과가 리턴된다.
dimension별 groupBy없이 time range 기반의 groupBy 쿼리라면 이 쿼리를 사용하면 된다. 다음 쿼리는 users_event 데이터 소스에서 country와 gender필드 값으로 필터링 하고 event를 시간대별로 users 평균을 집계하는 쿼리이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
{ "queryType":"timeseries", "dataSource":"users_event", "granularity":"hour", "descending":"true", "filter":{ "type":"and", "fields":[ { "type":"selector", "dimension":"country", "value":"US" }, { "type":"or", "fields":[ { "type":"selector", "dimension":"gender", "value":"M" }, { "type":"selector", "dimension":"gender", "value":"F" } ] } ] }, "aggregations":[ { "type":"count", "name":"rows" }, { "type":"hyperUnique", "name":"user_unique", "fieldName":"user_unique" } ], "postAggregations":[ { "type":"arithmetic", "name":"average_users_per_event", "fn":"/", "fields":[ { "type":"hyperUniqueCardinality", "fieldName":"user_unique" }, { "type":"fieldAccess", "name":"rows", "fieldName":"rows" } ] } ], "intervals":[ "2016-05-01T00:00:00.000/2016-05-02T00:00:00.000" ] }
응답 결과는 다음과 같으며 granularity 조건이 hour이기 때문에 다음과 같이 hour단위로 집계되어 결과가 나타난다. 시간대별로 event에 logging된 user의 unique value를 구할 수 있다.
1 2 3 4 5 6 7 8 9 10
[ { "timestamp": "2015-05-01T01:00:00.000Z", "result": { "user_unique": 95.17775613938109, "average_users_per_event": 0.015211404209586237, "rows": 6257 } } ]
TopN
- ordering spec을 가진 단일 dimension에 대한 approximate groupByQuery를 지원한다.
- groupBy질의시 postAggregations을 사용할 수 있으며 threshold를 지정하여 정해진 N개 만큼 가져올 수 있다.
- Arithmetic post-aggregator 를 사용하면 수식 연산으로 집계가 가능하다.
다음 예제 쿼리는 single dimension을 지정하여 여기서는 locale기준으로 top5쿼리를 가져오는 예제이다. threshold는 필수로 지정해야 하며 max값은 1000이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
{ "queryType": "topN", "dimension": "locale", "threshold": 5, "metric": "count", "dataSource": "user_event", "granularity": "all", "filter": { "type": "and", "fields": [ { "type": "selector", "dimension": "country", "value": "1" }, { "type": "or", "fields": [ { "type": "selector", "dimension": "gender", "value": "M" }, { "type": "selector", "dimension": "gender", "value": "F" } ] } ] }, ... 이후 생략
결과는 아래와 같이 locale별로 top5결과가 sorting되어 리턴된다.
1 2 3 4 5 6
{ "count": 6155, "user_unique": 94.13045363637423, "average_users_per_event": 0.015293331216307755, "locale": "EN" },
GroupBy
- dimension을 array형태로 줄 수 있으며 위의 예제의 경우
"dimensions": ["locale","gender"] 로 dimension을 정의하면 다음과 같은 결과를 얻을 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
{ "version": "v1", "timestamp": "2015-05-01T01:00:00.000Z", "event": { "locale": "EN", "gender": "F", "count": 33, "user_unique": 24.14173338018237, "average_users_per_event": 0.7315676781873446 } }, { "version": "v1", "timestamp": "2015-05-01T01:00:00.000Z", "event": { "locale": "EN", "gender": "M", "count": 7, "user_unique": 6.008806266444944, "average_users_per_event": 0.8584008952064206 } },
- Metadata Queries
Time Boundary
- 데이터 셋의 minTime과 maxTime을 리턴한다.
1 2 3 4 5 6 7 8 9
[ { "timestamp": "2015-05-01T00:38:34.000Z", "result": { "maxTime": "2015-05-01T06:58:38.000Z", "minTime": "2015-01-01T00:38:34.000Z" } } ]
Segment Metadata
- segment별 컬럼의 cardinality, segment의 min/max값 등을 가져올 수 있다. 다음은 segment metadata쿼리 타입으로 질의한 결과이다. register_time dimension 필드에 대한 정보를 보여주고 있다.
1 2 3 4 5 6 7 8 9
"register_time": { "type": "STRING", "hasMultipleValues": false, "size": 665310, "cardinality": 5048, "minValue": "2016-01-01 00:01:07.3", "maxValue": "2016-01-02 00:00:06.7", "errorMessage": null },
Datasource Metada
- 데이터 소스의메타 정보를 가져올 수 있다. 메타정보에는 데이터 소스의 마지막 수집된 이벤트의 타임스탬프이다.
- Search Queries
Search
- 검색 spec에 match하는 dimension 정보를 리턴한다. 코드 특성을 가지는 dimension의 count를 확인할 때 유용하다. 예를 들어 위의 데이터에서 Gender별로 분포를 알고 싶을때 사용한다. 다음의 쿼리는 user_event에서 locale에 "EN"이 포함된 데이터를 검색하는 예제이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
{ "queryType": "search", "dataSource": "user_event", "granularity": "day", "searchDimensions": [ "locale" ], "query": { "type": "insensitive_contains", "value": "EN" }, "sort" : { "type": "lexicographic" }, "intervals": [ "2015-05-01T00:00:00.000/2015-05-02T00:00:00.000" ] }
timestamp구간 위의 예제에서는 day별로 결과셋은 다음과 같다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
{ "dimension": "locale", "value": "EN", "count": 330 }, { "dimension": "locale", "value": "EN-AU", "count": 77 }, { "dimension": "locale", "value": "EN-CA", "count": 11 }, { "dimension": "locale", "value": "EN-GB", "count": 262 }, { "dimension": "locale", "value": "EN-US", "count": 134992 }
- Select Queries
- pagination(limit 몇 건)을 지원하는 쿼리로 druid의 raw데이터 값을 확인할때 사용된다.
다음 글에서는 Druid의 실시간과 배치가 적용된 lambda Architecture 에 대해 살펴볼 예정이다.
연재 순서는 : Druid 입문(1) -> 실시간 Ingestion(2) -> Batch Ingestion(3) -> Segment deep dive(4) -> Glue Architecture(5) -> Trouble Shooting(6)
TO BE CONTINUED