📣 极限科技诚招搜索运维工程师(Elasticsearch/Easysearch)- 全职/北京 👉 : 立即申请加入

适用版本: 7.8-8.9

1. 错误异常的基本描述 #

async search error while reducing partial results 是 Elasticsearch 在执行**异步搜索(Async Search)**过程中,对多个分片返回的部分结果进行归并(reduce)阶段失败时报出的异常。

异步搜索允许客户端提交一个查询,Elasticsearch 在后台持续执行,客户端可以通过 id 轮询获取已完成的部分结果。当某个分片的结果损坏、节点在归并过程中离线,或归并时内存不足,就会触发此错误。

典型报错信息 #

ElasticsearchStatusException: Async search: error while reducing partial results
Caused by: java.lang.IllegalArgumentException: ...
    at org.elasticsearch.search.aggregations.AggregatorBase.buildAggregation(AggregatorBase.java:...)
    at org.elasticsearch.search.aggregations.AggregatorBase.collect(AggregatorBase.java:...)

常见现象 #

  • 调用 GET /_async_search/<id> 时返回 500400 状态码,响应体中包含上述异常信息。
  • 异步搜索任务长时间停留在 is_partial: true 状态,无法完成归并。
  • Kibana 或其他客户端通过异步搜索加载大量数据时,页面显示"结果不可用"或持续 spinning。
  • 集群日志中频繁出现 async_search 相关 ERROR 级别日志,且伴随 reducing partial results 字样。

2. 为什么会发生这个错误 #

异步搜索的归并阶段需要在协调节点上将各分片返回的中间结果合并为最终响应。以下是最常见的原因:

2.1 内存压力导致归并失败 #

归并大量聚合结果(尤其是 termshistogrampercentiles 等)会消耗大量堆内存。当协调节点 JVM 堆使用率过高,归并过程中可能触发 CircuitBreakingException,导致部分结果无法完成合并。

Caused by: org.elasticsearch.common.breaker.CircuitBreakingException:
  [parent] Data too large, data for reducing partial results would exceed limit

2.2 参与归并的分片所在节点离线 #

异步搜索是长周期操作,如果某个持有目标分片的节点在查询执行期间下线,协调节点无法获取该分片的完整结果,归并时会抛出异常。

2.3 聚合定义存在不兼容或错误 #

某些聚合类型(如脚本聚合、嵌套聚合)在部分结果上执行时,如果脚本抛出异常或字段映射不一致,会导致归并失败。

{
  "aggs": {
    "invalid_script_agg": {
      "terms": {
        "script": {
          "source": "doc['not_exist_field'].value"  // 字段不存在,归并时报错
        }
      }
    }
  }
}

2.4 异步搜索结果过期被清理 #

异步搜索的结果默认保留一定时间(通过 wait_for_completion_timeoutkeep_alive 控制)。如果客户端轮询间隔过长,部分结果可能已被清理,再次归并时失败。

3. 如何排查此异常 #

3.1 第一步:获取完整的异步搜索响应 #

使用 GET /_async_search/<id> 查看当前任务状态,重点关注 is_partialis_runningresponse 字段:

# 查看异步搜索任务状态
GET /_async_search/FjF1ZXJ5Q...

# 返回示例
{
  "id": "FjF1ZXJ5Q...",
  "is_partial": true,
  "is_running": false,
  "response": {
    "_shards": { "total": 10, "successful": 8, "failed": 2 },
    "error": { "type": "elasticsearch_exception", "reason": "Async search: error while reducing partial results" }
  }
}

3.2 第二步:检查协调节点内存与熔断状态 #

# 查看 JVM 堆使用情况
GET /_nodes/stats/jvm

# 查看熔断状态
GET /_cluster/settings?include_defaults=true
GET /_nodes/stats/breaker

如果 breaker.parent.used 接近 limit,说明归并阶段因内存不足失败。

3.3 第三步:检查分片失败详情 #

