2142 字
约 7 分钟
12
S13-A28 · 聚合的内存从哪来:为什么"只加一个统计"就把节点打爆

报表页只加了一个"按城市统计订单额"的柱状图。上线当天,那个数据节点的堆内存从 45 % 涨到 91 %,Full GC 从一天两次变成一小时三次。查字段类型:city 是 text——当初为了支持"城市名模糊搜索"建的,没人注意到它同时也在被聚合。

这是 ES 里最经典的一类事故:检索与聚合用的是两套完全不同的数据结构,而 text 类型只对前者友好,对后者是一场灾难。它不会报错,只会慢慢把堆吃掉。


1. 一套字段,两种加载方式

用途 数据结构 存在哪 默认
检索(全文匹配) 倒排索引(term → doc list) 磁盘段文件 开
排序 / 聚合 正排(doc → values) doc_values:磁盘列存 开(keyword/数值)
排序 / 聚合(text) fielddata(term → doc list 的内存版) 堆内存 关,按需加载

第三行是全篇的关键:text 字段做聚合时,ES 必须把倒排索引整个读进堆,这叫 fielddata。它一旦加载就不会自己退回去,除非触发 circuit breaker 或手动 clear cache。

// 同一个聚合,两种字段类型的成本差三个量级
聚合 city(keyword):  doc_values 顺序扫描 → 堆占用 ≈ 0,走 page cache
聚合 city(text):     fielddata 全量加载 → 堆占用 = 唯一 term 数 × ~数十字节
// 一个 100 万行的索引,城市名若被分词成 3 段,term 基数会翻几倍

2. 为什么 text 是灾难:term 基数是乘出来的

text 会把"北京市朝阳区"切成多个 term。聚合按 term 分桶,不是按原值分桶——于是:

字段类型 分桶键 桶数(10 万条数据)
keyword 完整值 300 个城市
text(标准分词) 分词后的 term 上千个("北京"+"市"+"朝阳"+"区"…)
text(ngram 分词) 字符片段 上万到十万级
// 同一个 city 字段,两种写法的桶数差一个量级
聚合 "city":          北京 / 北京市 / 朝阳 / 区 → 上千桶
聚合 "city.keyword":  北京市朝阳区            → 300 桶
// 差的不是精度,是堆内存:桶数 × 单桶开销

第三行最致命:ngram 分词器会把每个词拆成 2–3 字的片段,聚合时的桶数直接爆到与文档数同阶。而这一切只表现为"堆涨了",没有任何错误提示。


3. 三个修法,代价不同

修法 做法 代价 适用
双字段 city(text,检索)+ city.keyword(keyword,聚合) 索引体积 +30–60 % 标准做法
fielddata 限额 fielddata_frequency_filter 只保留高频 term 丢失长尾桶 应急
预聚合 写入时算好聚合结果,查一个小索引 实时性下降 报表类

第一行的机制来自 fields 多字段(multi-fields):同一个原始值建两份索引,检索走 text、聚合走 keyword。它是最彻底也最常见的修法,代价是存储——所以字段设计时该先问"这个字段会不会被聚合"。

// 一个可用的映射(检索 + 聚合都对)
"city": {
  "type": "text",
  "analyzer": "ik_max_word",
  "fields": {
    "keyword": { "type": "keyword", "ignore_above": 256 }   // 聚合与排序走这个
  }
}
// 注意:聚合必须写 city.keyword,写 city 就会触发 fielddata

4. fielddata 的断路器:它只是止损

ES 有一个 indices.breaker.fielddata.limit(默认堆的 40 %)。超了会拒绝查询并抛 CircuitBreakingException——这是保护,不是修复。

现象 含义
CircuitBreakingException: fielddata 加载被拒,查询失败
堆使用率高但没报错 已加载的 fielddata 还没到限额
手动 clear cache 后恢复 只是把内存还回去,下次查询还会加载
// 应急三件套(按顺序:先止损,再定位)
$ curl -XPOST 'localhost:9200/myidx/_cache/clear?fielddata=true'      // 1 把内存还回去
$ curl -XPUT 'localhost:9200/_cluster/settings' -H 'Content-Type: application/json' \
    -d '{"persistent":{"indices.breaker.fielddata.limit":"25%"}}'      // 2 降限额
