什么是 Elasticsearch 的深度分页问题?如何解决?

#中间件#面试八股

什么是 Elasticsearch 的深度分页问题?如何解决?

结论先行:from + size 分页要求每个分片都返回前 from + size 条,协调节点再归并取第 from 到 from+size 条。翻到第 1000 页时,5 个分片各要返回 1 万条,内存与 CPU 随页码线性增长,因此有了 index.max_result_window(默认 10000)的保护阈值。解法是 search_after(推荐)、scroll(导出场景)、PIT + search_after(实时且一致)。

一、为什么越翻越慢

from=9990, size=10, 5 个分片

分片1 ── 取前 10000 条 ──┐
分片2 ── 取前 10000 条 ──┤
分片3 ── 取前 10000 条 ──┼──> 协调节点归并 50000 条 ──> 丢弃前 9990 条 ──> 返回 10 条
分片4 ── 取前 10000 条 ──┤
分片5 ── 取前 10000 条 ──┘

代价与 from + size 成正比,与 size 无关。这也是为什么“每页 10 条”翻到第 1000 页比“每页 100 条”翻到第 100 页更慢。

二、四种方案对比

方案原理实时性跳页内存适用
from + size各分片返回前 N 条归并实时支持随页码增长前 10000 条内的常规分页
search_after用上一页最后一条的排序值做游标实时不支持恒定无限下拉、滚动加载
scroll生成快照上下文,逐批拉取快照不支持上下文占堆全量导出、离线任务
PIT + search_after时间点快照 + 游标快照内一致不支持中等需要一致性的深翻页

三、search_after 的正确用法

必须有一个全局唯一的排序键,通常用“业务字段 + _id”组合:

GET /article/_search
{
  "size": 10,
  "sort": [
    { "publish_time": "desc" },
    { "_id": "asc" }
  ],
  "search_after": [1735660800000, "a1001"],
  "track_total_hits": false
}

下一页把最后一条的 sort 值原样放进 search_after。要点:

  • sort 必须唯一且稳定,只按时间排序会在同一毫秒的多条记录间跳漏或重复。
  • 不能跳页,只适合“下一页”。
  • track_total_hits: false 避免每次精确统计总量,能显著省时。

四、scroll 与 PIT

POST /article/_search?scroll=1m
{ "size": 1000, "sort": ["_doc"] }

POST /_search/scroll
{ "scroll": "1m", "scroll_id": "DXF1ZXJ5..." }

DELETE /_search/scroll
{ "scroll_id": "DXF1ZXJ5..." }

scroll 会持有段引用,期间不释放合并资源,长时间不清理会导致堆压力与磁盘膨胀,用完必须显式删除。

PIT(Point In Time)是 7.10 之后推荐的方式,为查询打开一个时间点视图,再配合 search_after:

POST /article/_pit?keep_alive=1m

GET /_search
{
  "size": 1000,
  "pit": { "id": "46ToAw..." , "keep_alive": "1m" },
  "sort": [{ "publish_time": "desc" }, { "_id": "asc" }],
  "search_after": [1735660800000, "a1001"]
}

PIT 只固定查询视图,不像 scroll 那样占用完整上下文,资源开销更小。

五、工程上的折中

做法说明
限制最大页数只允许翻到 100 页,超出引导用户改筛选条件
提高窗口index.max_result_window 调大只是推迟问题,且更耗内存
改为“跳页 + 游标”前 N 页用 from+size,之后切 search_after
按时间分区只查最近 30 天,缩小候选集

常见追问

  • 追问:为什么默认限制 10000?每个分片要为 from+size 排序并保留结果,页码越大堆压力越大,这是保护性阈值而非性能边界。
  • 追问:search_after 能跳页吗?不能,它是有状态的游标;要跳页只能回退到 from+size 或改用其他存储。
  • 追问:scroll 和 PIT 怎么选?导出全量用 scroll(简单、快照稳定);交互式深翻页用 PIT + search_after(资源更省)。

小结

  • 深度分页的根因是“每个分片都要返回前 from+size 条”。
  • 默认 10000 的窗口是保护阈值,调大只是延后爆炸。
  • 无限下拉用 search_after,全量导出用 scroll,一致性深翻页用 PIT。
  • 排序键必须唯一稳定,否则会漏数据或重复。
笔记加载中…