公司目前所使用的 Elasticsearch(以下简称 ES)版本是在 2015 年的时候选择的,当时选择了 ES 的最新版本 1.5.2。随着 ES 官方的快速发展,ES 也已经推出了很多新的版本,这些新版本往往伴随着一些新特性的发布以及性能的提升,为了使用到这些新特性,我们决定将 ES 的版本升级到 7.5.2。

ES 版本升级会遇到 API 不兼容的问题,我们的解决办法是在业务和 ES 之间增加一层_搜索平台_来做接口兼容。此外我们在版本升级之前还需要了解一下 ES-7.5.2 相对于 ES-1.5.2 有多少性能提升,所以我们需要针对这两个版本做一下性能测试并对结果进行对比。我们使用如下的四台负载来做性能测试

序号节点 IPCPU 型号CPU 核数内存硬盘操作系统
1172.19.66.58Intel(R) Xeon(R) CPU E5645 @2.40GHz23079520kB14GGNU/Linux 3.10.0-957.21.3.el7.x86_64
2172.19.66.70Intel(R) Xeon(R) CPU E5645 @2.40GHz23079524kB14GGNU/Linux 3.10.0-957.21.3.el7.x86_64
3172.19.66.77Intel(R) Xeon(R) CPU E5645 @2.40GHz23079524kB14GGNU/Linux 3.10.0-957.21.3.el7.x86_64
4172.19.66.133Intel(R) Xeon(R) CPU E5645 @2.40GHz23079524kB14GGNU/Linux 3.10.0-957.21.3.el7.x86_64

使用 time 和 dd 命令测试负载的磁盘性能

# 测试磁盘的写入性能
time dd if=/dev/zero of=test.data bs=8k count=10000 oflag=direct
# 测试磁盘的读取性能
time dd if=test.data of=/dev/null bs=8k count=10000 iflag=direct

通过测试可知以上四台负载的磁盘读写性能基本一致。

压测前的环境准备

创建索引和数据

1 号和 2 号节点安装 ES-1.5.2 集群,3 号和 4 号节点安装 ES-7.5.2 集群。两个集群各自的两个节点均设置为 master-eligible 和 data 节点,并且 JVM 的堆内存都设置为1GB。分别向两个集群写入相同的测试数据 112 万条,对于 text 类型两个集群都使用 ES 默认的分词器。两个集群的两个索引的 mapping 基本上一致,我把 1.5.27.5.2 这两个索引的 mapping 都放在了 gist 上面以供参考。

创建压测环境

我们使用 ApacheBench (ab) 来实现压测功能,为了方便操作我们利用 ab 实现了一个测试脚本 test.sh,在测试脚本中我们使用了 query.json 文件中的 ES 查询 DSL 来实现 ES 不同类型查询的性能测试,后面我们会修改 query.json 文件中的 DSL 来实现不同的查询测试。

进行压测

压测使用的客户机为 MacBookPro,客户机和 ES 集群位于同一个局域网,因此可以保证网络带宽不会成为瓶颈。客户机的具体配置如下

macOs Catalina 10.15.4 (19E287)
MacBook Pro (Retina, 13-inch, Early 2015)
2.7 GHz Dual-Core Intel Core i5
8 GB 1867 MHz DDR3

具体的测试结果如下,我们修改 query.json 的 DSL 来实现不同类型操作的测试。

term-level

term
1
2
3
4
5
6
7
8
{
"query": {
"term": {
"acctId": "A00000000000"
}
},
"size": 0
}

修改 query.json 的内容如上并且执行命令

./test.sh

随后我们可以得到脚本的执行结果,因为执行结果内容比较多所以我们只挑一些关键性的部分进行了解。完整的执行结果可以在 gist 上面 看到。

Elasticsearch-1.5.2 >>> 172.19.66.70
Requests per second:    165.87 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%     343
66%     399
75%     421
80%     454
90%     547
95%     620
98%     687
99%     784
100%    905 (longest request)

Elasticsearch-7.5.2 >>> 172.19.66.77
Requests per second:    1049.51 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%     53
66%     61
75%     68
80%     73
90%     87
95%     100
98%     118
99%     129
100%    202 (longest request)

