Elasticsearch 的深分页问题与 MySQL 的深分页类似。深分页指的是在查询大量数据时,跳过很多条记录才能获取目标页的结果,从而导致性能显著下降。在 Elasticsearch 中,当使用 from
和 size
参数进行分页查询时,深分页会造成大量的资源消耗,包括内存和 CPU,从而导致查询性能下降,甚至可能导致节点内存溢出。
一、Elasticsearch 深分页问题的原因分析
- from 和 size 参数的工作机制:
from
参数用于指定跳过的记录数,size
参数指定返回的记录数。对于深分页(例如from=100000
,size=10
)的请求,Elasticsearch 需要读取、过滤和排序大量的文档,即使最终只返回少量结果。- 当
from
值很大时,Elasticsearch 会扫描和排序大量文档,并将它们加载到内存中,然后跳过指定数量的文档,直到返回所需的size
条结果。
- 内存消耗:
- Elasticsearch 的分页查询会将文档加载到内存中,并使用优先队列对文档进行排序。当
from
值较大时,需要将更多的文档加载到内存并排序,导致内存使用量激增。 - 深分页操作可能会占用大量堆内存,导致
OutOfMemoryError
,从而使 Elasticsearch 节点宕机。
- Elasticsearch 的分页查询会将文档加载到内存中,并使用优先队列对文档进行排序。当
- 性能瓶颈:
- 当查询需要处理大量数据时,CPU 和 I/O 负载会显著增加,影响整个集群的查询性能。
- 当并发深分页请求增多时,集群的资源耗尽,响应时间急剧上升,影响其他正常请求的处理。
二、Elasticsearch 深分页的解决方案
为了解决 Elasticsearch 的深分页问题,可以采取以下几种优化方案:
方案 1:使用 search_after
进行滚动分页
search_after
是一种基于排序的分页方法,适合深度分页。search_after
不依赖 from
跳过文档,而是使用上一页最后一条记录的排序值来定位下一页的起点,因此避免了扫描和跳过大量记录。
示例:
假设我们按 id
字段进行排序,并获取第一页结果:
jsonCopy codeGET /employees/_search
{
"size": 10,
"sort": [{ "id": "asc" }]
}
返回结果后,将最后一条记录的 id
作为 search_after
参数的值,继续请求下一页:
jsonCopy codeGET /employees/_search
{
"size": 10,
"sort": [{ "id": "asc" }],
"search_after": [last_id]
}
原理: search_after
基于上一页最后一个文档的排序值来定位下一页的起点,无需跳过大量文档,显著减少了深分页的内存消耗。
优缺点:
- 优点:适合大数据集的深分页,减少内存和 CPU 消耗。
- 缺点:需要按排序字段获取上一页的最后一个值,且只能顺序分页,无法随机跳转到特定页。
方案 2:使用 scroll
实现游标分页
scroll
是 Elasticsearch 提供的滚动分页机制,适合处理超大量数据的批量读取。scroll
查询会生成一个游标(cursor),用于保持查询的上下文,避免重复扫描和跳过大量数据。
示例:
首先创建一个滚动上下文(游标),获取第一页:
jsonCopy codeGET /employees/_search?scroll=1m
{
"size": 1000,
"query": { "match_all": {} }
}
返回的结果中包含一个 _scroll_id
,使用该 _scroll_id
获取下一批数据:
jsonCopy codeGET /_search/scroll
{
"scroll": "1m",
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAsQ1WbmlOV1FpN2h2SEZnQ0U5b0haU3c"
}
原理: scroll
查询将初始查询的结果快照保存下来,并根据快照依次读取分页结果,避免重复的查询和排序计算。
优缺点:
- 优点:适合大规模数据的批量读取,性能稳定。
- 缺点:
scroll
不适合实时分页查询,因为数据快照在整个查询过程中保持不变,不反映实时数据更新。使用完后需及时释放 scroll 资源。
方案 3:使用 search_after
+ 分页 ID 或者分区键的组合方式
对于需要深分页的实时数据,可以根据业务需求分区数据。例如,先按某个分区键(如日期、类别)过滤数据,然后结合 search_after
滚动分页查询,减少每次查询的数据量。
示例:
假设需要查询员工表中不同部门的员工列表,可以先按 department
过滤,再使用 search_after
滚动分页:
jsonCopy codeGET /employees/_search
{
"query": { "term": { "department": "sales" } },
"size": 10,
"sort": [{ "id": "asc" }]
}
原理: 通过分区过滤,将大数据量分散到不同分区进行分页,避免深分页时的全量扫描。然后结合 search_after
实现滚动分页。
优缺点:
- 优点:减少每次查询的文档数量,适合对实时数据进行分区。
- 缺点:需要对业务数据进行分区和过滤,适用性受限。
方案 4:替代分页,使用 composite aggregation
分页聚合
在聚合查询场景下,composite aggregation
可以替代传统分页。composite aggregation
支持深度分页,且性能较稳定,适用于聚合查询。
示例:
按 department
字段分页聚合,获取每页聚合结果:
jsonCopy codeGET /employees/_search
{
"size": 0,
"aggs": {
"departments": {
"composite": {
"size": 10,
"sources": [
{ "department": { "terms": { "field": "department" } } }
]
}
}
}
}
在第一次请求返回的结果中包含 after_key
值,用于请求下一页:
jsonCopy codeGET /employees/_search
{
"size": 0,
"aggs": {
"departments": {
"composite": {
"size": 10,
"sources": [
{ "department": { "terms": { "field": "department" } } }
],
"after": { "department": "sales" } -- 下一页起点
}
}
}
}
原理: composite aggregation
使用 after
参数来定位下一页的起点,避免了大数据量聚合查询时的内存消耗问题。
优缺点:
- 优点:适用于聚合场景,性能稳定,支持深分页。
- 缺点:只能用于聚合查询,适用场景较为有限。
三、各解决方案的适用场景总结
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
search_after | 实时数据顺序分页 | 支持顺序深分页,减少内存消耗 | 仅适用于顺序分页,无法随机跳转页数 |
scroll | 批量数据读取,非实时分页 | 快照查询,适合大规模数据批量读取 | 不适合实时查询,需释放游标以节省资源 |
分区过滤 + search_after | 实时数据大规模分页 | 降低单次查询的数据量,减少深分页的扫描 | 需根据业务分区,适用性受限 |
composite aggregation | 聚合查询深分页 | 支持深分页的聚合查询,性能稳定 | 仅适用于聚合查询场景 |
四、总结
Elasticsearch 的深分页问题主要是由 from
和 size
的机制导致的。为了解决这一问题,可以使用 search_after
、scroll
、分区查询、composite aggregation
等方法进行优化。各方案的选择应根据具体业务需求和场景进行综合考量,合理利用 Elasticsearch 的分页机制,以实现性能与数据量的平衡。