分布式专题——51 ES 深度分页问题及其解决方案详解

1 深度分页概述

  • 深度分页 是指在处理大数据集查询时,用户尝试访问多页数据中较后面的页面时遇到的性能问题。例如:尝试访问排序后的数据列表的第1000页或更后面的页面时,数据库需要先跳过前面数十万条记录,这一过程涉及大量数据扫描和排序,会极大增加数据库查询负载,成为性能瓶颈;

  • Elasticsearch 的分页查询流程大致分为以下三步:

    • 步骤1:数据存储在各个分片(shard)中,协调节点将查询请求转发给各个节点;各个节点执行搜索后,将排序后的前N条数据返回给协调节点;

    • 步骤2:协调节点汇总各个分片返回的数据,再次排序,最终返回前N条数据给客户端;

    • 步骤3:这个流程会导致深度分页问题------翻页越多,性能越差,甚至导致ES出现OOM(内存溢出)

  • 在分布式系统中,对结果排序的成本随分页的深度成指数上升

  • 以"从10万名高考生中查询成绩为10001-10100位的100名考生信息"为例:

    • 数据分片情况:假设数据分为5个分片(shard 0~shard 4),每个分片存储2万条文档(docs);

    • 查询过程:为了获取第10001-10100位的考生,ES需要在每个分片上都查询出前10100条数据 ,然后将这些分片的结果汇总到协调节点,进行二次排序后,才能筛选出目标的100条数据;

    • 性能问题:二次排序过程发生在JVM堆内存(heap)中。单次查询的数据量取决于"查询的是第几条数据"而非"查询了几条数据"------比如查第10001-10100条,却要先取出前10100条做二次排序。因此,查询的排序越靠后,堆内存压力越大,越容易触发OOM;频繁的深分页查询还会导致频繁的FullGC(全量垃圾回收),进一步影响性能;

  • ES为避免用户因不了解内部原理而误操作,设置了一个阈值max_result_window ,默认值为10000。其作用是保护堆内存不被错误操作导致溢出,限制了默认情况下分页查询能获取的最大数据范围。

2 深度分页不推荐使用from+size

  • 在Elasticsearch中,分页查询通过fromsize两个参数实现:

    • from:指定从结果集中的第几条数据开始返回(类似"起始偏移量");

    • size:指定返回数据的数量(类似"每页条数");

  • :查询第一页5条数据的请求如下

    json 复制代码
    GET /employee/_search
    {
      "query": {
        "match_all": {}
      },
      "from": 0,
      "size": 5
    }
  • 深度分页的限制

    • from + size超过max_result_window(默认值为10000)时,会触发报错。例如from=10000,size=1时,from+size=10001超过阈值,报错信息为"from + size must be less than or equal to [10000] but was [10001]"

    • 这是因为Elasticsearch通过index.max_result_window限制最大分页窗口,避免因大数据量召回导致系统性能崩溃;

  • 针对from+size深度分页的限制,有两种可行方案:

    • 方案一 :对于大型数据集,采用scroll API召回数据(后续会详细分析);

    • 方案二 :调大index.max_result_window的默认值。示例代码如下:

      json 复制代码
      PUT /employee/_settings
      {
        "index.max_result_window": 20000
      }
  • from+size的优缺点

    • 优点 :支持随机翻页(如直接跳转到第5页);

    • 缺点

      • 受限于max_result_window设置,无法无限制翻页;
      • 存在深度分页问题,越往后翻页性能越慢;
  • from+size的适用场景

    • 适合小型数据集 ,或从大数据集中返回Top NN≤10000)结果集的业务场景;

    • 适合主流PC搜索引擎中支持随机跳转分页的场景(如百度搜索结果的多页随机跳转);

  • 官方明确建议:避免使用from+size过度分页或一次请求太多结果。核心原因是:搜索请求跨多个分片时,每个分片需加载"命中内容+先前页面的命中内容"到内存,分页越多,内存和CPU使用率越显著,易导致性能下降甚至节点故障。

3 深度分页问题的常见解决方案

