TiKV 运行满 2 年会 panic:795 天单调时钟溢出 bug(tikv#11940)复盘与预警建设原创
凌晨 3 点,告警群弹出一条信息:某 TiDB 集群的 tikv-0 节点发生重启。值班同学查看进程,发现 TiKV 存活时间从 795 天归零,日志最后一条是 tokio-runtime-worker panicked at 'index out of bounds: the len is 6 but the index is 6'。
这不是硬件故障,也不是配置变更。这是一个蛰伏了近 800 天的定时炸弹——TiKV 老版本把微秒级的单调时钟塞进 u32,进程连续运行约 795 天后溢出,索引越界直接 panic(tikv#11940 (opens new window))。官方 issue 的标题就叫 TiKV running over 2 years may panic。受影响的是早期的 v3.0.x、v4.0.x 及部分 v5.x;修复已合入各维护分支,v5.2.4、v5.3.2、v5.4.1、v6.0.0 及以上不再受此问题影响。
更尴尬的是,我们的告警体系当时只覆盖了 SQL 层(tidb-server)的重启事件,对于存储层(TiKV/PD/TiCDC)的进程异动完全是盲区。这次事件逼着我们把监控补齐到"对称"状态。
# 为什么偏偏是 795 天?单调时钟的 u32 微秒溢出
官方 issue 的标题就是结论:"TiKV running over 2 years may panic"——运行超过 2 年可能崩溃。2 年多正好对应约 795 天,社区因此把它叫作"795 天崩溃 bug"。
根因是单调时钟换算成微秒时的整型溢出。老版本 TiKV 在 RPC 超时 / 回退(backoff)相关的时间计算里,用 u32 存放以微秒为单位的时间戳。而 Linux 的 CLOCK_MONOTONIC 是"系统启动以来的总时长",随进程连续运行单调增长:
u32 上限 = 2^32 = 4,294,967,296 微秒
换算成天 = 4,294,967,296 / (1,000,000 x 3600 x 24) ≈ 49.7 天
2
单看这个数字会觉得 49.7 天就该炸——实际不会,因为参与运算的并非裸的单调时钟值,而是它与某个基准的增量。真正走到边界,是在进程连续运行约 795 天、增量累积到 u32 表达不下的时候:溢出后算出的值变成非法索引,Rust 的数组边界检查直接失败:
thread 'tokio-runtime-worker' panicked at
'index out of bounds: the len is 6 but the index is 6'
2
还需要一个"扳机"。溢出本身不会主动暴露,通常是网络波动、PD 节点断连之类导致 RPC 失败、重试/回退逻辑被唤醒时,才踩进这条溢出路径。这也解释了为什么它总在某次夜间网络抖动时爆掉,而不是精确停在第 795 天零点。
社区的复现方式很直接:用 Chaos Mesh 的时间注入(watchmaker)把 CLOCK_MONOTONIC 向前推进 68,719,436 秒(约 795 天),再 kill 掉 PD 制造 RPC 失败,几秒内就重现了同一个 panic。
- 越界发生在时间换算之后的索引路径上,是 Rust 的数组边界检查失败
- 失败直接向上传播为
panic,没有任何 graceful degradation 的余地 - 结果就是进程被操作系统回收,TiKV 节点失联,触发 Region 重新选举
运气好的话,这是单点故障;运气不好,如果同集群有多个节点是同一批次部署的,会在相近时间段内接连触发——形成"级联重启"。
# 诊断过程:版本比对是关键证据
定位到这个 bug 的过程,依赖一个关键观察:同集群内不同版本的 TiKV 节点表现不同。
我们复盘时发现:
- 节点 A(v5.4.0):运行 795 天,触发 panic 重启
- 节点 B(v5.4.0):运行 792 天,在节点 A 重启后 2 小时内也触发 panic
- 节点 C(v6.5.0):运行 836 天,完全未触发,进程持续稳定
这个对比立即缩小了排查范围:问题与 TiKV 版本强相关。顺着 panic 文本检索到 tikv#11940 (opens new window),确认受影响的是早期 v3.0.x / v4.0.x / 部分 v5.x;官方把有溢出风险的 u32 计数器换成了范围更大的类型、并修掉导致索引越界的逻辑,修复版本为 v5.2.4、v5.3.2、v5.4.1、v6.0.0+。
对暂时无法升级的老集群,官方给的规避手段也很朴素:在节点连续运行满 795 天之前分批滚动重启,把单调时钟重新归零。
结论:这是一个"已知且已修复"的老版本 bug,但生产环境还有老版本实例在跑,且已经逼近触发窗口。
# 三条 PromQL 告警规则的设计与理由
基于上述分析,我们设计了三条互补的告警规则,分别覆盖"事后发现"、"批量异常"、"事前预警"三个场景。
# 规则一:存储层重启覆盖(事后发现)
changes(process_start_time_seconds{job=~"tikv|pd|ticdc"}[10m]) > 0
为什么这样设:
process_start_time_seconds是标准 Prometheus 进程指标,节点重启时该值会重置为当前时间戳changes()函数在 10 分钟窗口内检测到值变化即触发,能及时捕获重启事件{job=~"tikv|pd|ticdc"}用正则同时覆盖存储层三大组件,补齐此前只监控job="tidb"的盲区
severity:warning(单纯重启不一定致命,需结合上下文判断)
# 规则二:集群级联重启探测(批量异常)
count by (cluster)(
changes(process_start_time_seconds{job="tikv"}[6h]) > 0
) >= 3
2
3
为什么这样设:
- 对单集群(按
cluster标签聚合)统计 6 小时内发生重启的 TiKV 节点数 >= 3表示"短时间内多台 TiKV 重启",可能是:- 运维批量升级(计划内,可抑制)
- 同批次节点集中触发老 bug(非计划,需紧急介入)
- 存储层网络/硬件故障(非计划,需紧急介入)
severity:critical(级联故障可能导致 Region 多数派失效,影响数据可用性)
# 规则三:795 天临界预警(事前预防,核心)
这是整个预警建设的核心。我们设置两档阈值:
二级预警(700~780 天):
(
(
(time() - process_start_time_seconds{job="tikv"}) / 86400
)
and on(instance)
(
tikv_server_info{version=~"v[1-5]\\..*"}
)
) >= 700 < 780
2
3
4
5
6
7
8
9
一级预警(780~795 天):
(
(
(time() - process_start_time_seconds{job="tikv"}) / 86400
)
and on(instance)
(
tikv_server_info{version=~"v[1-5]\\..*"}
)
) >= 780 < 795
2
3
4
5
6
7
8
9
为什么这样设:
- 计算运行天数:
(time() - process_start_time_seconds) / 86400得到进程已运行天数 - 版本过滤是关键:必须
and on(instance) tikv_server_info{version=~"v[1-5]\\..*"},只对可能受影响的 v5.x 及更早版本告警- 同集群里 v6.5.0 节点跑到 836 天也不会触发,不做过滤会误报
- 这条正则是粗过滤:v5.2.4 / v5.3.2 / v5.4.1 这些已打补丁的小版本同样会被命中。集群里若混有它们,需要再叠一层排除,否则是误报
tikv_server_info是 TiKV 暴露的版本信息指标,包含version标签
- 区间窗口写法:
>= 700 < 780是 PromQL 合法的链式比较,表示"大于等于 700 且小于 780"- 这种写法能确保节点一旦超过 780 天,二级预警自动熄灭,一级预警接手
- 避免"永远大于 700"导致的持续刷屏
severity:warning(二级)、critical(一级)
# 验证:用历史时间点回放确认命中
规则上线前,必须验证表达式真的能命中真实发生过的重启事件。
我们在 Prometheus 上执行历史查询(使用 @ 修饰符指定过去时间点):
# 节点 A 重启前的 795 天临界点
count(
(
(time() - process_start_time_seconds{job="tikv",instance="x.x.x.x:20180"}) / 86400
)
and on(instance)
(
tikv_server_info{version=~"v[1-5]\\..*"}
) @ 1693017600
) >= 795
2
3
4
5
6
7
8
9
10
结果显示:在该节点实际 panic 前 10 分钟,表达式值为 1,证明如果规则当时存在,会提前 10 分钟触发一级预警。
重复此验证对其他历史重启事件,确认覆盖率达到 100%,才将规则设为 enabled: true。
# 坑与边界
# 坑 1:运行时长类告警必须 join 版本信息
本文的核心教训。任何基于 uptime 的临界预警:
- 磁盘寿命预警(SMART 剩余寿命)
- 证书有效期预警(TLS 证书到期)
- 已知版本 bug 触发时间点预警(本文场景)
如果目标系统存在多版本混跑,必须把版本标签 join 进表达式。否则:
- 误报:对已修复版本告警,浪费值班精力
- 漏报:假设新版本有完全不同的 bug,旧规则可能不适配
举一反三:证书有效期告警应形如:
(
(last_over_time(cert_not_after[1h]) - time()) / 86400
) < 30
and on(instance) cert_info{subject=~".*internal.*"}
2
3
4
用 and join 证书主题信息,只对内部证书告警,避免开发测试证书刷屏。
# 坑 2:PromQL 链式区间比较的写法和适用场景
expr >= a < b 是合法的 PromQL,等价于 (expr >= a) and (expr < b)。适用场景:
- 需要定义明确的阈值窗口
- 避免"越界后永远告警"的尴尬
不适用场景:
- 需要复杂逻辑组合时,显式
and/or更易读 - 某些旧版 Prometheus 或 N9E 代理可能不支持,需提前测试
# 坑 3:存储层 vs 计算层监控要对称
分布式数据库常见的监控盲区:
- 只盯 SQL 层(tidb-server),认为"对外服务正常 = 集群健康"
- 忽略存储层(TiKV)、调度层(PD)、变更层(TiCDC)的内部异动
正确的分层监控心智模型:
| 层级 | 组件 | 关键指标 |
|---|---|---|
| SQL 层 | tidb-server | 连接数、QPS、慢查询、重启 |
| 存储层 | TiKV | Region 状态、存储容量、 compaction 压力、重启 |
| 调度层 | PD | Leader 分布、热点调度、成员健康 |
| 变更层 | TiCDC | changefeed 延迟、checkpoint 进度、重启 |
任何一层的重启、延迟飙升、资源耗尽,都需要独立告警,不能依赖"上层会不会报错"来间接发现。
# 下一步
🤖 Agent 可直接解析的元数据块(点击展开)
{
"_meta": {
"doc_version": "2026-08-27",
"article_id": "tikv-795day-tokio-timer-panic",
"profile_context": "TiDB 运维/SRE",
"estimated_setup_time": "30min(仅告警规则),若涉及版本升级需计划内维护窗口"
},
"quick_start": {
"step_1": "在 Prometheus/N9E 中导入三条 PromQL 告警规则",
"step_2": "确认 tikv_server_info 指标存在且包含 version 标签",
"step_3": "用历史时间点 @{timestamp} 回放验证表达式能命中",
"step_4": "设置告警路由:二级预警 → 邮件/IM,一级预警 → 电话/升级",
"step_5": "对 700~795 天节点制定计划内滚动重启方案(升级至 v5.2.4/v5.3.2/v5.4.1/v6.0.0+,或先滚动重启重置单调时钟)"
},
"safety_rules": [
"运行时长类告警必须 join 版本信息(tikv_server_info.version)",
"规则上线前必须用 @timestamp 回放验证命中真实事件",
"存储层/计算层监控要对称,不能只盯 tidb-server",
"795 天临界预警只针对未打补丁的 v5.x 及更早版本(version=~'v[1-5]\\..*' 为粗过滤,需再排除 v5.2.4/v5.3.2/v5.4.1)",
"一级预警(critical)必须配置电话/升级通道,不能只做 IM 通知"
],
"promql_rules": {
"restart_detection": |
changes(process_start_time_seconds{job=~'tikv|pd|ticdc'}[10m]) > 0
"cascade_restart": |
count by (cluster)(changes(process_start_time_seconds{job='tikv'}[6h]) > 0) >= 3
"795_day_warning": |
(((time() - process_start_time_seconds{job='tikv'}) / 86400)
and on(instance) (tikv_server_info{version=~'v[1-5]\\..*'})) >= 700 < 780
"795_day_critical": |
(((time() - process_start_time_seconds{job='tikv'}) / 86400)
and on(instance) (tikv_server_info{version=~'v[1-5]\\..*'})) >= 780 < 795
},
"affected_versions": {
"vulnerable": "早期 v3.0.x、v4.0.x 及部分 v5.x(RPC 超时/backoff 用 u32 存微秒级单调时钟,795 天后溢出)",
"fixed": "v5.2.4、v5.3.2、v5.4.1、v6.0.0 及以上(计数器扩宽,修掉索引越界逻辑)"
},
"verification": {
"historical_replay": "使用 @timestamp 修饰符在 Prometheus 上回放表达式",
"multi_version_check": "确保同集群内 v6.x+ 节点不因运行天数被误报"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
AI Agent 使用指南:
- 当用户说"TiKV 莫名其妙重启"→询问运行天数和版本,判断是否命中本文场景
- 当用户需要告警规则→直接输出 promql_rules 中的四条表达式
- 当用户计划升级→强调 affected_versions.fixed 的版本阈值(v5.2.4 / v5.3.2 / v5.4.1 / v6.0.0+);无法升级则在满 795 天前滚动重启