$ grep -rn "aggs" --include=*.json src/ | grep -v ".keyword"          // 3 找出还在用 text 的查询
// 第三条才是修复:改查询或改映射,前两条只是把事故推后

第三行说明:清缓存是止血,不是治病。而如果负载高峰恰好两次加载撞在一起,就会出现"查询时好时坏"的诡异现象。

// 三步定位:谁在加载 fielddata
$ curl -s 'localhost:9200/_nodes/stats/indices/fielddata?human&pretty' | grep -A3 fielddata
$ curl -s 'localhost:9200/myidx/_stats/fielddata?human&pretty' | grep memory_size
$ curl -s 'localhost:9200/myidx/_mapping' | python3 -c 'import sys,json;m=json.load(sys.stdin);[print(k,v) for k,v in list(m.values())[0]["mappings"]["properties"].items() if v.get("type")=="text"]'
// 第三条列出所有 text 字段——它们都是潜在爆点

5. 什么时候不该用 ES 做这个聚合

场景 为什么不该 替代
高基数维度(用户 ID、订单号) 桶数 = 基数,必然爆 预聚合 / ClickHouse
需要精确去重 cardinality 是近似 精确计数走库
多维交叉报表 桶数乘法膨胀 数据仓库
秒级刷新的看板 每次查询都要全量扫 定时预聚合

第一行的判据很硬:桶数 ≈ 维度基数。城市(几百)可以,用户(百万)不行——而"不行"的表现同样是"堆涨了",不是报错。


6. 排错:五种"聚合把节点打爆"

现象 八成原因 处理方向
加一个聚合堆就飙 text 字段触发了 fielddata 改用 .keyword
偶尔 CircuitBreakingException 多次查询叠加加载 改双字段 + 降限额
聚合结果比预期多很多 桶按 term 而非原值 同上
清缓存后很快又涨回来 查询逻辑仍在用 text 改代码,不是清缓存
单条聚合慢但不报错 doc_values 磁盘 IO 加 page cache 或预聚合

第四行是最常见的"治标循环":运维清缓存、业务继续用 text 聚合,于是每周处理一次同样的告警。根因在查询语句里,而不在集群。


7. 边界:什么情况"用 text 聚合"是可以的

场景 可以吗 理由
数据量很小(< 1 万条) 可以 term 基数天然低
一次性分析,事后删索引 可以 短命索引不值得优化
就是要按"词"统计(词频) 必须 语义上就该按 term 分桶
高频长期查询 不行 成本会反复出现

第三行容易被误判成"错误用法":做词频分析(比如统计评论里出现最多的词)本来就要用 text,此时 fielddata 是正确选择,只要控制数据量与限额。

判断口径一句话:聚合内存的风险 = 桶数 × 单桶开销,而桶数由"字段是不是被分词"决定——所以判据不是"数据量多大",而是"这个维度会被切成几个 term"。要按原值分组就用 keyword,要按词分组才用 text,两者不能靠调参互换。


8. 自测

  1. 检索与聚合分别用哪种数据结构?它们存在磁盘还是堆里?
  2. 为什么 text 字段做聚合会触发 fielddata?它加载后会自己释放吗?
  3. keyword 与 text 聚合的桶数差异从哪来?ngram 为什么会把桶数炸到与文档数同阶?
  4. 双字段方案解决了什么、代价是什么?
  5. 断路器 CircuitBreakingException 是修复还是止损?为什么清缓存解决不了问题?
  6. 什么判据能让你在加聚合前判断"会不会爆"?

9. 一句话总结

聚合的内存风险来自"桶数 × 单桶开销",而桶数由字段是否被分词决定:keyword 走磁盘上的 doc_values(几乎不占堆),text 则要把它整个倒排读进堆(fielddata),且加载后不会自己退回。修法的优先级是双字段(text 检索 + .keyword 聚合)→ 预聚合 → 换存储;fielddata_frequency_filter 与断路器只是止损。判据:按原值分组用 keyword,按词分组才用 text,两者不能靠调参互换。

S13-A28 · 聚合的内存从哪来:为什么"只加一个统计"就把节点打爆
https://www.clxhxhhr.top/posts/6746/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。