Trying to create too many scroll contexts. Must be less than or equal to: [500]. This limit can be set by changing the [search.max_open_scroll_context] setti。
此报错为在查询es时,scrool超过es默认限制,通常可以通过直接执行下方👇设置临时合永久的数量。
put /_cluster/settings
{
"persistent": {
"search.max_open_scroll_context": 5000
},
"transient": {
"search.max_open_scroll_context": 5000
}
}
一scrool作用
scroll 在 Elasticsearch 中用于处理大规模数据的分页查询。它能够在不重新计算每个分页的情况下,连续读取大量数据。传统的分页查询(使用 from 和 size 参数)在处理大数据集时性能较差,因为每次请求都需要重新排序和计算分页。scroll 通过在第一次查询时创建一个快照,并为后续请求维持上下文,从而提高了查询效率。
scroll 的主要作用:
- 处理大规模数据:适用于需要处理和检索大量数据的场景,例如数据迁移、全量索引重建、分析处理等。
- 保持快照:在第一次查询时创建一个数据快照,确保在整个滚动查询过程中数据的一致性,不会因为数据的更新而影响查询结果。
- 性能优化:避免了传统分页查询中的排序和分页计算开销,提高了性能。
二滚动停止
比如一共由1000条数据,size设置为1,这个查询语句中符合条件的有20条,它一共会查询1000次吗?
如果你的查询总共只有20条数据符合条件,而你设置的每次滚动查询的size为1,那么实际上不会进行1000次滚动查询。滚动查询会在没有更多符合条件的数据时终止。具体来说:
- 初始查询:第一次执行初始查询,返回第一条符合条件的数据,并生成一个scrollId。
- 滚动查询循环:
- 每次滚动查询会返回下一条符合条件的数据。
- 一旦返回的数据不再符合条件(即数据集为空),滚动查询就会停止。
具体流程:
- 初始查询:查询到第一条符合条件的数据,并返回scrollId。
- 滚动查询:使用scrollId进行滚动查询,获取下一条符合条件的数据,更新scrollId。
- 终止条件:如果某次滚动查询返回的数据为空(即没有更多符合条件的数据),循环终止。
三清理
在使用 Elasticsearch 的滚动查询 (scroll search) 时,清理 scroll 的时候只需要提供最后一次的 scrollId 就可以。滚动查询是通过一系列的 scrollId 来维护会话状态的,但在清理的时候,只需要使用最新的 scrollId,这样可以释放所有相关资源。
以下是清理 scroll 的步骤:
- 开始滚动查询: 发送一个初始查询请求,并设置 scroll 参数(例如1分钟)。Elasticsearch 会返回一个 scrollId 和查询结果的第一批数据。
- 滚动查询: 使用返回的 scrollId 进行后续请求,每次请求都会返回新的 scrollId 和下一批数据。
- 清理 scroll: 当不再需要进行滚动查询时,可以使用最后一次返回的 scrollId 调用 _search/scroll API 并设置 scroll_id 来清理滚动会话。
只提供最后一次的 scrollId,Elasticsearch 会清理整个滚动会话的所有资源。这样做不仅简化了清理过程,也确保资源被正确释放
如果在滚动查询的循环处理中加了会有什么后果
在滚动查询的循环处理中,如果每次都重新设置 this.scroll = new Scroll(TimeValue.timeValueMinutes(1L));,会导致一些潜在问题,主要包括:
-
资源泄漏: 每次循环都创建一个新的 Scroll 对象可能导致多余的 scroll context 在服务器端被创建,而这些 context 可能不会被及时清理。这样会占用不必要的系统资源,可能导致 Elasticsearch 的性能下降,甚至耗尽资源。
-
无效的 Scroll Id: 每次重新创建一个新的 Scroll 对象并不会自动关联到之前的滚动查询上下文。这意味着你实际上没有正确地维护滚动会话,可能导致滚动查询失败或无法获取预期的数据。
-
潜在的性能问题: 多次创建不必要的 Scroll 对象会导致额外的开销,影响系统的整体性能。
四代码
滚动查询流程
- 设置滚动时间窗口:在初始查询时设置 scroll 对象,定义滚动上下文的存活时间为 5 分钟。
this.scroll = new Scroll(TimeValue.timeValueMinutes(5L));
- 初始查询:执行初始查询并获取第一个批次的数据。
SearchResponse response = this.execute();
- 处理初始查询结果:调用 scrollFunction.getResult(response) 方法处理查询结果。
if (Objects.nonNull(response)) {
scrollFunction.getResult(response);
this.scrollId = response.getScrollId();
- 滚动查询循环:在循环中使用 scrollId 执行滚动查询,获取下一批次的数据并处理。
while (searchResponse.getHits() != null && searchResponse.getHits().getHits().length > 0 && StringUtils.isNoneBlank(this.scrollId)) {
SearchScrollRequest scrollRequest = new SearchScrollRequest(this.scrollId);
//也会将时间重置为最初设置的时间
scrollRequest.scroll(this.scroll); // 设置滚动上下文
searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT); // 执行滚动查询
this.scrollId = searchResponse.getScrollId(); // 更新 scrollId
if (searchResponse.getHits() != null && searchResponse.getHits().getHits().length > 0) {
scrollFunction.getResult(searchResponse); // 处理查询结果
}
}
- 清理查询上下文:在所有数据处理完成后,清理滚动上下文以释放资源。
private void clearScrollContext() {
if (this.scrollId != null && !"".equals(this.scrollId)) {
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(this.scrollId);
try {
if (this.client != null) {
this.client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
} else {
EsClient.getInstance().getClient().clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
}
} catch (IOException e) {
logger.error("清除ES-ScrollId失败", e);
}
}
}
以下是完整的代码
import org.apache.http.HttpHost;
import org.elasticsearch.action.search.ClearScrollRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.search.Scroll;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Objects;
public class ElasticsearchScroll {
private static final Logger logger = LoggerFactory.getLogger(ElasticsearchScroll.class);
private RestHighLevelClient client;
private Scroll scroll;
private String scrollId;
public ElasticsearchScroll(RestHighLevelClient client) {
this.client = client;
}
public void executeScrollQuery(String indexName, SearchSourceBuilder searchSourceBuilder, ScrollFunction<SearchResponse> scrollFunction) {
//首次执行
this.scroll = new Scroll(TimeValue.timeValueMinutes(1L)); // 设置滚动时间窗口
SearchRequest searchRequest = new SearchRequest(indexName);
searchRequest.scroll(scroll); // 设置滚动上下文
searchRequest.source(searchSourceBuilder); // 设置查询条件
try {
// 执行初始查询
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
processScrollResponse(searchResponse, scrollFunction);
} catch (IOException e) {
logger.error("Error executing initial search request", e);
} finally {
// 确保在最后清除滚动上下文
clearScrollContext();
}
}
private void processScrollResponse(SearchResponse searchResponse, ScrollFunction<SearchResponse> scrollFunction) throws IOException {
if (Objects.nonNull(searchResponse)) {
scrollFunction.getResult(searchResponse); // 处理查询结果
this.scrollId = searchResponse.getScrollId(); // 获取 scrollId
// 当查询结果不为空时,继续滚动查询
while (searchResponse.getHits() != null && searchResponse.getHits().getHits().length > 0 && StringUtils.isNoneBlank(this.scrollId)) {
SearchScrollRequest scrollRequest = new SearchScrollRequest(this.scrollId);
//也会将时间重置为最初设置的时间
scrollRequest.scroll(this.scroll); // 设置滚动上下文
searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT); // 执行滚动查询
this.scrollId = searchResponse.getScrollId(); // 更新 scrollId
if (searchResponse.getHits() != null && searchResponse.getHits().getHits().length > 0) {
scrollFunction.getResult(searchResponse); // 处理查询结果
}
}
}
}
private void clearScrollContext() {
if (this.scrollId != null && !"".equals(this.scrollId)) {
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(this.scrollId);
try {
if (this.client != null) {
this.client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
} else {
EsClient.getInstance().getClient().clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
}
} catch (IOException e) {
logger.error("清除ES-ScrollId失败", e);
}
}
}
@FunctionalInterface
public interface ScrollFunction<T> {
void getResult(T response);
}
public static void main(String[] args) {
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(new HttpHost("localhost", 9200, "http")));
try {
ElasticsearchScroll scrollExample = new ElasticsearchScroll(client);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(/* your query here */);
searchSourceBuilder.size(2000); // 设置每次查询的大小
scrollExample.executeScrollQuery("your_index", searchSourceBuilder, searchResponse -> {
// 处理查询结果
System.out.println("Processing batch...");
});
} finally {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
如果先进行创建一个对象EsOperater build = esOperateBuilder.indexes(indexName).build();
然后在执行滚动查询executeScrollQuery,那么可能导致线程不一致,可以这么改以下:
while (searchResponse.getHits() != null && searchResponse.getHits().getHits().length > 0 && StringUtils.isNoneBlank(this.scrollId)) {
SearchScrollRequest scrollRequest = new SearchScrollRequest(this.scrollId);
//也会将时间重置为最初设置的时间
scrollRequest.scroll(this.scroll); // 设置滚动上下文
searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT); // 执行滚动查询
//先进行清理为null
this.scroll = null;
this.scrollId = null;
//重新设置
// 设定滚动时间间隔
this.scroll = new Scroll(TimeValue.timeValueMinutes(10L));
this.scrollId = searchResponse.getScrollId(); // 更新 scrollId
if (searchResponse.getHits() != null && searchResponse.getHits().getHits().length > 0) {
scrollFunction.getResult(searchResponse); // 处理查询结果
}
}
但是这样会导致一个问题
在循环中每次都重新创建一个新的 Scroll 对象可能会导致产生很多的 scroll 上下文,这可能会超过 Elasticsearch 的默认限制,并且清理时只清理最后一个 scrollId 会导致无法清理所有的 scroll 上下文。