3.1 尝试避免使用深度分页

  • 解决深度分页问题的最佳办法是从业务层面避免深度分页。例如全球主流搜索引擎(谷歌、百度)和电商平台(淘宝),都通过功能限制或结果截断的方式,减少用户对深度分页的使用;

  • 例1:百度在搜索结果分页条中删除了"跳页"功能(仅保留"下一页"和前几页的有限跳转),且对极深分页进行限制。例如搜索"Elasticsearch"时,翻到第20页后就无法再继续往下翻,以此避免用户触发深度分页检索,从源头降低系统性能压力;

  • 例2:淘宝虽未删除"跳页"功能,但对商品搜索结果的分页深度做了限制------仅展示前100页的数据 。这一逻辑和Elasticsearch中max_result_window的作用本质一致,都是通过限制分页深度,避免因深度分页导致的系统资源过度消耗;

  • 例3:手机端APP(如各类内容、电商APP)普遍采用**"下拉加载更多"的交互形式**,连分页条都没有,用户只能逐次"下一页"式加载,从交互层面避免了深度跳页带来的性能问题。

3.2 Scroll Search(滚动查询)

3.2.1 概述

  • Scroll API 可从单个搜索请求中检索大量结果(甚至所有结果),工作方式类似传统数据库的游标(cursor) 。但需注意,scroll滚动遍历查询是非实时的,数据量大时响应时间可能较长;

    官方文档:[Paginate search results | Elasticsearch Guide 8.14] | Elastic

  • ES7之后,官方不再建议使用scroll API进行深度分页 。若需分页检索超过 Top 10,000+ 结果,推荐使用 search_after 参数配合时间点(PIT);

3.2.2 实现步骤

  • 第一次进行Scroll查询 :指定检索语句的同时,设置scroll上下文保留时间。此时返回的结果是"初始搜索请求时索引的快照",后续对文档的增删改仅影响之后的搜索请求;

    • 以Kibana航班测试数据集为例:

      json 复制代码
      GET /kibana_sample_data_flights/_search?scroll=5m
      {
        "query": {
          "term": {
            "OriginWeather": "Sunny"
          }
        },
        "size": 100
      }
      • scroll=5m:表示游标查询窗口保留5分钟(即快照结果的缓存有效时间);

      • size=100:表示单次返回100条数据;

    • 返回结果:

  • 向后翻页 :持续获取数据。通过上一次请求返回的 _scroll_id,继续发起请求获取后续数据,直到没有结果返回为止:

    • 例:

      json 复制代码
      GET /_search/scroll
      {
        "scroll": "5m",
        "scroll_id": "上一次请求返回的_scroll_id的值"
      }
    • 多次执行该操作,直到无数据返回,即可完成全量数据遍历。这种方式限制了单次内存消耗,更安全高效;

  • 删除游标scroll :scroll超时后会自动删除上下文,但为了避免资源浪费,不再使用时应主动删除scroll上下文;

    json 复制代码
    DELETE /_search/scroll
    {
      "scroll_id": "需要删除的_scroll_id的值"
    }

3.2.3 优缺点与适用场景

  • 优点:支持全量遍历 ,是检索大量文档的重要方法(但单次遍历的size值不能超过max_result_window的大小);

  • 缺点:

    • 响应是非实时的;

    • 保留上下文需要足够的堆内存空间;

    • 需要通过更多网络请求才能获取所有结果;

  • 适用场景:

    • 大量文档检索:需检索大量文档(甚至全量召回数据)时,scroll查询是良好选择;

    • 大量文档的数据处理:适合对大量文档进行数据处理(如索引迁移、数据导入其他技术栈)。

  • 注意:

    • ES7.x之后不建议使用scroll API进行深度分页

    • 若需分页检索并获取超过10000条结果,推荐使用search_after参数配合时间点(PIT)。

3.3 search_after查询

3.3.1 概述

  • search_after查询的核心逻辑是以前一页结果的排序值作为参照点,进而检索与该参照点相邻的下一页匹配数据。这种方式在处理大规模数据分页时更高效实用;

  • 使用search_after的前提是:后续多个请求需返回与第一次查询相同的排序结果序列。即便后续有新数据写入,也不会影响原有结果的结构;

  • scroll API 适用于高效的深度滚动,但滚动上下文成本高昂,不建议将其用于实时用户请求。而search_after参数通过提供一个活动光标来规避这个问题,这样可以使用上一页的结果来帮助检索下一页;

    官方文档:[Paginate search results | Elasticsearch Guide 8.14] | Elastic

  • 为保障搜索过程中数据的一致性,search_after需结合PIT(时间点) 功能实现。PIT是Elasticsearch 7.10版本后新增的特性,用于保存特定时间点的索引状态(轻量级视图)。

