关于SpringBoot集成ES Scroll API(滚动查询)的实践
创始人
2024-04-21 20:54:22
0

待到秋来九月八,我花开后百花杀

  • 背景:
  • 大胆尝试实践:
  • 学习
  • 踩坑
  • 最终解决

背景:

那是年初在某个交付项目,从用户侧获知了一个elastic search作为分布式数据库的一个瓶颈,那就是单次查询量超过了ES的默认单次查询上限10000。

在大部分业务下,为了执行ES的数据查询,开发者往往都直接使用了query某个条件获取数据,这些条件对应的数据大多都不会超过10000,因此在一般测试下难以发现这类问题。但系统经过经年累月的使用,数据量在不断增长,又因业务需求不可清除旧数据的情况下,这类问题就诞生了。
在这里插入图片描述

于是,我想到Elasticsearch 中,传统的分页查询使用from+size的模式,类似如下语句:

GET //_search
{"from": 0,"size": 10,"query": {"match_all": {}}
}

那我们可以大可尝试使用while的方式,每次在拼接query时改变from的值为from+size,那么我每次拿到的就是下一个分页,如此往复,那也可以实现无穷尽也。

大胆尝试实践:

public void search() {int from = 0;long total = 0;int size = 100;while (true) {NativeSearchQuery nativeSearchQuery = new NativeSearchQueryBuilder().withPageable(PageRequest.of(from, size)).withSort(new FieldSortBuilder("id").order(SortOrder.DESC)).build();SearchHits searchHits = elasticsearchRestTemplate.search(nativeSearchQuery, Book.class);if (!searchHits.hasSearchHits()) {break;}for (SearchHit searchHit : searchHits.getSearchHits()) {Book book = searchHit.getContent();}page++;System.out.println(page);System.out.println(total += searchHits.getSearchHits().size());}
}

实现完成,点击运行。

Yes!我失败了。

于是,我意识到我并没有认真看关于报错的信息,在我自我批评和自我反省下,再次对报错信息认真解读
在这里插入图片描述
强大的Elasticsearch已经告诉了我答案,建议我扩大index.max_result_window的上限参数,或者参阅scroll api的文档。

学习

至此,开启了我对Elasticsearch Search Scroll API(滚动查询)的学习和探索!

在浅显的入门Scroll API之后,相比之下,自己之前的想法就像是关公面前耍了一次大刀——小聪明了,总结我自己的想法,那就是当程序运行到极限时,我的代码就好似拼装了以下的一个查询语句,尝试在可视化插件中执行该语句,发现依然会报错,报错与单次查询10000条并无差别。

GET //_search
{"from": 9980,"size": 10010,"query": {"match_all": {}}
}
{"type": "illegal_argument_exception","reason": "Result window is too large, from + size must be less than or equal to: [10000] but was [19990]. See the scroll api for a more efficient way to request large data sets. This limit can be set by changing the [index.max_result_window] index level setting."
}

对于此类问题,Elasticsearch Search为我们提供了一个与springBoot集成的强大依赖ElasticsearchRestTemplate,可以参考官方的 API 文档:
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/java-rest-high-search-scroll.html

尝试使用解决问题:

public void scrollSearch() {NativeSearchQuery query = new NativeSearchQueryBuilder().withSort(new FieldSortBuilder("id").order(SortOrder.DESC))//排序.build();nativeSearchQuery.setMaxResults(1000);long scrollTimeOut = 60 * 1000;// 第一次查询SearchScrollHits searchScrollHits = elasticsearchRestTemplate.searchScrollStart(scrollTimeOut, query, Book.class, IndexCoordinates.of("ss.index.book"));while (searchScrollHits.hasSearchHits()) {System.out.println(total += searchScrollHits.getSearchHits().size());for (SearchHit searchHit : searchScrollHits.getSearchHits()) {Book book = searchHit.getContent();}// 再次查询searchScrollHits = elasticsearchRestTemplate.searchScrollContinue(searchScrollHits.getScrollId(), scrollTimeInMillis, Book.class, IndexCoordinates.of("book"));}
}

最后,感谢大家的聆听和阅读,我拿到了我期望的10000条以后的数据,

我以为我这次终于成功了,但我的麻烦才刚刚开始。

踩坑

该问题上线之后不久,一切都运行顺利,我以为已经高枕无忧的时候,我的麻烦再次来临。

用户侧反应了两个问题:

一、查询得时候发现有返回重复的数据。

二、从ES获取后上传数据发生了数据堆积。

查看日志,发现有大量的报错,报错信息表示存放的 scroll_id 超出了默认的500限制。

在这里插入图片描述

trying to creat too many scroll aontexts。Must be less than or equal to: [500].This limit can be set by changing the [search.max_open_scroll_context] setting.

因此我对问题做出以下分析

1、虽然进行 scroll 查询时会记录一个 scroll_id,但只有新请求时才会生成新的 scroll_id,这就有一定概率导致查询到的返回结果出现重复数据的可能。

2、scroll 开启时间过长,导致 scroll_id 的清除时间太久产生堆积,所以引起了数据查询延迟,引起了数据堆积。

