大数跨境
0
0

ElasticSearch深度分页问题及其解决方案详解

ElasticSearch深度分页问题及其解决方案详解 Owen跨境
2025-10-22
5
导读:这个问题如此经典,以至于几乎每个使用 ES 的开发者都会在面试或实践中遇到。今天,我们就来彻底拆解它,不仅告诉你“是什么”和“为什么”,更给你一套完整的“怎么办”的解决方案。

当你用 Elasticsearch 实现类似“淘宝订单列表”或者“新闻资讯历史”这类需要翻到第100页、第1000页之后的功能时,你可能已经掉进了一个著名的性能陷阱——深度分页

这个问题如此经典,以至于几乎每个使用 ES 的开发者都会在面试或实践中遇到。今天,我们就来彻底拆解它,不仅告诉你“是什么”和“为什么”,更给你一套完整的“怎么办”的解决方案。

一、 什么是深度分页?一个看似简单的需求

假设你的 ES 索引中有 1000 万条订单记录。前端需要一个分页列表,每页 10 条。当用户输入页码,点击“跳转”时,会发生什么?

  • 用户点击 第 1 页:系统返回第 1-10 条记录。

  • 用户点击 第 100 页:系统返回第 990-1000 条记录。

  • 用户点击 第 10000 页:系统返回第 99990-100000 条记录。

这个“跳转到非常靠后的页面”的行为,就是深度分页

二、 为什么深度分页是个“问题”?—— 理解 from + size 的工作原理

绝大多数初学者实现分页,会毫不犹豫地使用 from 和 size 参数:

