es学习系列之二:Aggregation collect mode and execution hint.md

如无特别说明,本文讨论的内容均基于 es 7.*

Term Aggregation的深度优先以及广度优先

Term aggregation 是我们常用的聚合查询,对于有 父子聚合 的场景下,理解其执行父聚合以及子聚合的时机对我们优化聚合查询有很大的帮助,collect mode就指定了它们的执行时机,共有两种模式,一种是depth first,另一种是breadth first。

depth first

一般来说,depth first适合大多数场景,因为大多数情况下需要聚合的字段不会是大量的唯一值。

  • 聚合过程
    • 1.先计算出一个桶,然后再根据这个桶计算出子聚合的结果,构建出一棵聚合树。然后不断重复前面的过程,直到所有的桶计算完毕。
    • 2.对步骤一计算出的结果进行排序,也就是对各个聚合树进行排序。
    • 3.根据过滤条件和 size 等参数修剪结果。
  • 适合场景
    • 大多数term都是重复的
    • 要求返回的aggs size比较大
    • 原因:不需要缓存父聚合的doc_id,直接聚合成一棵棵聚合树,每棵聚合树的每个节点数据结构为(value, doc_count),并且大多数的聚合树不会被修剪
  • 聚合计算下,多层聚合会让一个文档与其他文档产生关联,也就是说会形成一棵棵聚合树。深度优先就是先计算获得所有的聚合树,然后再进行后续处理。

breadth first

  • 聚合过程
    • 1.先计算出第一层聚合的结果。
    • 2.根据步骤一得出的结果进行排序。
    • 3.根据过滤条件和 size 等参数修整第一层节点。
    • 4.根据各个节点进行后续的子聚合计算。
  • 适合场景
    • 大多数term都是唯一的
    • 要求返回的aggs size 比较小
    • 原因:缓存父聚合的doc_id,聚合树第一层节点,即根节点,的数据结构为(value, doc_count, set(doc_id)),为了保证使用的缓存尽量小,则 avg(doc_count_per_bucket) * size(buckets) 尽量小
  • 对于基数大于请求的大小的字段或基数未知的字段(例如,数字字段或脚本),默认值为breadth_first
  • 当使用breadth_first模式时,属于最上层存储桶的文档集将被缓存以供后续重播,因此这样做会产生内存开销,该开销与匹配文档的数量成线性关系。
  • 使用广度优先设置时,仍可以使用order参数来引用子聚合中的数据。父级聚合知道需要先调用此子级聚合,然后再调用其他任何子级聚合。
    • 这里的主要意思是,如果父聚合的排序时候使用的是子聚合的结果,则会在执行父聚合前先执行该子聚合。

depth first vs. breadth first

  • 关键点:
    • 父子聚合的组合数的多少 + 返回buckets数量大小(分别对应计算的难度+使用率)
      • 父聚合字段的基数大,父子聚合的组合数多,需要返回的buckets数量小,适合广度优先
      • 父聚合字段的基数小,父子聚合的组合数少,需要返回的buckets数量大,适合深度优先
    • 比如:
      • 父聚合字段有10000,父子聚合的组合数约为 100000,需要返回buckets为5,则可能适合广度优先
      • 父聚合字段有10,父子聚合的组合数约为 100,需要返回buckets为100,则可能适合深度优先

Term Aggregation的Execution hint

global_ordinals

