华为云 CSS 全文检索服务索引重建与分片均衡实战:一次 mapping 冲突引发的写入阻塞排查

举报
yd_260132607 发表于 2026/11/03 10:08:04 2026/11/03
【摘要】 华为云 CSS 全文检索服务索引重建与分片均衡实战:一次 mapping 冲突引发的写入阻塞排查背景:突然变慢的写入上周五下午,运营反馈商品搜索结果延迟从 80ms 涨到 2s 以上,同时 Elasticsearch 集群日志里出现大量 `rejected execution of coordinating operation ... TOO_MANY_REQUESTS`。我们的 CSS 集...

华为云 CSS 全文检索服务索引重建与分片均衡实战:一次 mapping 冲突引发的写入阻塞排查

背景:突然变慢的写入

上周五下午,运营反馈商品搜索结果延迟从 80ms 涨到 2s 以上,同时 Elasticsearch 集群日志里出现大量 `rejected execution of coordinating operation ... TOO_MANY_REQUESTS`。我们的 CSS 集群是 3 个 data 节点、每个 4C16G,磁盘用的是通用型 SSD,索引是三个月前一次全量灌入建立的,mapping 里塞了 40 多个字段,其中十来个是 `nested`。当天上午刚做过一次批量更新脚本,跑了大概 200 万条文档。

监控上看,JVM 老年代占用 78%,GC 平均停顿 1.2s,磁盘水位到了 85%。第一直觉是资源不够了,于是准备扩容。但在扩容之前,还是决定先把「到底是谁在写、写到哪了」搞清楚。

第一步:用 tasks API 定位热点分片

先看写入到底压在哪个分片上。CSS 完全兼容开源 Elasticsearch 的 API,所以直接用 `_cat` 系列接口:

```

GET /_cat/shards?v&h=index,shard,prirep,state,docs,store,node

GET /_cat/tasks?v&detailed&h=task_id,action,runtime,node

```

`_cat/tasks` 里刷出大量 `indices:data/write/bulk[s]` 任务,runtime 超过 5s 的有几百个,而且集中在同一个 index 的某几个 shard 上。`store` 列显示有两个分片的体积是其他分片的 3 倍多。这基本排除了「集群整体资源不足」,指向的是分片分布不均 + 单分片写入热点。

再看索引的 setting:

```

GET /my-index/_settings?flat_settings=true

```

`number_of_shards` 是 6,但 `number_of_replicas` 是 0。也就是说这个索引从一开始就没有副本,节点数从 5 缩到 3 之后,Shard 分配在当前节点上非常集中,节点重启后也没有其他地方可以快速接替。

第二步:mapping 冲突才是导火索

节点日志往前翻,找到了真正的报错:

```

mapper_parsing_exception: [object mapping for [attributes] tried to parse field [price] as object, but found a concrete value]

```

上游的批量更新脚本,把 `attributes.price` 从「对象」改成了「标量」,而老的文档里这个字段还是对象类型。ES 的 mapping 一旦确定就不允许直接改字段类型,这会导致整个写入被拒绝,直到脚本回退。

而那个 200 万条的批量更新,本身也是个坏设计:每次 1000 条的 bulk 里混了新增和更新两种文档,且用了默认的 1 个副本、refresh_interval 也是默认值,等于把所有写入压力都压在一个 index 的主分片上。

第三步:三个动作止血

第一步,把 mapping 冲突隔离掉。 因为是「对象改标量」,直接改 mapping 是不可能的。正确做法是新建一个 `my-index-v2`,用新的 mapping(`price` 直接定义为 `scaled_float`),然后用 `_reindex` + `script` 做类型转换搬运:

```

POST /_reindex?wait_for_completion=false&refresh

{

  "source": { "index": "my-index", "size": 5000 },

  "dest": { "index": "my-index-v2" },

  "script": {

    "source": "ctx._source.attributes.price = (ctx._source.attributes.price instanceof Map) ? ctx._source.attributes.price.value : ctx._source.attributes.price"

  }

}

```

第二步,扩容写入侧。 把 `refresh_interval` 显式调到 `30s`(牺牲一点实时性,换写入吞吐),bulk 的 `max_concurrent_requests` 降到 4,同时把更新脚本改成 `_update_by_query` 走异步,避免和检索争抢 IO。

第三步,把副本补上并让分片散开。 新索引直接按数据量规划成 12 个主分片、1 个副本:

```

PUT /my-index-v2

{ "settings": { "number_of_shards": 12, "number_of_replicas": 1, "refresh_interval": "30s" } }

```

节点从 3 扩到 6 之后,副本终于有了落点,检索流量和写入流量也被分片天然打散。CSS 控制台里也能直接做节点规格变更,但分片数一旦定下来就不能改,所以这一步做在扩容之前,避免了二次重建。

效果验证

迁移完成切别名后,延迟从 2s+ 回落到 60ms 左右,`_cat/tasks` 里的长尾任务消失,429 拒绝降为 0。唯一需要接受的是数据有大约 30 秒的可见性延迟,对商品搜索这个场景完全可以接受。

复盘:三条沉淀下来的规则

第一,**mapping 变更走「新索引 + reindex + 别名切换」,绝不在原索引上改字段类型**。ES 的 mapping 是不可逆的,踩一次的代价就是一次全量重建。

第二,**索引创建时就把 `nested` 字段数量和分片数一起规划**。40 个字段里塞十几个 nested,会让每次查询都要付出嵌套文档命中的代价,也让单个文档的体积膨胀到内存里反序列化开销剧增。

第三,**写入侧和检索侧必须做资源隔离**。批量更新用独立索引 + 异步 `_update_by_query`,检索走副本;节点扩缩容前先确认副本数不是 0,否则「扩容」这个动作其实对写入热点没有任何帮助——这次要是一开始就直接扩容,大概率只是把问题从 3 个节点搬到 6 个节点,成本上去了,故障却还在。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。