GET /my_index/_search{  "from"9990,   // 从第9990条记录开始  "size"10,      // 取10条  "query": { ... }}

正是这个看似无害的 from + size,成了性能的杀手。

ES 的分页原理和 MySQL 的 LIMIT offset, size 有本质区别。它的工作流程分为两个阶段:

  1. Query 阶段

    • 协调节点将请求广播到所有相关分片(假设有 5 个主分片)。

    • 每个分片需要在本地独立地执行查询,并各自计算出满足条件的前 from + size 条数据的排序结果

    • 对于 from=9990, size=10,每个分片需要生成自己分片内的前 10000 条数据(9990 + 10)的排序结果,而不仅仅是10条!

    • 然后,每个分片将这 10000 条数据的元数据(_id_score 等)返回给协调节点。

  2. Fetch 阶段

    • 协调节点收到 5 个分片 × 10000 条数据 = 50000 条数据的元数据。

    • 然后,它需要对这 50000 条数据进行全局排序,找出排名前 10000 条(从第9990到第10000条)。

    • 最后,协调节点再根据这最终10条结果的元数据,去对应的分片上拉取完整的文档数据(_source),返回给客户端。

问题的核心在于:

  • 资源消耗与 from + size 成正比。你想翻到第 10000 页,就需要在每个分片排序和保存大量的中间结果。

  • 巨大的网络开销:协调节点需要收集和排序海量的元数据。

  • 内存压力:这些中间结果通常需要存储在内存中(JVM Heap),当并发多个深度分页请求时,极易导致协调节点 OOM(内存溢出) 而崩溃。

因此,ES 默认通过 index.max_result_window 参数(通常为 10000)来限制 from + size 的深度,这是一种保护机制。

三、 解决方案:如何优雅地实现深度分页?

面对这个难题,我们有多种武器,关键是根据场景选择最合适的。

方案一:Scroll API —— 快照式遍历(适用于后台大量数据导出)

Scroll 的设计初衷并非为了实时分页,而是为了一次性处理大量数据(如全量导出、数据迁移、离线分析)。

  • 工作原理:第一次查询时,ES 会创建一个数据快照,并返回一个 scroll_id。后续的分页不再使用 from,而是通过这个 scroll_id 来不断获取下一批结果,就像游标一样。

  • 优点:避免了重复排序和计算,性能极高。

  • 缺点

    • 非实时:快照建立后,对索引的新增、修改、删除不会反映到快照中。

    • 占用资源:快照会一直占用资源,直到超时释放。不适合高并发实时请求。

  • 代码示例

// 1. 初始化,创建快照POST /my_index/_search?scroll=5m // 快照保持5分钟{  "size"100}// 返回一个 `scroll_id`
// 2. 后续遍历POST /_search/scroll{  "scroll""5m",  "scroll_id""DnF1ZXJ5VGhlbkZldGNoBQAAAAAA..."}

方案二:Search After —— 实时游标分页(推荐用于代替 from+size

这是目前解决实时深度分页的最佳实践。它的思想是:“既然不能跳页,那我们就一页一页地往下翻”。

  • 工作原理:使用上一页结果中的一组排序值(Sort Values)作为“游标”,来定位下一页的起始位置。

  • 优点

    • 实时:能够查询到最新写入的数据。

    • 高性能:避免了 from + size 的全局排序问题,每次只取 size 条数据。

  • 缺点

    • 只能逐页下行:不支持“跳转到任意页码”,类似手机App上的“上拉加载更多”。这是它最大的限制。

    • 要求排序值唯一:排序条件中必须包含一个唯一字段(如 _id),以防止翻页时出现结果重复或丢失。

  • 代码示例

// 第一页GET /my_index/_search{  "size"10,  "sort": [    {"timestamp""desc"},    {"_id""asc"// 唯一性保证  ]}// 返回结果中会包含一个 "sort" 数组
// 第二页及以后,使用上一页最后一条的 sort 值GET /my_index/_search{  "size"10,  "search_after": [1640995200000"abc123"], // 上一页最后一条的 sort 值  "sort": [    {"timestamp""desc"},    {"_id""asc"}  ]}

方案三:业务层面优化 —— 禁止任意跳页(产品设计上规避)

这是最简单、最有效的方案。在很多场景下,用户根本不需要跳到第 10000 页。

  • “无限滚动”:像抖音、微博那样,不断下拉加载,本质上就是 Search After 的 UI 体现。

  • “分段跳页”:Google 搜索那样,只允许你跳转到附近的前后10页。超出范围则提示用户使用更精确的搜索条件。

  • “仅提供上一页/下一页”:很多后台管理系统采用此方式。

四、 总结与选型建议

方案
核心原理
适用场景
缺点
from + size
全局排序,内存开销大
浅分页(前1000条)
深度分页时性能灾难
Scroll
快照游标,遍历数据
后台大量数据导出、离线处理
非实时,占用资源
Search After
实时游标,顺序翻页
实时深度分页、App/Web无限加载
不支持跳页,需要唯一排序
业务优化
从产品设计上规避问题
绝大多数C端用户场景
无法满足所有需求

黄金法则:

  1. 首先考虑产品设计,能否避免深度跳页?

  2. 如果必须深度分页,且是顺序浏览首选 Search After

  3. 如果是一次性的海量数据拉取,使用 Scroll

  4. 永远不要在生产环境对用户请求使用大数值的 from + size

理解了这些,下次当产品经理提出“我们要支持订单列表跳转到任意页”时,你就可以自信地走上前,和他聊聊用户体验与技术实现之间的平衡艺术了。

图片文末福利图片


《2025软件测试全套资料》

我整理了3份独家资料助你起步:
1️⃣ 《软件测试学习路线图》:按天拆解学习计划,拒绝迷茫
2️⃣ 《软件测试视频教程》:少走半年弯路
3️⃣ 《软件测试面试文档》:针对性补充技能

扫描下方二维码免费领取

图片


【声明】内容源于网络
0
0
Owen跨境
跨境分享汇 | 持续提供优质内容
内容 44793
粉丝 1
Owen跨境 跨境分享汇 | 持续提供优质内容
总阅读208.6k
粉丝1
内容44.8k