我们知道doc values存储的时候,会对应original value分配一个ordinal以减小磁盘的使用,也减小聚合过程中内存的使用。

  • 聚合过程
    • segment级别的聚合:
      • 在聚合字段的doc values数据结构中,完成 (doc_id, ordinal) -> (ordinal, set(doc_id)) 的聚合计算
    • shard级别的聚合:
      • 在shard内部多个segment之上构建 global ordinla map,其数据结构为 (segment_id, ordinal, global ordinal),当ordinal对应的初始值相同时,其对应的global ordinal也相同。
      • 根据 global ordinla map,完成 (ordinal, set(doc_id)) -> (global ordinal, set(doc_id) 的转换。
      • 根据 global ordinal 进行分桶,并根据doc count进行排序,选出前n个桶,完成 (global ordinal, set(doc_id) -> (global ordinal, doc count) 的聚合计算。
      • 根据 global ordinal map以及segment的doc values,把global ordinal替换成原始值,完成 (global ordinal, doc count) -> (segment_id, ordinal, doc count) -> (original value, doc count) 的转换。
    • index级别的聚合:
      • 在协调节点完成全局前n个结果的聚合。
  • global ordinals 的有效性
    • 因为global ordinals为shard上的所有segment提供了统一的map,所以当新的segment变为可见时(常见为refresh的时候),还需要完全重建它们。所以,global ordinals 更加适用于 历史数据

map

相对而言map更加简单,主要的不同点在于shard级别聚合的时候不再构建global ordinal map,而是直接返回original value到shard

  • 聚合过程
    • segment级别的聚合:
      • 在聚合字段的doc values数据结构中,完成 (doc_id, ordinal) -> (ordinal, set(doc_id)) 的聚合计算
      • 把ordinal替换成初始值,完成 (ordinal, set(doc_id)) -> (original, set(doc_id)) 的转换
    • shard级别的聚合:
      • 根据 original value进行分桶,并根据doc count进行排序,选出前n个桶,完成 (original, set(doc_id) -> (original, doc count) 的聚合计算。
    • index级别的聚合:
      • 在协调节点完成全局前n个结果的聚合。

global_ordinals vs. map

  • 适合global_ordinals模式
    • 聚合字段基数不大
    • refresh 间隔比较大
    • 不再写入的索引,比如历史索引
    • 开启 eager_global_ordinals 配置,在写入索引时就构建global ordinals map。但是可能会对写入索引的速度有影响
  • 适合map模式
    • 聚合字段基数很大
    • 需要注意可能会引起更大的内存消耗量

实战演练

冷数据的聚合

可以看到我们针对冷数据的聚合,我们在数据量接近8kw时,global ordinal的聚合速度要比map更快。

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# 2020-08-16 18:00:00 ~ 2020-08-16 21:00:00
# 接近 8kw 条数据的聚合
GET /co'd'l-2020.08.16-000120/_count
{
"query": {
"bool": {
"must": [
{
"range": {
"startTimeMillis": {
"from": 1597572000000,
"include_lower": true,
"include_upper": true,
"to": 1597582800000
}
}
},
{
"match": {
"process.serviceName": {
"query": "nginx"
}
}
}
]
}
}
}
返回:
{
"count" : 79595834,
"_shards" : {
"total" : 6,
"successful" : 6,
"skipped" : 0,
"failed" : 0
}
}

# 2020-08-16 18:00:00 ~ 2020-08-16 21:00:00
# global ordinals 模式
# 耗时:
# 17207ms
# 17089ms
# 16624ms
# 平均耗时: 16973ms
GET /cold_data-2020.08.16-000120/_search?request_cache=false
{
"aggregations": {
"requestIDs": {
"aggregations": {
"startTimeMillis": {
"max": {
"field": "startTimeMillis"
}
}
},
"terms": {
"field": "requestID",
"execution_hint": "global_ordinals",
"order": [
{
"startTimeMillis": "desc"
}
],
"size": 10
}
}
},
"query": {
"bool": {
"must": [
{
"range": {
"startTimeMillis": {
"from": 1597572000000,
"include_lower": true,
"include_upper": true,
"to": 1597582800000
}
}
},
{
"match": {
"process.serviceName": {
"query": "nginx"
}
}
}
]
}
},
"size": 0
}

# 2020-08-16 18:00:00 ~ 2020-08-16 21:00:00
# map 模式
# 耗时:
# 26452ms
# 26405ms
# 26747ms
# 平均耗时: 26534ms
GET /cold_data-2020.08.16-000120/_search?request_cache=false
{
"aggregations": {
"requestIDs": {
"aggregations": {
"startTimeMillis": {
"max": {
"field": "startTimeMillis"
}
}
},
"terms": {
"field": "requestID",
"execution_hint": "map",
"order": [
{
"startTimeMillis": "desc"
}
],
"size": 10
}
}
},
"query": {
"bool": {
"must": [
{
"range": {
"startTimeMillis": {
"from": 1597572000000,
"include_lower": true,
"include_upper": true,
"to": 1597582800000
}
}
},
{
"match": {
"process.serviceName": {
"query": "nginx"
}
}
}
]
}
},
"size": 0
}

热数据

可以看到我们针对热数据的聚合,我们在数据量接近3kw时,map的聚合速度要比global ordinla更快。

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# 2020-08-16 20:30:00 ~ 2020-08-16 21:00:00
# 接近 3kw 条数据的聚合
GET /hot_data-2020.08.16-000121/_count
{
"query": {
"bool": {
"must": [
{
"range": {
"startTimeMillis": {
"from": 1597581000000,
"include_lower": true,
"include_upper": true,
"to": 1597582800000
}
}
},
{
"match": {
"process.serviceName": {
"query": "nginx"
}
}
}
]
}
}
}
返回:
{
"count" : 31648173,
"_shards" : {
"total" : 6,
"successful" : 6,
"skipped" : 0,
"failed" : 0
}
}


# 2020-08-16 20:30:00 ~ 2020-08-16 21:00:00
# global ordinals 模式
# 耗时:
# 15256ms
# 16699ms
# 14630ms
# 平均耗时: 15528ms
GET /hot_data-2020.08.16-000121/_search?request_cache=false
{
"aggregations": {
"requestIDs": {
"aggregations": {
"startTimeMillis": {
"max": {
"field": "startTimeMillis"
}
}
},
"terms": {
"field": "requestID",
"execution_hint": "global_ordinals",
"order": [
{
"startTimeMillis": "desc"
}
],
"size": 10
}
}
},
"query": {
"bool": {
"must": [
{
"range": {
"startTimeMillis": {
"from": 1597581000000,
"include_lower": true,
"include_upper": true,
"to": 1597582800000
}
}
},
{
"match": {
"process.serviceName": {
"query": "nginx"
}
}
}
]
}
},
"size": 0
}

# 2020-08-16 20:30:00 ~ 2020-08-16 21:00:00
# map 模式
# 耗时:
# 12908ms
# 11807ms
# 10977ms
# 平均耗时: 11897ms
GET /hot_data-2020.08.16-000121/_search?request_cache=false
{
"aggregations": {
"requestIDs": {
"aggregations": {
"startTimeMillis": {
"max": {
"field": "startTimeMillis"
}
}
},
"terms": {
"field": "requestID",
"execution_hint": "map",
"order": [
{
"startTimeMillis": "desc"
}
],
"size": 10
}
}
},
"query": {
"bool": {
"must": [
{
"range": {
"startTimeMillis": {
"from": 1597581000000,
"include_lower": true,
"include_upper": true,
"to": 1597582800000
}
}
},
{
"match": {
"process.serviceName": {
"query": "nginx"
}
}
}
]
}
},
"size": 0
}

小结

需要注意这种并不算是普通的父子聚合查询,因为子聚合是父聚合的排序字段,所以就没有对collect mode进行讨论。

在冷数据的情况下,对大基数字段聚合时,global ordinal几乎都要比map模式要快。但是热数据情况下,map模式却不一定比global ordinal 模式快,因为map可能会耗损大量的内存。比如上面的热数据,在数据量接近8kw时,map反而比global ordinal更慢。

global ordinal 和 map 并不是绝对的,往往跟数据的实际情况(基数大小、字段类型)紧密相关,需要根据实际情况进行调参。

总结

本文首先讨论了term aggregation的在父子聚合查询下collecto mode参数对聚合查询的影响,并分析了深度优先和广度优先的区别。还讨论了executrion hit对大基数字段聚合的影响,并总结了适用的场景。

参考

doc-values
Deep Dive on Doc Values
Elasticsearch聚合——Bucket Aggregations
Shard request cache
eager_global_ordinals
Tune for search speed
Elasticsearch聚合操作的时间复杂度是O(n)吗?
聚合查询越来越慢?——详解Elasticsearch的Global Ordinals与High Cardinality
Improving the performance of high-cardinality terms aggregations

本文为学习过程中产生的总结,由于学艺不精可能有些观点或者描述有误,还望各位同学帮忙指正,共同进步。