该文章内容已经收录在《面试进阶之路》,从原理出发,直击面试难点,实现更高维度的降维打击!
目录
文章目录
- 目录
- 美团面试:Elasticsearch 深分页问题如何解决?
- 普通分页方案
- 技术解决方案
- 滚动查询:Scroll
- PIT + search_after
- 技术方案、性能对比
- 业务解决方案
美团面试:Elasticsearch 深分页问题如何解决?
Elasticsearch 的深分页问题在面试中是经常会被问到的,接下来梳理一下对应的 技术 和 业务 解决方案
普通分页方案
在 Elasticsearch 中,正常情况下会使用 from、size 来实现分页,但是当分页条数过多,比如 from + size > 10000 ,那么就会带来问题,先来看一下 Elasticsearch 分页查询流程:
- 协调节点将分页查询请求转发给各个节点,各个节点执行搜索,将前 N 条数据返回给协调节点
- 协调节点汇总各个分片的数据,再次排序之后,返回前 N 条数据给客户端
深分页存在问题:
当客户端查询的分页越多,各个节点执行的查询结果也就越多,最后协调节点还要汇总各个节点的查询结果,并且进行排序,整个排序过程在 堆 中,当数据量过大,会出现两个问题:
- 出现 OOM 问题
- 频繁的深分页会导致频繁的 GC(因为会在堆中产生大量的数据,排序之后要及时清除)
比如说要查询 10001 - 10100 的这 100 条数据,此时不仅仅去每个分片中取 100 条数据,因为每个分片排序之后并不一定是全局排序后的结果,因此每个分片都要查询前 10100 条数据,最终各个分片的数据发往协调节点,在协调节点中进行排序,取最终的 10100 条数据
可以看到,当 from + size 很大的时候,协调节点中需要排序的数据量为: (from + size) * 分片数量 ,排序、存储成本都很高
Elasticsearch 内部对深分页的限制:
对于深分页带来的严重后果,Elasticsearch 内部也进行了限制,通过 max_result_window 限制了查询结果窗口的大小,默认是 10000
深分页问题解决:
解决 ES 深分页问题有两方面解决方案:业务解决方案、技术解决方案
技术解决方案
滚动查询:Scroll
通过 scroll 滚动查询来解决深分页问题,原理为:
- scroll 会在第一次搜索时,保存一个视图快照,之后基于快照对数据一批一批进行搜索,直至搜索出全部数据,避免一次搜索出大量数据
scroll 查询数据流程:
(1)第一次查询:参数传入 scroll 的过期时间参数,1m 表示快照数据缓存的时间为 1 分钟,并返回一个 scroll_id
GET /es_db/search?scroll=1m
{"query": {"match_all": {}},"size": 2
}
(2)之后查询携带上 scroll_id 值
GET /_search/scroll
{"scroll": "1m","scroll_id": "FGluY2x1ZGVfY29udGV4dGF9D1dKbXDFIZXJ5QW5kRWnV02gBFmNWVdjbjRvUzVhZXIicG1HUE02blcwCAAAAAAAABmzR2Y1V3Zo5VVNTdWJokbESZM3XzbJ"
}
适合场景:
- 适合于海量数据导出、非 C 端业务、对数据实时性要求不高的场景
存在问题:
-
该方式官方已经不推荐使用
-
使用 scroll 查询,需要维护数据快照,消耗大量的资源,并且查询的是静态数据
PIT + search_after
search_after 提供了 实时游标 来记录上次查询的最后一条记录,可以根据上一页的查询结果来检索下一页(因此要求要有一条全局唯一的变量用作排序),并且可以实现实时查询,但不支持跳页查询的功能
search_after 原理:
- 第一次查询和 scroll 一样,从各个分片获取前 N 条数据,进行排序,返回客户端前 N 条数据
- 第二次查询会记录第一页数据的最大值,作为第二页数据拉取的查询条件,之后所有分片基于这个最大值进行查询,减少了各个分片查询的数据量,最后汇总给协调节点进行排序
在 search_after 查询过程中,需要相同的查询的多个搜索请求,如果在查询请求之间数据发生变化,可能导致最终数据出现不一致的情况,因此创建一个时间点(PIT)来保存当前的索引状态
PIT(Point In Time) 是 ES 7.10 版本之后推出的新特性
PIT + search_after 查询流程
(1)创建一个 PIT 保存搜索期间的当前索引状态
POST /es_db/_pit?keep_alive=1m
# 返回 PIT 的值
{
"id": "39K1AwEFZXNfZGIWZTN2N2Nrdk5RRjY3QjBma1h5aFRodwAWdkhjbE9YNVRTMUNDcWNQQVR2ZXYzdwAAAAAAAAA9jhZvaGpLSDlzVVMxbW5idG5DZ0xEUHFRAAEWZTN2N2Nrdk5RRjY3QjBma1h5aFRodwAA"
}
(2)根据 PIT 首次查询
GET /_search
{"query": {"match_all": {}},"pit": {"id": "39K1AwEFZXNfZGIWZTN2N2Nrdk5RRjY3QjBma1h5aFRodwAWdkhjbE9YNVRTMUNDcWNQQVR2ZXYzdwAAAAAAAAA9jhZvaGpLSDlzVVMxbW5idG5DZ0xEUHFRAAEWZTN2N2Nrdk5RRjY3QjBma1h5aFRodwAA","keep_alive": "1m"},"size": 2,"sort": [{"_id": "asc"}]
}
(3)根据 search_after 和 PIT 实现翻页查询
GET /_search
{"query": {"match_all": {}},"pit": {"id": "39K1AwEFZXNFdGlZWTN2N2NkRSRjY3jQjBma1hSaFRodwAWdkhjbjE9YNVRTMUNDcWNQQVR2ZXYzdwAAAAAAAAA9jzhVaGpLSDIzVMxbMxbWi5dG5DZ0xEUHFAAEZWTN2N2NkRSRjY3jQjBma1hSaFRodwAA","keep_alive": "1m"},"size": 2,"sort": [{"_id": "asc"}],"search_after": [5]
}
PIT + search_after 优点:
- 通过 PIT 可以保存当前索引状态,避免查询过程中数据变化导致的不一致
- search_after 基于上次结果进行查询,避免每次查询大量的数据
技术方案、性能对比
官方建议: ES7 之后官方不建议使用 scroll 进行深度分页,而是建议使用 PIT + search_after 来实现深分页,各方案对应优缺点如下:
分页方式 | 性能 | 优点 | 缺点 | 场景 |
---|---|---|---|---|
from + size | 低 | 灵活性好,实现简单 | 深度分页问题 | 数据量比较小,能容忍深度分页问题 |
scroll | 中 | 解决了深度分页问题 | 无法反应数据的实时性(快照版本)维护成本高,需要维护一个 scroll_id | 海量数据的导出需要查询海量结果集的数据 |
search_after | 高 | 性能最好,不存在深度分页问题能够反映数据的实时变更 | 实现复杂,需要有一个全局唯一的字段连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果,它不适用于大幅度跳页查询 | 海量数据的分页 |
ES 分页性能对比:
分页方式 | 1~10 | 49000~49010 | 99000~99010 |
---|---|---|---|
form + size | 8ms | 30ms | 117ms |
scroll | 7ms | 66ms | 36ms |
search_after | 5ms | 8ms | 7ms |
业务解决方案
一般对于深分页问题,从业务上可以很好地进行解决,主流的有两种方案:
- 限制跳转最大页数,避免一次跳太多页,导致查询大量数据
- 限制不允许跳页,每次只可以下一页,这样也可以避免查询大量数据
在微信公众号、淘宝、百度都使用这样的业务限制,来避免深分页问题