ES 中,存在三种常见的分页方案:

  • FROM, SIZE
  • Search-After
  • Scroll

下面将依次比较三种方案之间的 trede-off,并给出相应建议的应用场景。

常见分页,FROM, SIZE

ES 提供了常见的分页功能,通过在 search API 中,指定 from 和 size 来实现分页的效果:

{
"from": 10,
"size": 20,
"sort": [{"timestamp": "desc"}],
"query": {
"match_all": {} # 返回所有 doc
}
}

from: 表示起点位置,默认是 0.

size:表示返回的数量,默认是 10.

这种分页方式到没什么好说的,但需要注意的是由于 ES 为了支持海量数据的查询,本身采用了分布式的架构。

而对于分布式架构来说,存在一个典型的深度分页的问题。

ES 在存储数据时,会将其分配到不同的 shard 中。在查询时,如果 from 值过大,就会导致分页起点太深。

每个 shard 查询时,都会将 from 之前位置的所有数据和请求 size 的总数返回给 coordinator. 简单来说,就是想取第 n 页的内容,但是却返回了前 n 的内容。

而对于 coordinator 来说,会显著导致内存和CPU使用率升高,特别是在高并发的场景下,导致性能下降或者节点故障。

举例来说,当前 ES 共有 4 个 shard,并且每个 shard 没有副本。假如分页的大小为 10. 然后想取第11 页前 5 条内容。对应的 from = 1000,size = 5.

ES 的查询过程为:

  1. 每个 shard 将所在数据加载到内存并排序,然后取前 1005 个,返回给 coordinator.
  2. 每个 shard 都执行上面的操作。
  3. 最后 coordinator 将 1005 * 4 = 4020 条数据排序,然后取 5 条数据返回。

可以发现,from 的位置太深,造成了如下的问题:

  • 返回给 coordinator 数值太大,明明就需要 5 条数据,但却给 coordinator 1005 条数据
  • coordinator 需要处理每个 shard 返回前 11 页的结果。但需要的仅是第 11 页的内容,却对前 11 页的内容进行了排序,浪费了内存和 cpu 的资源。

ES 为了规避这个问题,通过设置 max_result_window 来限制 from 和 size 的大小,默认大小仅支持 10000 条。当超过 10000 的大小,则会报出异常。

在页数不深或者考虑内存,低并发等情况,可以通过临时调整 max_result_window 来解决该问题,但如果页数太深则建议使用的 Search-After 的方式。

SearchAfter 分页

为了应对深度分页的情况,ES 推荐使用 SearchAfter 的方式,来实现数据的深度翻页检索。

在具体实现上,通过动态指针的技术。在第一次使用 search api 查询时,附带一个 sort 参数,其中 sort 的值必须唯一,可以用 _id 作为排序参数。

{
"from": 0,
"size": 1,
"sort": [
{"timestamp": "desc"},
{"_id": "asc"}
],
"query": {
"match_all": {}
}
}

每个 shard 在排序后会记录当前查询的最后位置,然后将其返回。

{
"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": "cmi_alarm_info",
"_type": "_doc",
"_id": "1,3,d_to_s_JitterAvg",
"_score": null,
"_source": {
"src_device_id": 1,
"dst_device_id": 3,
"type": "d_to_s_JitterAvg",
"status": "normal",
"create_time": 1617085800
},
"sort": [
1617085800,
"1,3,d_to_s_JitterAvg"
]
}
]
}
}

下次查询时,在 search_after 携带 Response 中返回的 sort 参数,实现分页的查询。

{
"from": 0,
"size": 1,
"sort": [
{"timestamp": "desc"},
{"_id": "asc"}
],
"query": {
"match_all": {}
},
"search_after": [
1617084900,
"13,3,d_to_s_JitterAvg"
]
}

和 from size 的查询方式相比, 每个 shard 每次返回给 coordinator 的结果仅为 size 数量,将空间复杂度由 O(n) 降为 O(1).

但 Search-after 也有一些问题:

首先就是不支持跳页的情况。

如果需求上一定需要跳页时,只能通过 from 或者 size 的方式。同时为了避免深度分页的问题,一般可以采用限制页面数量的方式。在确定 size 后,设置一个最大的分页值。在查询时,分页数不允许超过该值。

其次,随着翻页深度的增加,查询的效率也会有所降低,但不会导致 OOM,算是可以完成深度查询的任务。原因在于,虽然说通过排序字段,可以很好的定位出下一次翻页的开始位置。但在每次请求时,从头扫描该字段,找到该字段的位置。页数越深,找到该位置的时间也就越长。

Scroll 分页

虽然说 search-after 可以在一定程度上避免深度分页的问题,但在处理大数据量,效率并不高。在一些对实时性要求不高的场景,如利用 Spark 进行大规模计算时。就可以利用 scroll 分页的方式,检索所有数据。

scroll 的请求方式分为两步:

  1. 第一次请求,ES 会返回生成生成的 scroll_id
  2. 之后的请求,不断使用 scroll_id 进行查询,直到所有数据被检索完成。

第一次请求,添加 scroll 标识,并拿到 scroll_id 作为下次请求的参数:

POST /my-index-000001/_search?scroll=1m
{
"size": 100,
"query": {
"match": {
"message": "foo"
}
}
} Response:
{
"_scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAADlx8Wb0VzanNRSENRbUtBQVEzbHltcF9WQQ==",
"took": 0,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
}
}

第二次请求,使用 scroll_id 直到遍历完所有数据:

POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}

