批量 UPSERT 死锁率与「batch size × 并发 writer 数」呈非线性交互:消费者从 8 实例扩到 24 后死锁率从 0.3% 跳到 4.7%,缩 batch 救不回来——安全操作域怎么定量划定?
该问题的正文与回答。
背景
我负责一条实时指标落库管道:Kafka 3.5(KRaft,3 broker,topic metrics-write,96 分区),消费组 metrics-writer,下游 MySQL 8.0.36(16C64G 单主,InnoDB,innodb_flush_log_at_trx_commit=1,sync_binlog=1,隔离级别 RR)。写入语句是多行 INSERT INTO t_metrics (metric_key, ts, val) VALUES (...) ON DUPLICATE KEY UPDATE val=VALUES(val),单批 200 行、单事务 commit,rewriteBatchedStatements=true。
正常负载下(消费组 8 实例,每实例 12 分区,峰值 ~8000 msg/s),死锁率约 0.3%/天,可接受。
现象
上周业务方加了三个新指标源,Kafka 分区从 64 扩到 96,消费组 HPA 自动从 8 实例扩到 24 实例(每实例 4 分区),消息量涨到 ~15000 msg/s。
结果:
- 死锁率从 0.3% 飙到 4.7%(
SHOW ENGINE INNODB STATUS里LATEST DETECTED DEADLOCK频繁出现,ROLLBACK行数从 ~200/天 到 ~3000/天) - 死锁模式高度一致:两个事务都在同一
metric_key的uk_metric_key_ts唯一索引上,一个走 INSERT 路径拿 insert intention lock,另一个走 UPDATE 路径拿 record lock,AB-BA 环路 - 吞吐没崩(QPS 仍 ~15000),但 P99 写延迟从 12ms 涨到 85ms(重试放大)
- 重试后数据正确(幂等键
metric_key + ts唯一),但重试风暴把 binlog 量推高 40%,从库延迟从 200ms 涨到 1.2s
已尝试
- 缩 batch size:200 → 100 → 50。死锁率降了(4.7% → 2.1% → 1.4%),但吞吐同比例下降(每行 commit 更频繁),从库延迟没改善,P99 反而更差(更多小事务 = 更多 redo fsync)。缩 batch 是治标,不是治本。
- 加指数退避重试(初始 50ms,最大 2s,最多 3 次)。重试成功率 ~92%,但重试期间锁已释放、其他事务可能已写入,导致
ON DUPLICATE KEY UPDATE覆盖顺序不确定——数据最终正确但写入顺序不可审计。 - RC 隔离级别。死锁率确实降到 0.8%(gap lock 减少),但我们这张表有「按
ts范围查最近 N 条」的读路径,RC 下 phantom read 在业务侧被接受,我还没敢全量切。 - 按
metric_key哈希分片,同一 key 固定路由到同一消费者实例。理论上消除跨实例竞争,但 96 分区 × 24 实例下,热点 key(top 200 指标占 60% 流量)仍然集中在少数实例上,死锁率只降到 2.8%。
卡点
核心问题是:死锁率 = f(batch_size, concurrent_writers, key_skew),这个函数不是单调的,也不是线性的。我现在的困境是:
- 8 实例 × batch 200 → 死锁 0.3%(安全)
- 24 实例 × batch 200 → 死锁 4.7%(不可接受)
- 24 实例 × batch 50 → 死锁 1.4%(勉强,但吞吐和 P99 都差)
- 12 实例 × batch 100 → 我还没测,猜在 1%~2% 之间
我需要一个定量模型或经验公式,能告诉我:在给定 concurrent_writers 和 key_skew 的条件下,batch_size 的安全上限是多少?或者反过来说:给定可接受死锁率(比如 <0.5%),我能容忍的最大并发 writer 数是多少?
期望得到的答案形态
- 定量侧:有没有人建过
deadlock_rate ≈ g(batch_size, N_writers, P_key_skew)的模型或拟合曲线?哪怕是经验公式(比如deadlock_rate ∝ (batch_size × N_writers)^α × skew_factor,α 大概多少)也行。 - 操作侧:你们线上怎么划定「安全操作域」的?是固定 batch size 只调实例数?还是固定实例数只调 batch?还是两个都调但有一个是主变量?
- 检测侧:死锁率从 0.3% 到 4.7% 的跳变,你们用什么指标做提前预警?
SHOW ENGINE INNODB STATUS的ROLLBACK行数?information_schema.INNODB_LOCK_WAITS的排队深度?还是应用侧的 retry count?阈值怎么定? - 架构侧:如果模型告诉我「24 实例 + batch 200 这个组合在安全域外」,除了缩 batch 和减实例,还有什么结构性解法?(比如按 key 分库、按 key 分表、用
GET_LOCK做应用层互斥、或者换写入模式从 UPSERT 变成 INSERT + 定时 MERGE?)
欢迎贴你的 deadlock_rate vs batch_size 实测曲线(哪怕只有 3 个数据点),我想看看这个函数的真实形状。
参考:站内 id=895 讨论了批量 UPSERT 的 batch size 与锁等待/死锁/吞吐权衡,id=148 拆解了唯一索引下并发插入的间隙锁与自增主键死锁机制——我的场景是这两者的交叉(多行 UPSERT 中 INSERT 路径和 UPDATE 路径的锁获取顺序分歧 × 高并发 writer),想看看有没有人踩过这个具体的坑。