0
0
1
0
博客/.../

热点 Region 逼停了写入:一次 TiDB 写入性能雪崩的 6 小时排查

 cationlin  发表于  2026-09-23
原创互联网

背景:城市大脑的实时写入业务

去年我们承接了一个区县级城市大脑项目,核心模块要实时归集 IoT 感知设备的数据——路灯、井盖、传感器、视频结构化结果,写入量大概每天 2000 万行左右。数据库选型时我们选了 TiDB 6.5,集群规模是 3 个 TiDB + 3 个 TiKV + 3 个 PD,部署在政务云上。

上线前我们做了压测,单表批量写入能稳在 8k QPS,平均延迟 5ms 左右,团队都觉得这块没问题。表结构大概长这样:

CREATE TABLE device_metric (
  id        BIGINT NOT NULL AUTO_INCREMENT,
  device_id VARCHAR(32) NOT NULL,
  metric_code VARCHAR(16),
  ts        DATETIME NOT NULL,
  metric_json JSON,
  PRIMARY KEY (id),
  KEY idx_device_ts (device_id, ts)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

我们用 AUTO_INCREMENT 主键,写入时由 TiDB 分配自增 ID,应用端走 INSERT INTO ... VALUES (...) 的 batch 写入。前两周一切正常。

现象:写入延迟突然雪崩

第三周周一早上,运维同学被电话叫醒:应用端 batch insert 大面积超时,错误日志里全是 ERROR 9007 (Hy000): Write conflict 和连接池打满。我们登上 Grafana 一看,几个数字很刺眼:

  • 写入平均延迟从 5ms 涨到 800ms+
  • 集群写入 QPS 从 8k 掉到 3k
  • 某一个 TiKV 节点的 CPU 长期 90%+,另外两个 TiKV 却在 30% 闲着
  • TiDB 监控里频繁刷出 Region is hot 的告警

第一反应是"是不是网络抖动了",但 ping 和 iperf 都正常。接着怀疑是不是有慢查询把 TiKV 压住了,可那段时间根本没有大查询。这就有点懵了——明明是写入瓶颈,为什么只有一个节点扛雷,另外两个在旁边看戏?

排查:用 pd-ctl 抓写入热点

我们打开 PD 的管控工具 pd-ctl,先看全局热点分布:

pd-ctl region top write
pd-ctl region top read

top read 基本平稳,但 top write 的输出让我们吃了一惊:某一个 Region 的写流量占了全集群的 70% 以上,它的 leader 正好落在那台 CPU 90% 的 TiKV-1 上。其余 Region 的写流量加起来都不如它零头。

顺着这个 Region 往下挖,用 pd-ctl region <region_id> 看到它承载的全部是 device_metric 这张表的新写入行。问题开始聚焦了:为什么所有新行都往同一个 Region 里钻?

根因:自增主键导致的写入热点

这里踩的是 TiDB 一个经典坑。TiDB 底层按 Region(默认 96MB 一个)做数据分片,行数据按 RowID 有序排列。当表用 AUTO_INCREMENT 主键时,TiDB 为了兼容 MySQL 语义,会把行按顺序写到递增的 _tidb_rowid 上——也就是说,新写入的行 RowID 永远单调递增,永远落在当前最后一个 Region 的尾部。

结果就是:所有写入请求都锤在同一个 Region、同一台 TiKV 上,这台机器成了写入热点(hot Region),Compaction 和 Raft 同步全堆在它身上,延迟自然飙高;而 Region 没打散前,其他 TiKV 确实在闲着。

官方文档其实写得很清楚:高并发写入场景要避免单调递增主键,推荐用 SHARD_ROW_ID_BITS 打散,或者在 7.x 之后直接用 AUTO_RANDOM。

教训:自增 ID 在单机 MySQL 里是常识,但在 TiDB 分布式存储里,单调递增恰恰是热点温床。选型和建表时这步必须前置想清楚。

解决:两种打散方案

我们当时集群是 6.5,有两个可选方案:

A. SHARD_ROW_ID_BITS 建表加 SHARD_ROW_ID_BITS=4 PRE_SPLIT_REGIONS=4 全版本 需重建表
B. AUTO_RANDOM 主键改 BIGINT AUTO_RANDOM 7.x+ 需重建表

方案 B 更优雅,但我们集群还没升级,最终选了方案 A。把表重建成分片写入:

CREATE TABLE device_metric (
  id        BIGINT NOT NULL,
  device_id VARCHAR(32) NOT NULL,
  metric_code VARCHAR(16),
  ts        DATETIME NOT NULL,
  metric_json JSON,
  PRIMARY KEY (id) /* 业务侧自行生成随机/雪花 ID */
) SHARD_ROW_ID_BITS = 4
  PRE_SPLIT_REGIONS = 4
  ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

SHARD_ROW_ID_BITS = 4 会在 RowID 高位打散成 16 个分片,PRE_SPLIT_REGIONS = 4 让建表时直接预切出 16 个 Region,写入一开始就均匀落到多台 TiKV 上。旧数据通过 Dumpling 导出、TiDB Lightning 导入新表,切换在业务低峰完成,全程约 40 分钟。

提示:如果已经是 7.x,直接用 id BIGINT AUTO_RANDOM PRIMARY KEY 替代自增,TiDB 会自动生成打散的随机行 ID,省去业务侧改 ID 生成逻辑。

验证:延迟回到基线

切表完成后,我们盯着 Grafana 看了半小时:

  • 写入平均延迟从 800ms 回到 5~8ms
  • pd-ctl region top write 里各 Region 写流量趋于均衡,热点告警清零
  • 集群写入 QPS 恢复到 8k+,单 TiKV CPU 回落到 40% 左右

为了确认不是偶发,我们连续观察了三天,写入曲线平稳,再没出现雪崩。这次算是彻底根治了。

总结:TiDB 高写入表的几条设计要点

回头看,这个问题完全可以在设计阶段规避。给后来人几条硬建议:

  1. 高并发写入表,禁止用单调递增的 AUTO_INCREMENT 主键。RowID 会被集中到同一 Region。
  2. 建表即打散:SHARD_ROW_ID_BITS = 4(一般 4~5 足够,太大反而增加 Region 管理开销);7.x 优先考虑 AUTO_RANDOM。
  3. 上线后用 pd-ctl region top write 持续观察,热点苗头一出现就干预,别等雪崩。
  4. 分区表 + 打散策略可以组合,按时间分区的流水表配合打散,冷热分离和扩展性都更好。
  5. 压测要带真实写入分布,我们上线前压测用的是均匀 ID,没暴露自增热点,这才埋了雷。

TiDB 的弹性扩缩容很好用,但"分布式"不等于"写入自动均衡"——热点 Region 这种问题,还是得靠表设计把根因解决掉,加机器是治标不治本。


*版权声明 © 本文为原创首发于 TiDB 社区,转载请注明出处。*

0
0
1
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论