这次由于我认真读取了报错信息,但我不愿以牺牲上限为代价,不然那将是无底洞式的调大上限,于是我在原先代码基础上做出了修改:

    public void scrollSearch() {NativeSearchQuery query = new NativeSearchQueryBuilder().withSort(new FieldSortBuilder("id").order(SortOrder.DESC))//排序.build();nativeSearchQuery.setMaxResults(1000);long scrollTimeOut = 60 * 1000;// 第一次查询SearchScrollHits searchScrollHits = elasticsearchRestTemplate.searchScrollStart(scrollTimeOut, query, Book.class, IndexCoordinates.of("ss.index.book"));//记录新请求SearchScrollHits newHits;while (searchScrollHits.hasSearchHits()) {System.out.println(total += searchScrollHits.getSearchHits().size());for (SearchHit searchHit : searchScrollHits.getSearchHits()) {Book book = searchHit.getContent();}// 再次查询try {newHits = elasticsearchRestTemplate.searchScrollContinue(searchScrollHits.getScrollId(), scrollTimeInMillis, Book.class, IndexCoordinates.of("book"));searchScrollHits = newHits;} finally {elasticsearchRestTemplate.searchScrollClear(Lists.newArrayList(searchScrollHits.getScrollId()));}}}

这样,我每次发出查询请求获得数据之后,都会清除掉上一次生成的scroll_id,如此保证scroll_id不会超出500;

不出意外的,我又出意外了,显然我在对scroll_id理解上出了问题,它不是对每页的标记,而是每一次滚动查询整个生命周期的唯一id,上述代码导致的结果就是我永远只能拿到前两页的内容,之后因为找不到生命周期的唯一id报错。
在这里插入图片描述

最终解决

那么最终导致scroll_id堆积的原因也找到了:searchScrollStart入参从来都不是什么TimeOut时间,而是scroll_id存在的生命周期时间,如果这个时间过大,那么scroll_id就得不到及时清理,最终超过500进而报错,解决方案就会有两种:

1.根据实际每次请求的调用需求时间,调整scroll_id的生命周期,保持在一个合适的时间,从而防止堆积

2.动态清理scroll_id

显然第一种是在难为我胖虎,我选择了第二种,将searchScrollStart视为一次滚动查询请求的开始,searchScrollHits.hasSearchHits()为false时视为结束,此时searchScrollClear在合适不过。

第三次修改:

public void scrollSearch() {NativeSearchQuery query = new NativeSearchQueryBuilder().withSort(new FieldSortBuilder("id").order(SortOrder.DESC)).build();// 设置每页数据量nativeSearchQuery.setMaxResults(1000);long scrollTimeInMillis = 60 * 1000;// 第一次查询SearchScrollHits searchScrollHits = elasticsearchRestTemplate.searchScrollStart(scrollTimeInMillis, nativeSearchQuery, Book.class, IndexCoordinates.of("book"));String scrollId = searchScrollHits.getScrollId();while (searchScrollHits.hasSearchHits()) {System.out.println(total += searchScrollHits.getSearchHits().size());for (SearchHit searchHit : searchScrollHits.getSearchHits()) {Book book = searchHit.getContent();}// 再次查询searchScrollHits = elasticsearchRestTemplate.searchScrollContinue(scrollId, scrollTimeInMillis, Book.class, IndexCoordinates.of("book"));scrollId = searchScrollHits.getScrollId();}List scrollIds = new ArrayList<>();scrollIds.add(scrollId);// 清除 scrollelasticsearchRestTemplate.searchScrollClear(scrollIds);
}

至此,才是真正的谢谢大家的阅读啦~~~good bye!

相关内容

热门资讯

【MySQL】锁 锁 文章目录锁全局锁表级锁表锁元数据锁(MDL)意向锁AUTO-INC锁...
【内网安全】 隧道搭建穿透上线... 文章目录内网穿透-Ngrok-入门-上线1、服务端配置:2、客户端连接服务端ÿ...
GCN的几种模型复现笔记 引言 本篇笔记紧接上文,主要是上一篇看写了快2w字,再去接入代码感觉有点...
数据分页展示逻辑 import java.util.Arrays;import java.util.List;impo...
Redis为什么选择单线程?R... 目录专栏导读一、Redis版本迭代二、Redis4.0之前为什么一直采用单线程?三、R...
【已解决】ERROR: Cou... 正确指令: pip install pyyaml
关于测试,我发现了哪些新大陆 关于测试 平常也只是听说过一些关于测试的术语,但并没有使用过测试工具。偶然看到编程老师...
Lock 接口解读 前置知识点Synchronized synchronized 是 Java 中的关键字,...
Win7 专业版安装中文包、汉... 参考资料:http://www.metsky.com/archives/350.htm...
3 ROS1通讯编程提高(1) 3 ROS1通讯编程提高3.1 使用VS Code编译ROS13.1.1 VS Code的安装和配置...
大模型未来趋势 大模型是人工智能领域的重要发展趋势之一,未来有着广阔的应用前景和发展空间。以下是大模型未来的趋势和展...
python实战应用讲解-【n... 目录 如何在Python中计算残余的平方和 方法1:使用其Base公式 方法2:使用statsmod...
学习u-boot 需要了解的m... 一、常用函数 1. origin 函数 origin 函数的返回值就是变量来源。使用格式如下...
常用python爬虫库介绍与简... 通用 urllib -网络库(stdlib)。 requests -网络库。 grab – 网络库&...
药品批准文号查询|药融云-中国... 药品批文是国家食品药品监督管理局(NMPA)对药品的审评和批准的证明文件...
【2023-03-22】SRS... 【2023-03-22】SRS推流搭配FFmpeg实现目标检测 说明: 外侧测试使用SRS播放器测...
有限元三角形单元的等效节点力 文章目录前言一、重新复习一下有限元三角形单元的理论1、三角形单元的形函数(Nÿ...
初级算法-哈希表 主要记录算法和数据结构学习笔记,新的一年更上一层楼! 初级算法-哈希表...
进程间通信【Linux】 1. 进程间通信 1.1 什么是进程间通信 在 Linux 系统中,进程间通信...
【Docker】P3 Dock... Docker数据卷、宿主机与挂载数据卷的概念及作用挂载宿主机配置数据卷挂载操作示例一个容器挂载多个目...