# 查看具体失败的分片
GET /_cluster/allocation/explain

# 查看是否有节点离线
GET /_cat/nodes?v
GET /_cat/health?v

3.4 第四步:简化查询复现问题 #

将原始查询中的聚合逐一移除,定位具体是哪个聚合导致归并失败:

{
  "aggs": {
    "test_agg": { "terms": { "field": "user_id" } }
  },
  "size": 0
}

4. 解决方案 #

4.1 缓解内存压力 #

# 调整异步搜索的 keep_alive 时间,避免过期结果堆积
POST /_async_search/submit
{
  "keep_alive": "5m",
  "wait_for_completion_timeout": "1s",
  "query": { "match_all": {} }
}

如果聚合结果集过大,考虑:

  • 降低 terms 聚合的 size 参数(默认 10,可设为更小值)
  • 使用 composite 聚合替代深层嵌套聚合,分批获取结果
  • 增加协调节点堆内存(需重启)

4.2 修复节点离线问题 #

# 确认分片分配状态
GET /_cat/shards?v&h=index,shard,prirep,state,node

# 如果分片未分配,尝试手动触发分配
POST /_cluster/reroute?retry_failed=true

4.3 修正聚合定义 #

检查聚合中引用的字段是否存在于 mapping 中:

# 查看索引 mapping
GET /your_index/_mapping

确保脚本聚合中引用的字段存在且有正确的类型,避免空值或类型不匹配导致的归并异常。

4.4 调整异步搜索参数 #

参数说明建议值
keep_alive结果在集群中保留的时间根据客户端轮询间隔设置,通常 5m~10m
wait_for_completion_timeout等待完成的超时时间长查询建议设短(如 1s),快速返回异步 ID
batched_reduce_size批量归并的分片数默认值通常合理,过大可能加剧内存压力

5. 预防建议与最佳实践 #

  • 合理设置 keep_alive:不要设置过长的保留时间,避免部分结果堆积占用内存。
  • 监控 JVM 堆与熔断指标:在堆使用率超过 70% 时提前告警,避免归并阶段触发熔断。
  • 避免超大聚合:对高基数字段做 terms 聚合时,评估 size 设置是否合理,必要时使用 composite 聚合分页。
  • 异步搜索超时设置:在客户端实现合理的重试与退避策略,避免频繁轮询加重集群负担。
  • 索引生命周期管理:定期清理历史索引,减少单次查询需要扫描的分片数量。

借助 INFINI 产品提升排障效率 #

  • INFINI Console 可实时查看集群 JVM 堆、熔断状态、分片分配与异步搜索任务列表,帮助快速定位是内存问题还是节点问题。
  • INFINI Gateway 可部署在 Elasticsearch 前方,对异步搜索请求做缓存、限流与慢查询记录,识别导致归并失败的异常查询模式。

6. 小结 #

async search error while reducing partial results 本质上是异步搜索在结果归并阶段失败的信号,最常见的原因是协调节点内存不足参与分片的节点离线聚合定义错误。排查时应优先检查 JVM 堆使用率与分片分配状态,再结合具体聚合定义定位根因。通过合理设置 keep_alive、优化聚合复杂度和监控内存使用,可以有效预防此类异常。

相关错误 #

附:源码上下文 #

以下是触发此异常的 Elasticsearch 源码片段,便于结合调用栈定位问题:

checkCancellation();
AsyncSearchResponse asyncSearchResponse;
try {
    asyncSearchResponse = mutableSearchResponse.toAsyncSearchResponse(
        this, expirationTimeMillis, restoreResponseHeaders);
} catch(Exception e) {
    ElasticsearchException exception = new ElasticsearchStatusException(
        "Async search: error while reducing partial results",
        ExceptionsHelper.status(e), e);
    asyncSearchResponse = mutableSearchResponse.toAsyncSearchResponse(
        this, expirationTimeMillis, exception);
}
return asyncSearchResponse;