[ElasticSearch] 亿级数据分页查询优化过程
Posted 一杯糖不加咖啡
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[ElasticSearch] 亿级数据分页查询优化过程相关的知识,希望对你有一定的参考价值。
一、需求场景
公司业务需求,有约10亿条数据,根据一些条件进行查询,字段数量比较庞大(27个),查询条件不太多(约8个),例如比较常见的时间范围、地区范围。
程序要求响应速度在200ms以内。
二、硬件环境
3台搭载ES6.8.5的服务器组成ES集群,配置均为1.4T硬盘+256G内存
三、整体设计
- 采用分页查询展示数据
- 避免ES中的复杂查询操作
- 字段均设置为keyword保证占用最少的空间
四、分页方法选择(参考:博文地址)
-
From+Size
因为最初考虑不周,使用的是RestHighLevelClient进行查询,查询语句:// esEntityRepository是继承了extends ElasticsearchRepository<EsEntity, Long> 的封装工具类 org.springframework.data.domain.Page<EsEntity> result = esEntityRepository.search(booleanQueryBuilder,PageRequest.of(pageNum, pageSize) );
例如查出第 6~8 笔数据
GET /my_index/_search "from": 5, "size": 3
from:表示记录开始的顺序,默认0
size:表示取回几笔数据,默认10
from+size 默认不能超过10000,可以通过修改索引参数index.max_result_window调整1.2 基本原理
协调节点将查询请求发送给所有分片
各分片收到请求后,查出 from + size 的数据,并返回给协调节点。例如:当前请求是查询第5~10笔数据,from + size = 10,因此每个分片都要返回排序后的前10笔数据
协调节点将收到的数据进行合并,重新排序,然后返回指定的size分页数据给客户端1.3 存在的问题
深分页的情况下,也就是from + size 值特别大,可能会造成慢查询。因为每个节点都需要返回 from + size 的排序数据,然后最终还需要合并以及排序,就算没有内存溢出,对CPU和IO也有巨大影响。根据官网介绍:
index.max_result_window
The maximum value of from + size for searches to this index. Defaults to 10000. Search requests take heap memory and time proportional to from + size and this limits that memory. See Scroll or Search After for a more efficient alternative to raising this.官网已经指出了from + size搜索到该索引 的最大值默认为 10000,搜索请求占用的堆内存和时间与之成正比 from + size,因此会限制该内存。有关提高此效果的更有效的选择,请参见 Scroll 或者 Search After。
下面我们使用官网推荐的两种办法进行分页。
-
Scroll(根据官网介绍该方法已经不推荐使用了)
2.1 基本用法
第一步,初始化 scroll ,并获取第一批数据:POST /my_index/_search?scroll=5m "size":2, "query": "match_all": , "sort": [ "num": "order": "asc" ]
scroll=5m:指定查询上下文的有效期,这边设置为5分钟
size:指定初始化分页大小,这边设置2
sort:排序
返回值如下:"_scroll_id" : "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFDFaQ3hQblVCNEZuWkdyWGFCcDFnAAAAAADGUE0WclVEQkh0VUlSWnFFM05QUEJaMU5Sdw==", "took" : 0, "timed_out" : false, "_shards" : "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 , "hits" : "total" : "value" : 10, "relation" : "eq" , "max_score" : null, "hits" : [ "_index" : "my_index", "_type" : "_doc", "_id" : "pJBvPnUB4FnZGrXaoStR", "_score" : null, "_source" : "num" : 1 , "sort" : [ 1 ] , "_index" : "my_index", "_type" : "_doc", "_id" : "pZBvPnUB4FnZGrXaoStR", "_score" : null, "_source" : "num" : 2 , "sort" : [ 2 ] ]
注意:返回值的第一项是_scroll_id,后面的每一步查询都需要传递该参数
第二步,获取下一页数据:POST /_search/scroll "scroll": "5m", "scroll_id": "FGluY2x1ZGVfY29udGV4dF91dWlkDXF1ZXJ5QW5kRmV0Y2gBFDFaQ3hQblVCNEZuWkdyWGFCcDFnAAAAAADGUE0WclVEQkh0VUlSWnFFM05QUEJaMU5Sdw=="
scroll:指定查询上下文的有效期,这边仍然设置为5分钟。注意:这步执行完毕后,查询上下文的有效期将重新计算。可以理解为scroll有效期的设置指的是2次查询的时间间隔,如果超过该有效期没有任务查询发生,那将会失效。
scroll_id:传递最近一次查询返回的_scroll_id2.2 基本原理
scroll 初始化会返回所有符合条件的数据,并形成一份快照数据
每次获取分页数据,都会以类似游标的方式读取该快照
快照的有效期由scroll参数指定,每次查询重新传递scroll参数,会刷新有效期
快照形成后,ES索引数据的更新都不会被 scroll 读取到,这边读取的仍然是快照生成时的数据。因此,不适用于实时数据要求非常高的场景。2.3 存在的问题
不建议同时打开大量的 scroll 上下文,因为在 scroll 有效期到达之前,都会占用一定的内存空间。官方默认同时打开的 scroll 数量是500,可以通过 search.max_open_scroll_context参数调整
scroll 上下文指的是第一次初始化形成的快照,后续基于该上下文的查询都不会查到最新数据。因此不适用于实时性高的场景。
为了避免这些问题,官方推出另一个查询方式:Search After -
Search After
3.1 基本用法
第一步:构造数据:PUT /my_index/_bulk?refresh "index": "num":1, "name":"zhangsan" "index": "num":2, "name":"lisi", "age": 20 "index": "num":2, "name":"lisi", "age": 23 "index": "num":3, "name":"wangwu" "index": "num":4, "name":"zhaoliu"
第二步:查出第1页数据,按num+name进行组合排序
GET /my_index/_search "size": 2, "sort": [ "num": "order": "asc" , "name.keyword": "order": "asc" ]
结果如下:
"hits" : [ "_index" : "my_index", "_type" : "_doc", "_id" : "BpHsPnUB4FnZGrXapQfN", "_score" : null, "_source" : "num" : 1, "name" : "zhangsan" , "sort" : [ 1, "zhangsan" ] , "_index" : "my_index", "_type" : "_doc", "_id" : "B5HsPnUB4FnZGrXapQfN", "_score" : null, "_source" : "num" : 2, "name" : "lisi", "age" : 20 , "sort" : [ 2, "lisi" ] ]
结果如下:
"hits" : [ "_index" : "my_index", "_type" : "_doc", "_id" : "CZHsPnUB4FnZGrXapQfN", "_score" : null, "_source" : "num" : 3, "name" : "wangwu" , "sort" : [ 3, "wangwu" ] , "_index" : "my_index", "_type" : "_doc", "_id" : "CpHsPnUB4FnZGrXapQfN", "_score" : null, "_source" : "num" : 4, "name" : "zhaoliu" , "sort" : [ 4, "zhaoliu" ] ]
3.2 基本原理
search after 在排序的基础上,基于上一笔的sort值,查询排在它之后的数据,以此来实现分页
search after 是无状态的,它总是查询ES索引最新的数据,这一点跟 scroll 查询完全相反3.3 存在的问题
sort 排序组合必须是唯一的,否则会出现数据丢失的情况。例如上面的例子,[2,“lisi”] 这个组合有2笔数据,但是第二页直接从[3,“wangwu”]开始了。建议是加上id字段作为排序字段。
该方法在分页中不能调页而且只使用该方法只能跳转下一页(因为获取不到上一页的最后一条数据) -
总结
4.1 from / size :
该查询的实现原理类似于mysql中的limit,比如查询第10001条数据,那么需要将前面的10000条都拿出来,进行过滤,最终才得到数据。性能较差,实现简单,适用于少量数据,数据量不超过10w,适合浅分页。4.2 scroll:该查询实现类似于消息消费的机制,首次查询的时候会在内存中保存一个历史快照以及游标(scroll_id),记录当前消息查询的终止位置,下次查询的时候将基于游标进行消费。性能良好,维护成本高,在游标失效前,不会更新数据,不够灵活,一旦游标创建size就不可改变,适用于大量数据导出或者索引重建、深分页,并且数据实时要求不高,最适合离线场景。
4.3 search_after: 性能优秀,类似于优化后的分页查询,历史条件过滤掉数据,适合深分页。
-
SearchAfter实现可跳转前后N页
5.1 基本思路
SearchAfter之所以只能选择下一页是因为只能获取到当前页最后一条数据作为search_after的参数,那么如果我能把当前页前后N页最后一条都给前端返回是不是就能实现了呢?
假设目标为跳转前后5页,则当选择第1页时给前端返回页码以及数据情况:1-数据 2-该页最后一条 3-该页最后一条 4-该页最后一条 5-该页最后一条 6-该页最后一条
当选择第6页时返回页码以及数据情况:
1-该页最后一条 2-该页最后一条 3-该页最后一条 4-该页最后一条 5-该页最后一条 6-数据 7-该页最后一条 8-该页最后一条 9-该页最后一条 10-该页最后一条 11-该页最后一条
5.2 基本用法
使用SearchAfter的语句进行查询,但是size是原来的2N+1倍,在程序中处理返回数据还是该页码的最后一条数据
前端传值:目标页码、后端SearchAfter参数(当目标页码>N+1
时为目标页码-(N+1)页码
的最后一条,否则为空)5.3 存在问题
a. 如此设计存在大量返回但没有使用的数据,产生一定的资源浪费和时间消耗
b. 同样只能实现跳转前后N页,不可实现任意跳转 -
产品设计优化
使用第5种方案进行设计,最终结果仍然达不到查询全部数据耗时为200ms以内,分析原因可能是数据量大而服务器内存不足(优秀博文、强烈推荐:参考链接)。于是只好改变产品设计缩短ES默认查询时间间隔,此方法为最终采用,但探索过程学到了很多有趣的知识。
以上是关于[ElasticSearch] 亿级数据分页查询优化过程的主要内容,如果未能解决你的问题,请参考以下文章
财务平台亿级数据量毫秒级查询优化之elasticsearch原理解析(转)
ElasticSearch实战(五十)-让Elasticsearch飞起来!百亿级数据存储与查询优化实战!!!
ElasticSearch实战(五十)-让Elasticsearch飞起来!百亿级数据存储与查询优化实战!!!