待到秋来九月八,我花开后百花杀
- 背景:
- 大胆尝试实践:
- 学习
- 踩坑
- 最终解决
背景:
那是年初在某个交付项目,从用户侧获知了一个elastic search作为分布式数据库的一个瓶颈,那就是单次查询量超过了ES的默认单次查询上限10000。
在大部分业务下,为了执行ES的数据查询,开发者往往都直接使用了query某个条件获取数据,这些条件对应的数据大多都不会超过10000,因此在一般测试下难以发现这类问题。但系统经过经年累月的使用,数据量在不断增长,又因业务需求不可清除旧数据的情况下,这类问题就诞生了。
于是,我想到Elasticsearch 中,传统的分页查询使用from+size
的模式,类似如下语句:
GET /<index>/_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<Book> searchHits = elasticsearchRestTemplate.search(nativeSearchQuery, Book.class);
if (!searchHits.hasSearchHits()) {
break;
}
for (SearchHit<Book> 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 /<index>/_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<Book> searchScrollHits = elasticsearchRestTemplate.searchScrollStart(scrollTimeOut, query, Book.class, IndexCoordinates.of("ss.index.book"));
while (searchScrollHits.hasSearchHits()) {
System.out.println(total += searchScrollHits.getSearchHits().size());
for (SearchHit<Book> 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<Book> searchScrollHits = elasticsearchRestTemplate.searchScrollStart(scrollTimeOut, query, Book.class, IndexCoordinates.of("ss.index.book"));
//记录新请求
SearchScrollHits<Book> newHits;
while (searchScrollHits.hasSearchHits()) {
System.out.println(total += searchScrollHits.getSearchHits().size());
for (SearchHit<Book> 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<Book> 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<Book> searchHit : searchScrollHits.getSearchHits()) {
Book book = searchHit.getContent();
}
// 再次查询
searchScrollHits = elasticsearchRestTemplate.searchScrollContinue(scrollId, scrollTimeInMillis, Book.class, IndexCoordinates.of("book"));
scrollId = searchScrollHits.getScrollId();
}
List<String> scrollIds = new ArrayList<>();
scrollIds.add(scrollId);
// 清除 scroll
elasticsearchRestTemplate.searchScrollClear(scrollIds);
}
至此,才是真正的谢谢大家的阅读啦~~~good bye!