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

适用版本: 7.9-8.9

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

Aggregation [xxx] must have cardinality 1 but was [N] 是 Elasticsearch 在执行聚合(Aggregation)过程中抛出的运行时异常。该错误的核心含义是:某个聚合器被期望只作用于单个桶(bucket),但实际执行时却遇到了多个桶,即聚合的基数(cardinality)不符合预期。

此异常最常见于 global 聚合嵌套子聚合的场景,也可能出现在自定义聚合插件或脚本聚合中。

常见现象 #

  • 搜索请求返回 HTTP 500 状态码,响应体中包含 aggregation_execution_exception
  • Kibana 可视化面板、Canvas 报表或自定义 Dashboard 出现加载失败。
  • 应用程序日志中出现 AggregationExecutionException,并伴随上述错误信息。
  • 在 Elasticsearch 服务端日志(elasticsearch.log)中可以检索到完整的异常堆栈。

典型报错与异常栈 #

{
  "error": {
    "root_cause": [
      {
        "type": "aggregation_execution_exception",
        "reason": "Aggregation [global_aggs] must have cardinality 1 but was [2]"
      }
    ],
    "type": "search_phase_execution_exception",
    "reason": "all shards failed",
    "failed_shards": [...]
  },
  "status": 500
}

服务端日志中的异常堆栈通常类似以下形式:

org.elasticsearch.search.aggregations.AggregationExecutionException: Aggregation [global_aggs] must have cardinality 1 but was [2]
    at org.elasticsearch.search.aggregations.bucket.global.GlobalAggregator.buildAggregations(GlobalAggregator.java:87)
    at org.elasticsearch.search.aggregations.AggregatorBase.collect(AggregatorBase.java:190)
    ...

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

错误机制解析 #

Elasticsearch 中的 GlobalAggregator 是一个特殊的聚合器,它始终只产生一个桶(即 cardinality = 1),用于忽略查询条件、对所有文档进行聚合统计。

GlobalAggregator 内部嵌套的子聚合器在计算时,如果子聚合器的 CardinalityUpperBound 不是 ONE,或者父子聚合之间的基数约束不匹配,就会触发此异常。

常见原因 #

  1. 聚合 DSL 结构错误:在同一个 global 聚合下,错误地并列放置了会产生多个桶的子聚合,且父聚合器无法正确处理多桶场景。
  2. 自定义聚合插件缺陷:如果使用了第三方或自研的聚合插件,其 cardinality() 方法返回值不符合预期(例如错误返回了 MANY 而非 ONE)。
  3. 版本不兼容的脚本聚合:使用了与当前 Elasticsearch 版本不兼容的 scripted_metric 聚合或 bucket_sort 聚合嵌套方式。
  4. 索引映射变更导致的类型冲突:聚合字段的 fielddatadoc_values 设置发生变更,导致聚合执行路径异常。
  5. 跨集群搜索(CCR)或别名场景下的问题:当查询目标跨越多个索引,且各索引的 mapping 不一致时,可能触发聚合执行路径的异常分支。

3. 如何排查这个异常 #

建议按以下步骤系统排查:

第一步:获取完整报错上下文 #