可以看到在并发为 60 的情况下,执行压测 10 秒钟,1.5.2 的 QPS 为 165.87,7.5.2 的 QPS 为 1049.51,此外 7.5.2 的 99%的请求耗时都是在 130ms 以下的,由此可以证明 7.5.2 相较于 1.5.2 在 term 查询上确实有很大的性能提升。

range

query.json

1
2
3
4
5
6
7
8
9
10
{
"query": {
"range": {
"createdTimeStr": {
"gte": "2020-01-01T00:00:00.000+0800"
}
}
},
"size": 0
}

性能测试结果

Elasticsearch-1.5.2 >>> 172.19.66.70
Requests per second:    1237.86 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%     45
66%     52
75%     58
80%     62
90%     75
95%     85
98%     101
99%     113
100%    189 (longest request)

Elasticsearch-7.5.2 >>> 172.19.66.77
Requests per second:    1099.03 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%     47
66%     54
75%     59
80%     63
90%     75
95%     86
98%     102
99%     120
100%    166 (longest request)

可以看出 7.5.2 在 range 查询上有一定的性能提升,但是提升并不明显。

match

query.json

1
2
3
4
5
6
7
8
{
"query": {
"match": {
"userName.analyzed": "江苏千米"
}
},
"size": 0
}

性能测试结果

Elasticsearch-1.5.2 >>> 172.19.66.70
Requests per second:    789.62 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%     74
66%     92
75%     107
80%     115
90%     132
95%     148
98%     169
99%     187
100%    336 (longest request)

Elasticsearch-7.5.2 >>> 172.19.66.77
Requests per second:    1229.56 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%     44
66%     52
75%     57
80%     62
90%     72
95%     85
98%     107
99%     135
100%    275 (longest request)

由上面的结果可见 7.5.2 对 match 查询也是有一定的性能提升的。

aggregation 操作

metrics

query.json

1
2
3
4
5
6
7
8
9
10
{
"aggs": {
"balance_stats": {
"extended_stats": {
"field": "balance"
}
}
},
"size": 0
}

性能测试结果

Elasticsearch-1.5.2 >>> 172.19.66.70
Requests per second:    32.67 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%    1838
66%    1938
75%    1970
80%    1972
90%    2427
95%    2555
98%    2597
99%    3486
100%   3806 (longest request)

Elasticsearch-7.5.2 >>> 172.19.66.77
Requests per second:    1393.98 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%     41
66%     48
75%     53
80%     56
90%     65
95%     74
98%     84
99%     93
100%    141 (longest request)

7.5.2 在指标聚合上的速度提升也十分的可观。

bucket

query.json

1
2
3
4
5
6
7
8
9
10
11
{
"aggs": {
"typeName": {
"terms": {
"field": "balanceTypeName",
"size": 50
}
}
},
"size": 0
}

性能测试结果

Elasticsearch-1.5.2 >>> 172.19.66.70
Requests per second:    79.98 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%    721
66%    1015
75%    1171
80%    1258
90%    1488
95%    1571
98%    1600
99%    1618
100%   1643 (longest request)

Elasticsearch-7.5.2 >>> 172.19.66.77
Requests per second:    1629.80 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%     34
66%     40
75%     45
80%     48
90%     57
95%     65
98%     77
99%     86
100%    153 (longest request)

7.5.2 在桶聚合上也有着很好的性能优化效果。

小结

从上面的 term、range、match、metric、bucket 的测试结果中我们可以看到,除了 range 之外其余的几个 DSL 在 7.5.2 都有了较大的性能提升。但是我们也要清醒地认识到,我们在上面的单次性能测试中使用的都是完全一样的 DSL,这就可能会使得 ES 中的缓存生效,因此该性能测试可能无法完全展现出真实环境中的 ES 性能,因为在真实环境中 ES 的缓存可能并不能像这里测试的这样完美的生效。

此外,由于这里的机器资源有限,我们只能做一个简单的性能对比实验,证明 7.5.2 的性能确实是优于 1.5.2 的。真正的 ES-7.5.2 集群的性能极限还要等到_搜索平台_的兼容性测试完成后,这时候我们可以搭建一个真正的生产环境的 7.5.2 集群,然后对此集群进行极限压测,等后面做了我会再做一份笔记。

数据写入的性能测试

