ElasticSearch中的深度分页问题

server/2025/3/12 8:42:06/

在使用 ElasticSearch 进行搜索时,很多小伙伴会遇到“深度分页”问题。当需要获取大量的分页数据时,查询性能会急剧下降,甚至导致集群负载过高。这篇文章将深入剖析 ElasticSearch 深度分页的成因、危害,并提供一些常用的优化方案。


一、什么是深度分页?

深度分页的定义

在 ElasticSearch 中,我们可以通过 from 和 size 参数进行分页控制:

GET /my_index/_search
{"from": 0,"size": 10
}
  • from:跳过的记录数
  • size:返回的记录数

例如,from=1000size=10 表示跳过前 1000 条记录,从第 1001 条开始获取 10 条记录。

深度分页就是:

        当 from 参数的值很大时(如 from=10000 以上),ElasticSearch 需要跳过大量数据来获取指定页的内容,这种情况称为深度分页

深度分页的危害

在 ElasticSearch 中,数据是存储在多个分片(Shard)上的,每个分片本质上是一个独立的 Lucene 索引。分页查询会在每个分片上独立执行查询,然后将结果合并和排序。理解深度分页的危害需要从 ElasticSearch 的分布式架构和分片查询流程入手。

分片查询的工作原理

  • 分布式存储
    当一个索引被分配多个分片(例如,5 个分片)时,数据会被均匀分布到不同分片上。每个分片可以分布在不同的节点上。

  • 并行查询
    当进行查询时,ElasticSearch 会将查询请求发送到所有分片,每个分片独立执行查询并返回结果。

  • 结果合并
    收集每个分片返回的结果后,ElasticSearch 在协调节点上进行全局排序和合并,最终返回指定的 fromsize 范围内的结果。

深度分页时分片的执行流程

GET /my_index/_search
{"from": 10000,"size": 10
}
  • 每个分片分别查询
    每个分片都会独立查询出至少 from + size = 10010 条数据。

  • 本地排序
    每个分片对这 10010 条数据进行排序。

  • 返回数据
    每个分片返回排序后的前 10010 条数据到协调节点。

  • 全局排序与裁剪
    协调节点将所有分片的结果合并,然后进行全局排序,并在这些结果中取第 10000 到 10010 条数据,最终返回给用户。

深度分页的危害剖析

我们分析一下上述过程,这会带来一些什么危害呢

内存消耗急剧增加
  • 分片内存开销
    每个分片需要在内存中加载 from + size 条数据(例如 10010 条)。如果分片较多且数据量庞大,这会导致每个分片消耗大量内存。

  • 协调节点内存开销
    协调节点需要收集所有分片返回的大量数据,并在内存中进行全局排序。这可能导致内存溢出(OOM)。

磁盘和 CPU 负载增加
  • 分片读取开销
    为了跳过大量数据,每个分片必须从磁盘中读取大量文档,增加了 I/O 开销。

  • 排序计算开销
    每个分片需要对大量数据进行排序,然后协调节点需要再进行全局排序,增加了 CPU 计算负担。

查询延迟显著增加
  • 线性增长的延迟
    随着 from 值增大,每个分片需要加载和处理的数据量增多,导致查询延迟线性增加。例如,从第 10000 条开始获取数据的延迟会远高于从第 100 条开始获取数据。
集群稳定性风险
  • 节点压力
    大量内存和 CPU 的消耗会增加分片所在节点的负载,严重时可能导致节点性能下降甚至崩溃。

  • 全局影响
    单个深度分页查询可能拖垮整个集群,影响其他正常查询的性能,降低服务可用性。

二、ES中的深度分页方式

如果使用from、size来实现ES的分页查询,我们将会面临深度分页问题,但是我们又想进行深度分页,ES如何解决呢,我们来看下面两种分页方式

Scroll滚动查询

如何解决深度分页

在 ElasticSearch 中,使用传统的分页方式(from + size)进行深度分页会导致性能急剧下降。比如,当你查询第 10000 页的数据时,from 设置为 999900,这意味着 ElasticSearch 需要扫描前 999900 条记录并将其加载到内存中,再丢弃这些记录,只保留最后 100 条。这种操作不仅浪费大量内存,而且延迟非常高。

Scroll 滚动查询通过引入游标机制数据快照,有效地避免了这种大规模跳过数据带来的性能问题。

举个例子:

假设你需要从一张 100 万条记录的大表中导出所有数据。如果使用传统分页方法(from + size),当你查询第 10000 页时,ElasticSearch 需要先扫描和加载前 999900 条数据,再丢弃它们,仅返回你需要的 100 条数据。这就像每次找第 10000 本书,都要从第 1 本开始数到第 10000 本,既费时又耗力。

而使用 Scroll 滚动查询 就像拥有一个记忆书签,每次查询都会在你停下的地方做个标记,下次直接从这个标记处继续,不需要重新扫描前面的数据。它通过创建一个数据快照(Snapshot),固定查询时的数据状态,并维护一个滚动上下文,保证每次返回数据的同时保存位置,避免重复扫描。

使用方式