从 Elasticsearch 服务端日志中提取完整的异常堆栈,确认:

  • 报错发生在哪个聚合名称(Aggregation [xxx]
  • 期望值(must have cardinality 1)与实际值(but was [N]
# 在 Elasticsearch 日志中搜索相关异常
grep -A 20 "must have cardinality 1" elasticsearch.log

第二步:检查聚合 DSL #

将触发异常的搜索请求 DSL 提取出来,重点检查 global 聚合及其子聚合的定义:

{
  "size": 0,
  "aggs": {
    "global_aggs": {
      "global": {},
      "aggs": {
        "sub_aggs": {
          "terms": { "field": "category.keyword" }
        }
      }
    }
  }
}

第三步:简化复现 #

通过逐步删除子聚合的方式,定位具体是哪个子聚合导致了基数不匹配:

  1. 先只保留 global 聚合本身,确认基础查询是否正常。
  2. 逐个添加子聚合,直到错误复现,从而锁定问题聚合。
  3. 检查该子聚合的 field 是否存在、doc_values 是否启用。

排查时需要注意的问题 #

  • 不要混淆 cardinality 聚合与 CardinalityUpperBound:此错误中的 “cardinality” 指的是聚合器产生的桶数量上限,与 cardinality 聚合(去重计数)是两个完全不同的概念。
  • 注意 global 聚合的特殊性global 聚合会忽略 query 条件,如果你需要在 global 聚合中过滤文档,应使用 filter 聚合嵌套在 global 内部,而不是直接添加 query
  • 多索引场景需逐一验证 mapping:使用 _all 或索引别名查询时,务必确认所有目标索引的 mapping 中聚合字段的定义一致。

4. 如何解决这个错误 #

方案一:修正聚合 DSL 结构 #

如果错误源于 global 聚合嵌套了不合法的子聚合,请调整 DSL 结构。以下是一个正确的写法示例:

{
  "size": 0,
  "query": {
    "term": { "status": "active" }
  },
  "aggs": {
    "all_docs": {
      "global": {},
      "aggs": {
        "avg_price": {
          "avg": { "field": "price" }
        }
      }
    },
    "filtered_docs": {
      "filter": { "term": { "status": "active" } },
      "aggs": {
        "avg_price": {
          "avg": { "field": "price" }
        }
      }
    }
  }
}

方案二:检查并修正字段映射 #

如果聚合字段的 mapping 存在问题,可以通过以下方式修复:

# 查看字段的 mapping
GET /your_index/_mapping/field/your_field

# 如果 doc_values 被禁用,需要重建索引并启用
PUT /new_index
{
  "mappings": {
    "properties": {
      "your_field": {
        "type": "keyword",
        "doc_values": true
      }
    }
  }
}

方案三:升级或修复自定义聚合插件 #

如果使用了自定义聚合插件,请检查其 cardinality() 方法实现:

// 正确的实现示例
@Override
public CardinalityUpperBound cardinality() {
    return CardinalityUpperBound.ONE; // global 聚合必须返回 ONE
}

后续注意事项与推荐建议 #

  • 在开发环境验证聚合 DSL:复杂聚合上线前,务必在开发或测试环境中用真实数据验证,确认不会出现基数不匹配的问题。
  • 统一多索引的 mapping 定义:使用索引模板(Index Template)确保所有相关索引的字段类型、分析器和 doc_values 设置一致。
  • 为聚合查询添加监控:通过 INFINI Gateway 对聚合类请求进行观测,及时发现执行耗时过长或频繁报错的聚合查询。

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

  • INFINI Console 适合查看集群健康度、索引 mapping、聚合执行趋势和错误日志,帮助快速判断异常是 DSL 问题还是集群问题。
  • INFINI Gateway 适合部署在 Elasticsearch 前面做请求观测、慢查询记录和不合理 DSL 拦截,可以有效识别导致基数异常的恶意或错误聚合请求。
  • 建议将聚合查询的执行耗时、失败次数和触发聚合名称统一接入监控面板,在异常扩散前及时发现并修复。

5. 小结 #

Aggregation [xxx] must have cardinality 1 but was [N] 是 Elasticsearch 聚合执行阶段的约束校验异常,通常指向 global 聚合或其子聚合的基数不匹配问题。排查时应从聚合 DSL 结构、字段 mapping 和自定义插件三个维度入手,通过简化复现的方式快速定位根因。

只要规范聚合 DSL 的编写方式、统一索引 mapping 定义,并结合 INFINI Console 和 INFINI Gateway 做好请求观测,这类异常完全可以在开发阶段被发现和修复。

相关错误 #

附:日志上下文 #

下面保留当前页面中的源码或日志片段,便于继续结合异常调用栈定位问题:

if (cardinality != CardinalityUpperBound.ONE) {
    throw new AggregationExecutionException("Aggregation [" + name() + "] must have cardinality 1 but was [" + cardinality + "]");
}
return new GlobalAggregator(name, factories, context, metadata);