上面我们测试了 ES 的数据检索和聚合的性能,下面我们也对 ES 的数据写入性能做一个测试。创建 data.json 并保存如下的数据

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
{
"id": 86523,
"name": "普通高等教育“十一五”国家级规划教材配套参考书:电工电子学学习辅导与习题解答(第3版)",
"publish": "高等教育出版社",
"author": "张伯尧,叶挺秀",
"info": "《电工电子学学习辅导与习题解答(第3版)》",
"price": 86.9,
"createdTimeStr": "2020-01-01T12:00:00.000+0800",
"createdTime": 1547873713709,
"type": "college",
"sellOut": false,
"students": [
{
"name": "王博文",
"class": 1
},
{
"name": "徐晓静",
"class": 2
},
{
"name": "路小楠",
"class": 1
}
]
}

使用 ab 测试 1.5.2 的集群写入性能

ab -n 50000 -c 200 -p data.json -T 'application/json' http://172.19.66.70:9200/books/Book

性能报告如下

Time taken for tests:   26.680 seconds
Requests per second:    1874.06 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%     72
66%     85
75%     95
80%     101
90%     123
95%     147
98%     1236
99%     1276
100%    3652 (longest request)

使用 ab 测试 7.5.2 的集群写入性能

ab -n 50000 -c 200 -p data.json -T 'application/json' http://172.19.66.77:9200/books/_doc

性能报告如下