初始化滚动查询

GET /my_index/_search?scroll=1m
{"size": 100,"query": { "match_all": {} }
}
  • scroll=1m:滚动上下文有效期为 1 分钟。
  • size=100:每次返回 100 条记录。

响应如下:

获取下一批数据

GET /_search/scroll
{"scroll": "1m","scroll_id": "DnF1ZXJ5VGhlbkZldGNoAwAAAAAAA..."
}

响应也是和上面的响应结构相同,只是返回的scroll_id不同 

清理滚动上下文

完成查询后,及时清理滚动上下文以释放内存:

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

优缺点

  • 优点

    • 避免跳过大量数据,性能稳定。
    • 保持数据一致性,适合处理大量数据的场景。
    • 不受 max_result_window 限制,可以获取超过 10000 条的数据。
  • 缺点

    • 内存占用:滚动上下文会占用内存,需要及时释放。
    • 只适用于静态数据:动态更新的数据在滚动过程中不会反映最新变化。
    • 只能向前遍历,不支持随机跳转。

使用场景

  • 数据导出:将索引中的数据批量导出到外部存储。
  • 日志分析:批量分析和处理日志数据。
  • 批处理任务:需要处理大量数据的离线任务,如数据迁移和备份。

滚动查询的工作原理

  1. 创建快照
    第一次查询时,ElasticSearch 为查询结果集创建一个快照,该快照保持数据的一致性,即使在滚动查询过程中有新的数据写入索引,查询结果依然基于快照的数据集,不受新数据影响。

  2. 分批读取
    每次滚动查询返回指定数量的数据(例如 100 条),并生成一个 _scroll_id,作为指向下一批数据的游标。ElasticSearch 会根据这个游标继续从上次结束的位置读取下一批数据,而无需重新扫描前面的数据。

  3. 游标推进
    每次查询会返回新的 _scroll_id,用于后续请求。你只需提供这个 _scroll_id,ElasticSearch 就能知道从哪里继续查找,而不必跳过大量记录。

search_after

如何解决深度分页

search_after 通过使用上一页最后一条记录的排序值来定位下一页的起始点,避免了传统分页需要跳过大量数据的问题。这种方法在需要实时展示大量数据时特别有效。

举个例子:
想象你在一个电子商城里查看商品,商品按价格升序排列。传统的分页就像数到第 10000 个商品,跳过前 9999 个才能展示出来。而 search_after 则是记住第 100 条商品的价格和编号,下次从这个位置直接继续展示第 101 条商品,不用重新扫描前面的商品。

使用方式

第一次查询

GET /my_index/_search
{"size": 10,"sort": [{ "price": "asc" },{ "_id": "asc" }]
}

后续查询

使用上一页最后一条记录的排序值作为 search_after 参数,获取下一页数据。

GET /my_index/_search
{"size": 10,"sort": [{ "price": "asc" },{ "_id": "asc" }],"search_after": [ 199.99, "product_12345" ]
}

优缺点

  • 优点

    • 高效分页:避免跳过大量数据,查询性能稳定。
    • 内存占用小:无状态查询,不需要维护上下文。
    • 实时性强:适合动态数据的实时分页。
  • 缺点

    • 只能向前分页:无法跳转到任意页,只能按顺序向前获取数据。
    • 排序要求:必须有唯一的排序字段组合,确保结果的唯一性。

使用场景

  • 实时数据展示:新闻、商品、用户动态等实时数据的分页查询。
  • 日志检索:按时间戳顺序查询和分析日志。
  • 动态数据分析:需要连续获取新数据并按特定顺序排列的场景。

search_after的工作原理

  1. 排序值标记位置
    每次查询结果都会包含排序字段的值(例如 price_id)。search_after 通过这些值标记当前位置,下次查询时从这个位置继续。

  2. 避免跳过数据
    不使用 from 来跳过记录,而是精确地从上次查询的最后位置开始读取。

  3. 无状态查询
    每次查询都是独立的,不需要在服务器上维持上下文,节省内存。

PIT与search_after结合

为什么 search_after 需要配合 PIT(Point in Time)?

直接使用 search_after 进行分页确实可行,但在某些场景下,它有显著的局限性,而引入 PIT(Point in Time) 可以有效解决这些问题。下面我们来详细解释为什么需要结合 PIT,以及它带来的优势。

直接使用 search_after 的问题

  1. 数据不一致性
    如果在分页查询过程中,索引中的数据发生了变化(例如新增、更新或删除文档),每次查询返回的结果可能会受到这些变化的影响,导致数据不一致。

    示例场景

    • 第一次查询:你获取了排序后第 100 条记录,排序字段的值是 timestamp: 2024-06-16T10:00:00
    • 在第二次查询之前:有一条新的记录被插入,排序值正好在你上次获取的记录之前。
    • 第二次查询:当你使用 search_after 进行下一页查询时,结果集可能会跳过或重复某些记录,导致分页不连续。
  2. 并发查询干扰
    当多个查询或数据写入操作并发进行时,直接使用 search_after 的分页请求很容易受到其他操作的干扰,导致分页结果不可预测。

  3. 无法保证长时间分页
    在长时间的分页操作中,数据的变化可能越来越多,导致分页的连续性和完整性无法保证。