对于 Scroll 来说,会返回第一次请求时刻的所有文档,之后文档的改变并不会被查询到,保留的时间通过 scroll 参数指定。在查询性能上,时间和空间复杂度都为 O(1),能以恒定的速度查询完所有数据。

在原理上,相当于第一次查询阶段, 保留所有的 doc id 信息。在随后的查询中,根据的需要的 doc id,在不同的 shard 中拉取不同的文档。和 search-after 相比,省去了每次都要全局排序的过程。

总结

from, size 适用于常见的查询,例如需要支持跳页并实时查询的场景。但查询深度过深时,会有深度分页的问题,造成 OOM.

如果在业务上,可以不选择跳页的方式,可以使用的 search-after 的方式避免深度分页的问题。但如果一定要跳页的话,只能采用限制最大分页数的方式。

但对于超大数据量,以及需要高并发获取等离线场景,scroll 是比较好的一种方式。

参考

深度分页

scroll

阿里-分页方式

ES 分页方案的更多相关文章

  1. es分页搜索

    1.es分页语法GET /_search?from=起始数&size=页面显示条数例如:GET /test_index/test_type/_search?from=0&size=3 ...

  2. agumaster 分页方案

    本文例程下载:https://files.cnblogs.com/files/xiandedanteng/agumaster20200430-1.zip 之前的分页方案有点小瑕疵,这回修正了一下. 控 ...

  3. mysql高效分页方案及原理

    很久以前的一次面试中,被面试官问到这个问题,由于平时用到的分页方法不多,只从索引.分表.使用子查询精准定位偏移以外,没有使用到其它方法. 后来在看其它博客看到了一些不同的方案,也一直没有整理.今天有时 ...

  4. es分页条数限制

    "error": { "root_cause": [ { "type": "query_phase_execution_excep ...

  5. Vue Element Tabe Pager 分页方案

    表格和分页分离的,但是使用中,却是结合在一起的. 分析 有以下方式触发查询: mounted 加载数据. 查询按钮 加载数据. pager 变化加载数据 加载数据函数: loadData 问题 mou ...

  6. Mysql 千万级快速查询|分页方案

    1.简单的 直接查主键id SELECT id FROM tblist WHERE LIMIT 500000,10 2对于有where 条件,又想走索引用limit的,必须创建一个索引,将where  ...

  7. SQL分页语句三方案

    方法一: SELECT TOP 页大小 * FROM table1 WHERE id NOT IN ( SELECT TOP 页大小*(页数-1) id FROM table1 ORDER BY id ...

  8. 分页查询es时,返回的数据不是自己所期望的问题

    在进行es分页查询时,一般都是用sql语句转成es查询字符串,在项目中遇到过不少次返回的数据不是自己所期望的那样时,多半原因是自己的sql拼接的有问题. 解决办法:务必要保证自己的sql语句拼接正确.

  9. 日均5亿查询量的京东订单中心,为什么舍MySQL用ES?

    阅读本文大概需要 8 分钟. 来源:京东技术订阅号(ID:jingdongjishu) 作者:张sir   京东到家订单中心系统业务中,无论是外部商家的订单生产,或是内部上下游系统的依赖,订单查询的调 ...

  10. 【转帖】MySQL用得好好的,为什么要转ES?

    MySQL用得好好的,为什么要转ES? http://developer.51cto.com/art/201911/605288.htm Elasticsearch作为一款功能强大的分布式搜索引擎,支 ...

随机推荐

  1. null和空 not null

    所谓的NULL就是什么都没有,连\0都没有,\0在字符串中是结束符,但是在物理内存是占空间的,等于一个字节,而NULL就是连这一个字节都没有.在 数据库里是严格区分的,任何数跟NULL进行运算都是NU ...

  2. ABP官方文档翻译 8.2 SignalR集成

    SignalR集成 介绍 安装 服务器端 客户端 建立连接 內建特征 通知 在线客户端 PascalCase与CamelCase对比 你的SignalR代码 介绍 ABP中的Abp.Web.Signa ...

  3. 1019. General Palindromic Number (20)

    生词以及在文中意思 forward 向前地 backward 向后地 palindromic 回文的 base 基数(如十进制的10 和二进制的2) numeral system 数制 decimal ...

  4. StackExchange.Redis在net中使用

    redis 官网https://redis.io redis 下载  进入下载页面  https://redis.io/download https://github.com/MicrosoftArc ...

  5. C# - 匿名对象取值

    在new出匿名对象的函数内可以直接调用该匿名对象的属性取值. 可是在其它函数就无法调用匿名对象的属性或方法. 这时,我们可以通过c#的反射机制取值: 文章出处:https://www.cnblogs. ...

  6. mysql指定编码集

    DROP TABLE app_info CREATE TABLE `app_info` ( `app_id` ) NOT NULL COMMENT '应用ID', `) NOT NULL COMMEN ...

  7. 简单使用DESeq做差异分析

    简单使用DESeq做差异分析 Posted: 五月 06, 2017  Under: Transcriptomics  By Kai  no Comments DESeq这个R包主要针对count d ...

  8. 整合Spring+Struts2+Mybatis加spring单元测试等

    前言 自己是在CentOS7的IntelliJ IDEA里开发的,里面中文输入法有问题经常用不了,所以这里用了很多chinglish,希望不要介意: 一:pom依赖 <?xml version= ...

  9. delphi 过滤开头 结尾 全部 空格的函数

    function TrimAnsi(const S: AnsiString): Ansistring; var I, L: Integer; begin L := Length(S); I := ; ...

  10. SwipeRefreshLayout嵌套ScrollView实现下拉刷新

    API doc:http://developer.android.com/reference/android/support/v4/widget/SwipeRefreshLayout.html 首先须 ...