Time taken for tests:   103.203 seconds
Requests per second:    484.48 [#/sec] (mean)
Percentage of the requests served within a certain time (ms)
50%     344
66%     387
75%     438
80%     478
90%     615
95%     739
98%     979
99%     1097
100%    2436 (longest request)

很奇怪的是 ES-7.5.2 的数据写入性能明显低于 ES-1.5.2,为了规避掉机器之间的细微的差异,我把 ES-7.5.2 安装在节点 1 和节点 2 之上,之后停止 ES-1.5.2 集群同时启动 ES-7.5.2 集群,然后把数据写到新的 7.5.2 的集群中去,测试结果和上面还是基本一致。具体原因暂时还不清楚,后面再做进一步的深入了解。

数据写入慢的原因

我们删除上面的测试中所创建的 books 索引,之后给每个索引只创建一条文档

ab -n 1 -c 1 -p data.json -T 'application/json' http://172.19.66.70:9200/books/Book
ab -n 1 -c 1 -p data.json -T 'application/json' http://172.19.66.77:9200/books/_doc

只写入一条数据我们发现 1.5.2 也是快于 7.5.2 的集群的,之后我们使用如下接口观察我们写入的数据的段文件情况,首先是 1.5.2 集群

http://172.19.66.70:9200/books/_segments

得到 1.5.2 的段文件信息如下

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
{
"_shards": {
"total": 4,
"successful": 4,
"failed": 0
},
"indices": {
"books": {
"shards": {
"0": [
{
"routing": {
"state": "STARTED",
"primary": true,
"node": "Fn-qbetJSyS7Qn6qb8rcaQ"
},
"num_committed_segments": 1,
"num_search_segments": 1,
"segments": {
"_0": {
"generation": 0,
"num_docs": 1,
"deleted_docs": 0,
"size_in_bytes": 9870,
"memory_in_bytes": 12898,
"committed": true,
"search": true,
"version": "4.10.4",
"compound": true
}
}
},
{
"routing": {
"state": "STARTED",
"primary": false,
"node": "PvI9366jTQKbFXWF2HRg6w"
},
"num_committed_segments": 1,
"num_search_segments": 1,
"segments": {
"_0": {
"generation": 0,
"num_docs": 1,
"deleted_docs": 0,
"size_in_bytes": 9870,
"memory_in_bytes": 12898,
"committed": true,
"search": true,
"version": "4.10.4",
"compound": true
}
}
}
],
"1": [
{
"routing": {
"state": "STARTED",
"primary": false,
"node": "Fn-qbetJSyS7Qn6qb8rcaQ"
},
"num_committed_segments": 0,
"num_search_segments": 0,
"segments": {}
},
{
"routing": {
"state": "STARTED",
"primary": true,
"node": "PvI9366jTQKbFXWF2HRg6w"
},
"num_committed_segments": 0,
"num_search_segments": 0,
"segments": {}
}
]
}
}
}
}

从上面的结果中我们可以看到这条文档被保存到了 books 索引的 0 号分片中,这个分片包含了 primary 和 replica 两份数据,1 号分片中的数据则为空。我们可以看到 0 号分片的主备两个分片的 segments.size_in_bytes 的值均为 9870,这代表了 segment 在磁盘上占用的空间大小。

了解了以上信息之后,我们继续获得 7.5.2 的段文件信息

http://172.19.66.77:9200/books/_segments

得到的 7.5.2 的段文件信息如下

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
{
"_shards": {
"total": 4,
"successful": 4,
"failed": 0
},
"indices": {
"books": {
"shards": {
"0": [
{
"routing": {
"state": "STARTED",
"primary": true,
"node": "sLtK-N1eSFiGNV7PeA1ntg"
},
"num_committed_segments": 0,
"num_search_segments": 0,
"segments": {}
},
{
"routing": {
"state": "STARTED",
"primary": false,
"node": "HNJ8oebqSRGO6C9YJbjCFA"
},
"num_committed_segments": 0,
"num_search_segments": 0,
"segments": {}
}
],
"1": [
{
"routing": {
"state": "STARTED",
"primary": false,
"node": "sLtK-N1eSFiGNV7PeA1ntg"
},
"num_committed_segments": 0,
"num_search_segments": 1,
"segments": {
"_0": {
"generation": 0,
"num_docs": 1,
"deleted_docs": 0,
"size_in_bytes": 11863,
"memory_in_bytes": 5007,
"committed": false,
"search": true,
"version": "8.3.0",
"compound": true,
"attributes": {
"Lucene50StoredFieldsFormat.mode": "BEST_SPEED"
}
}
}
},
{
"routing": {
"state": "STARTED",
"primary": true,
"node": "HNJ8oebqSRGO6C9YJbjCFA"
},
"num_committed_segments": 0,
"num_search_segments": 1,
"segments": {
"_0": {
"generation": 0,
"num_docs": 1,
"deleted_docs": 0,
"size_in_bytes": 11863,
"memory_in_bytes": 5007,
"committed": false,
"search": true,
"version": "8.3.0",
"compound": true,
"attributes": {
"Lucene50StoredFieldsFormat.mode": "BEST_SPEED"
}
}
}
}
]
}
}
}
}

7.5.2 集群的 books 索引的文档分配在了分片 1 上,同样分片 1 也包含了主副两个分片。我们可以看到 7.5.2 的段文件占用的磁盘空间为 11863,比 1.5.2 的 9870 要大一些,这是为什么呢?按道理来说 Lucene 从版本 4.10.4 升级到版本 8.3.0,占用空间应该有一定程度上的优化才对,至少也不应该是占用空间变大了啊。

通过查找资料,我们可以知道在新版本的 Elasticsearch 中,为了优化 ES 的排序聚合的速度而引入了 Doc values 属性,该属性是在文档索引时生成的,目的是为了替代以前在文档查询时才生成的 Fielddata 属性。

我们可以对字段的 mapping 属性添加

"doc_values": false

这样可以禁用字段的 doc_values 属性,在禁用了 7.5.2 的字段的 doc_values 之后,段文件占用空间明显减小,索引速度也有明显的提升。

BUT!!!

通过以上的配置之后,7.5.2 的数据索引速度仍然只是 1.5.2 的一半😶,具体原因还需进一步的分析😢。

5 月 19 日补充:

关闭两个集群的 refresh_interval(关闭自动 refresh 之后,写入到索引的数据不会被查询到,除非手动的执行了 refresh 操作之后才可以)

PUT /books/_settings

{
    "refresh_interval": -1
}

同时关闭 7.5.2 的 doc_values,并且只保存了 long 类型的数据以避免分词器的影响,之后测试结果发现仍然是 7.5.2 的时间更长。不过此时我们使用如下的接口查询索引的详细信息

GET /books/_stats

可以发现两个索引的 indexing.index_time_in_millis 的值其实是一样呢,这是为啥呢。。。

参考

Apache Bench 安装与使用
性能测试应该怎么做?

滴滴 ElasticSearch 平台跨版本升级以及平台重构之路
https://www.elastic.co/guide/en/elasticsearch/reference/7.5/query-dsl.html
https://www.elastic.co/guide/en/elasticsearch/reference/7.5/search-aggregations.html