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. Elasticsearch增删改查 之 —— Update更新

    更新操作,一般用这个的,应该不会很多吧!ES本身还是一个倾向于查询检索的框架,对于这种更新的操作,太过频繁总归是不好的. 不过阅读本篇后,你可以使用Script对所有的文档执行更新操作,也可以使用do ...

  2. 一个按比特位拷贝数据的函数copybits

    一个按比特位拷贝数据的函数 没有进行特别的优化.其实还可以在拷贝源开始位置和目标开始位置是2的整数倍位置的时候进行优化. 说明 这个函数用于从src数组首地址跳过sbb个字节,又跳过ssb个比特位,拷 ...

  3. SQL函数:用于将小写的数值翻译成大写的字符串

    --功能:  用于将小写的数值翻译成大写的字符串(支持到分,即小数点后两位)       --入口参数:@decNum------数字型变量    --返回:字符串    --举例:select db ...

  4. 跨服务器的sql使用

    由于想从别的服务器上的数据库导入一些数据过来 经网上查阅,得到 select * from openrowset( 'SQLOLEDB', '服务器名字'; '用户名'; '密码',数据库名字.dbo ...

  5. [译]JDK 6 and JDK 7中的subString()方法

    (说明,该文章翻译自The substring() Method in JDK 6 and JDK 7) 在JDK 6 and JDK 7中的substring(int beginIndex, int ...

  6. [转]RMI方式Ehcache集群的源码分析

    RMI方式Ehcache集群的源码分析   Ehcache不仅支持基本的内存缓存,还支持多种方式将本地内存中的缓存同步到其他使用Ehcache的服务器中,形成集群.如下图所示:   Ehcache支持 ...

  7. C++11新语法糖之尾置返回类型

    C++11的尾置返回类型初衷是为了方便复杂函数的声明和定义,但是当复杂度稍微提升一些的时候很明显能注意到这种设计的作用微乎其微. 首先考虑如下代码: C++ //返回指向数组的指针 auto func ...

  8. Apache软件基金会项目百度百科链接

    Apache软件基金会 顶级项目 ▪ ActiveMQ ▪ Ant ▪ Apache HTTP Server ▪ APR ▪ Beehive ▪ Camel ▪ Cassandra ▪ Cayenne ...

  9. linux使用"userdel 用户名“删除用户的解决办法

    在新建用户的时候我们可以忽略掉给用户分组的信息,而直接使用“useradd 用户名” 来直接添加新用户,所以由此想到删除用户能不能直接使用“userdel 用户名”来删除用户呢?回答是否定的,如此会残 ...

  10. SQL语句中LEFT JOIN、JOIN、INNER JOIN、RIGHT JOIN的区别?

    w3school的一套sql教程: http://www.w3school.com.cn/sql/index.asp left join :左连接,返回左表中所有的记录以及右表中连接字段相等的记录.r ...