3.3.2 实现步骤

  • 获取索引的PIT

    • 使用search_after需要具有相同査询和排序值的多个搜索请求。如果在这些请求之间发生刷新,结果的顺序可能会发生变化,从而导致跨页面的结果不一致。为防止出现这种情况,可以创建一个时间点(PIT)以保留搜索中的当前索引状态;

    • 例:

      json 复制代码
      POST /kibana_sample_data_flights/_pit?keep_alive=5m
      • keep_alive=5m:表示PIT的保留时间为5分钟,超时后Elasticsearch会删除该视图并报错(如search_context_missing_exception);

      • 返回结果:会生成一个id(即PIT的唯一标识);

  • 根据PIT首次查询

    • 基于PIT创建基础查询,设置分页条件(查询、排序、分页大小等);

    • 示例代码

      json 复制代码
      GET /_search
      {
        "query": {
          "term": {
            "OriginWeather": "Sunny"
          }
        },
        "pit": {
          "id": "第一步返回的PIT id",
          "keep_alive": "1m"
        },
        "size": 10,
        "sort": [
          {
            "timestamp": "asc"
          }
        ]
      }
    • 响应结果中,每个文档的最后会包含排序值+决胜字段(tiebreaker)

      • 排序值:如上图示例代码的返回结果中的1723434063000(对应sort指定的字段值);
      • 决胜字段(tiebreaker):如上图示例代码的返回结果中的130,是文档的唯一标识,确保分页无丢失或重复;
  • 根据search_after和PIT实现后续翻页

    • 以上一页最后一个文档的sort字段值(含tiebreaker)作为search_after参数,结合PIT进行后续分页;

    • 示例代码

      json 复制代码
      GET /_search
      {
        "query": {
          "term": {
            "OriginWeather": "Sunny"
          }
        },
        "pit": {
          "id": "PIT id",
          "keep_alive": "5m"
        },
        "size": 10,
        "sort": [
          {
            "timestamp": "asc"
          }
        ],
        "search_after": [1723434063000, 130]
      }
    • 特点:仅支持向后翻页,不支持随机跳页;

  • 优点:不严格受制于max_result_window,可无限向后翻页(单次请求size仍受max_result_window限制,但总结果集可超过该阈值);

  • 缺点:仅支持向后翻页,不支持随机翻页,更适合手机端"下拉加载"类场景(如今日头条的分页搜索)。

4 ES的三种分页方式总结

分页方式 性能 优点 缺点 适用场景
from + size 支持随机翻页 受制于max_result_window设置,不能无限制翻页;存在深度翻页问题,越往后翻页越慢 需要随机跳转不同页(PC端主流搜索引擎);在10000条数据之内分页显示
scroll 支持全量遍历,但单次遍历的size值不能超过max_result_window的大小 响应是非实时的;保留上下文需要具有足够的堆内存空间;需要通过更多的网络请求才能获取所有结果 需要遍历全量数据
search_after 不严格受制于max_result_window,可以无限地往后翻页 只支持向后翻页,不支持随机翻页 仅需要向后翻页;超过10000条数据,需要分页
相关推荐
FreeBuf_2 小时前
思科CCX软件曝高危RCE:攻击者可利用Java RMI和CCX Editor获取root权限
java·网络·安全
_esther_2 小时前
【字符串String类大集合】构造创建_常量池情况_获取方法_截取方法_转换方法_String和基本数据类型互转方法
java
lkbhua莱克瓦242 小时前
Java基础——集合进阶5
java·开发语言·集合·泛型
WZTTMoon2 小时前
Spring 配置解析与 @Value 注入核心流程详解
java·spring boot·spring
程序定小飞3 小时前
基于springboot的健身房管理系统开发与设计
java·spring boot·后端
wxin_VXbishe3 小时前
springboot在线课堂教学辅助系统-计算机毕业设计源码07741
java·c++·spring boot·python·spring·django·php
信仰_2739932433 小时前
RedisCluster客户端路由智能缓存
java·spring·缓存
兰雪簪轩3 小时前
仓颉语言内存布局优化技巧:从字节对齐到缓存友好的深度实践
java·spring·缓存
南山十一少3 小时前
基于 Spring Boot 与 RabbitMQ 的分布式消息通信机制设计与实现
spring boot·分布式·java-rabbitmq