背景:PmaControl 最繁忙的写入路径
每隔几秒,每个 PmaControl 代理都会从您的 MariaDB / MySQL 服务器收集数百项指标:状态变量、InnoDB 计数器、复制状态、查询摘要。所有数据都汇聚到 Integrate::insert_value(),由它写入 ts_value_general_* 系列表——这是整个应用中最繁忙的写入路径。
为了绝不超过 max_allowed_packet,元组会被分组为块(chunk):SQL 预算根据服务器动态计算(并留有安全余量),一批 200,000 条测量数据会变成大约 32 条 INSERT INTO ... VALUES (...), (...), ... 语句,每条约 256 KiB。
在此之前,这 32 条语句以 autocommit 模式执行:32 次 INSERT,32 次隐式 COMMIT。
缺陷:静默的部分写入
如果第 17 个块失败了会怎样——表满、死锁、连接断开?
修复之前:第 1 到 16 块保持已写入状态,第 17 到 32 块丢失,而下游的记账逻辑(服务器/变量关联、摄入检查点)可能照常执行,仿佛一切成功。一个逻辑批次变成了一次静默的部分写入——对监控数据而言这是最糟糕的情形:图表上出现无人报告的空洞。
三个独立的 issue 都指向同一个根因:
- #1467 / #1471 —— 批次中途失败导致部分写入;
- #1468 / #1472 —— 单个超出预算的元组仍被发送到服务器,注定在
max_allowed_packet上失败; - #1469 —— 一个静默回退可能禁用动态预算检测。
修复:一个批次 = 一个事务
修复(PR #2323)引入了 executeTimeSeriesInsertBatch():同一指标类型的所有块现在都被包裹在单个事务中:
START TRANSACTION;
INSERT INTO ts_value_general_int (...) VALUES (...), (...), ...; -- 块 1
INSERT INTO ts_value_general_int (...) VALUES (...), (...), ...; -- 块 2
-- ... 其余 30 个块 ...
COMMIT;
任何失败——falsy 结果、非良性警告、异常——都会触发 ROLLBACK 并抛出带有安全诊断元数据(表名、块号、估算字节数、预算)的类型化异常。测量文件被保留以供重放:要么整批持久化,要么一条不写。
额外收益:估算大小已超过预算的孤立元组会在事务开启之前被检测出来——注定失败的 INSERT 根本不会发送到服务器。
12% 之问:代价是多少?
一个横跨 32 条语句的事务意味着 InnoDB 侧的额外开销:更长的 undo log、更久持有的锁。我们预计会付出少量开销,并认为这是原子性保证完全值得的代价。
合并之前我们做了基准测试。协议:
- 精确重放
insert_value()的热循环(分块 → 构建 SQL → 执行),而非合成微基准; - 200,000 条确定性元组写入
ts_value_general_int(主键唯一),预算 256 KiB → 32 块; - MariaDB 11.8、PHP 8.5.8、InnoDB 表,每轮之间
TRUNCATE,1 轮预热 + 5 轮计时测量; - 同一 LXC 容器、同一数据,两次测量之间只有应用代码不同。
结果:
| 轮次 | 之前(autocommit ×32) | 之后(1 个事务) |
|---|---|---|
| 1 | 1.127 s | 0.927 s |
| 2 | 1.199 s | 0.947 s |
| 3 | 1.155 s | 1.012 s |
| 4 | 1.168 s | 1.019 s |
| 5 | 1.157 s | 1.026 s |
| 中位数 | 1.157 s | 1.012 s |
中位数快了 12.5%。 原子性修复没有任何代价:它反而节省时间。摄入吞吐量从约每秒 173,000 条提升到 198,000 条。
为什么更快:COMMIT 的隐藏成本
答案在于 COMMIT 在 InnoDB 中真正做了什么。
每次提交时,InnoDB 必须让事务持久化:写入 redo log 页面,并且——在 innodb_flush_log_at_trx_commit = 1(默认值,也是唯一真正安全的值)下——强制对日志文件执行 fsync()。这个刷盘是事务生命周期中最昂贵的操作:它要物理等待存储设备。
在 autocommit 模式下,每条 INSERT 都是独立事务:
- 之前:32 块 = 32 次 COMMIT = 32 次 redo log 刷盘;
- 之后:32 块 = 1 次 COMMIT = 1 次 redo log 刷盘。
省下的 31 次同步远远超过略长的 undo log 的开销。这与「把 SQL 导入包在事务里会快得多」以及「MariaDB 的 group commit 在并发事务间摊薄刷盘成本」是同一机制——只是这里应用在单个逻辑批次内部。
收益取决于硬件:在 fsync() 很慢的存储上(机械磁盘、没有安全写缓存的虚拟化环境),差距会进一步拉大;在高速 NVMe 上会收窄。但符号永远不变:单事务始终至少不会更慢。
要点总结
- 原子性不是需要付费的奢侈品,它常常本身就是优化。 将相关写入归入一个事务可以消除 redo log 刷盘——安全与性能方向一致。
- 对真实代码路径做基准测试。 正是在真实数据库上重放真实热循环,才把「我们接受开销」变成了「根本没有开销」。
- 失败必须响亮且彻底。 写了一半的指标批次比重放一次的批次更糟:自此修复起,PmaControl 保证时序摄入的全有或全无。
该修复自 2026 年 7 月 25 日起已在 PmaControl 的 master 分支提供。
评论 (0)
暂无评论。
发表评论