PIT(Point in Time)如何解决这些问题

PIT 通过在查询开始时创建一个固定数据视图来解决上述问题。具体来说:

  1. 数据一致性保证

    • 固定视图:PIT 创建时会固定一个数据视图,即使索引中新增、更新或删除了数据,PIT 下的查询结果集不会受到这些变化的影响。
    • 这意味着所有的分页请求都会基于创建 PIT 时的数据快照进行,保证分页结果的连续性和一致性。

    举例说明
    想象你正在看一张快照照片,这张照片捕捉了某一瞬间的所有信息。即使在现实中事物发生了变化,照片里的信息依然保持不变。PIT 就像这样一张数据的快照,让你在分页时始终看到快照时刻的结果。

  2. 避免并发干扰

    • PIT 创建的数据视图是独立的,不会受到其他并发查询或写入操作的干扰。因此,即使在高并发场景下,你的分页查询也能保持稳定。
  3. 长时间分页安全

    • 通过设置合适的 keep_alive 时间(如 1m5m),PIT 可以保持长时间有效,支持长时间的分页操作而不会因为数据变化导致分页结果混乱。

PIT+search_after使用方式

1.创建 PIT 视图
创建一个固定的数据视图,并设置有效期(如 1 分钟):

POST /my_index/_search?keep_alive=1m
{"size": 10,"sort": [{ "timestamp": "asc" },{ "_id": "asc" }]
}

返回的 pit_id 用于后续分页请求。

2.分页查询
使用 pit_idsearch_after 进行分页:

POST /_search
{"size": 10,"pit": {"id": "46ToAwMDaWR...ZmZjZTg","keep_alive": "1m"},"sort": [{ "timestamp": "asc" },{ "_id": "asc" }],"search_after": [ "2024-06-16T10:00:00", "record_123" ]
}

3.关闭 PIT 视图
分页完成后,关闭 PIT 释放资源:

DELETE /_pit
{"id": "46ToAwMDaWR...ZmZjZTg"
}

http://www.ppmy.cn/server/151562.html

相关文章

JavaScript 事件

JavaScript 事件 JavaScript 事件是前端开发中不可或缺的一部分,它们允许开发者创建动态、交互式的网页。本文将深入探讨 JavaScript 事件的概念、用法和最佳实践。 什么是 JavaScript 事件? JavaScript 事件是发生在 HTML 元素上的交互动作。当用户或浏览器执行某些操作时…

Python-基于Pygame的小游戏(滑雪大冒险)(一)

前言:《滑雪大冒险》是一款休闲跑酷类游戏,玩家需要在游戏中与雪崩竞速,同时避开雪地上的各种障碍物,如石头、草丛和冰凌等。游戏的核心玩法是在雪山上滑行,避免被身后的雪崩吞没,并尽可能向前推进。与传统…

supervision - 好用的计算机视觉 AI 工具库

Supervision库是一款出色的Python计算机视觉低代码工具,其设计初衷在于为用户提供一个便捷且高效的接口,用以处理数据集以及直观地展示检测结果。简化了对象检测、分类、标注、跟踪等计算机视觉的开发流程。开发者仅需加载数据集和模型,就能轻…

C# 入门编程

<div id"content_views" class"htmledit_views"><p> 无论你是编程新手&#xff0c;还是想要深化.NET技能的开发者&#xff0c;本文都将为你提供一条清晰的学习路径&#xff0c;从<a href"https://so.csdn.net/so/search?qC%23…

Redis API(springboot整合,已封装)

目录 结构maven导包 pom.xmlapplication.ymlredis 配置类编写Service方法调用示例 结构 maven导包 pom.xml 依赖项主要添加如下 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId…

信息化基础知识——电子商务(山东省大数据职称考试)

大数据分析应用-初级 第一部分 基础知识 一、大数据法律法规、政策文件、相关标准 二、计算机基础知识 三、信息化基础知识 四、密码学 五、大数据安全 六、数据库系统 七、数据仓库. 第二部分 专业知识 一、大数据技术与应用 二、大数据分析模型 三、数据科学 电子商务 大数…

鸿蒙app封装 axios post请求失败问题

这个问题是我的一个疏忽大意&#xff0c;在这里记录一下。如果有相同问题的朋友&#xff0c;可以借鉴。 当我 ohpm install ohos/axios 后&#xff0c;进行简单post请求验证&#xff0c;可以请求成功。 然后&#xff0c;我对axios 进行了封装。对axios 添加请求拦截器/添加响…

Android14 AOSP 允许system分区和vendor分区应用进行AIDL通信

在Android14上&#xff0c;出于种种原因&#xff0c;system分区的应用无法和vendor分区的应用直接通过AIDL的方法进行通信&#xff0c;但是项目的某个功能又需要如此。 好在Binder底层其实是支持的&#xff0c;只是在上层进行了屏蔽。 修改 frameworks/native/libs/binder/Bp…