TiKV 也有"寿命":一次 795 天 tokio 定时器溢出 panic 的复盘与预警建设原创
凌晨 3 点,告警群弹出一条信息:某 TiDB 集群的 tikv-0 节点发生重启。值班同学查看进程,发现 TiKV 存活时间从 795 天归零,日志最后一条是 tokio-runtime-worker panicked at 'timer wheel overflow'。
这不是硬件故障,也不是配置变更。这是一个蛰伏了近 800 天的定时炸弹——TiKV 老版本(v6 及更早)存在的已知 bug:tokio 定时器 wheel 溢出。它只会在进程运行到约 700~795 天时触发,此前从未见过。
更尴尬的是,我们的告警体系当时只覆盖了 SQL 层(tidb-server)的重启事件,对于存储层(TiKV/PD/TiCDC)的进程异动完全是盲区。这次事件逼着我们把监控补齐到"对称"状态。
# 为什么偏偏是 795 天?tokio timer wheel 溢出的原理
TiKV 底层使用 tokio 作为异步运行时,而 tokio 的早期版本(v0.2.x 及之前)的 timer wheel 使用 32 位整数存储时间戳,时间精度为毫秒。
32 位有符号整数的最大值约为 2.147e9 毫秒,换算成天:
2.147e9 ms ÷ 1000 = 2.147e6 秒
2.147e6 ÷ 86400 ≈ 24.85 天(以单个 slot 计)
2
但 tokio 的 timer wheel 并非单一精度,它采用分层设计(通常 4~6 层),每层负责不同精度的时间范围。当进程运行时间超过某个层的处理上限时,wheel 在层级转换过程中发生整数溢出,触发 panic。
具体到 TiKV 使用的 tokio 版本,这个临界点在 700~795 天 之间。这不是估算,而是社区 issue 和源码级验证的结论:
- 当
Instant::now()的单调时钟值超过 timer wheel 最大可表示范围时,底层Wheel::poll()方法会触发边界检查失败 - 失败直接向上传播为
panic,没有任何 graceful degradation 的余地 - 结果就是进程被操作系统回收,TiKV 节点失联,触发 Region 重新选举
运气好的话,这是单点故障;运气不好,如果同集群有多个节点是同一批次部署的,会在相近时间段内接连触发——形成"级联重启"。
# 诊断过程:版本比对是关键证据
定位到这个 bug 的过程,依赖一个关键观察:同集群内不同版本的 TiKV 节点表现不同。
我们复盘时发现:
- 节点 A(v6.1.0):运行 795 天,触发 panic 重启
- 节点 B(v6.1.0):运行 792 天,在节点 A 重启后 2 小时内也触发 panic
- 节点 C(v7.5.0):运行 836 天,完全未触发,进程持续稳定
这个对比立即缩小了排查范围:问题与 TiKV 版本强相关。查阅 TiKV/TiDB 官方 changelog 和社区 issue,确认 tokio 版本在 v7.x 系列已升级,timer wheel 的实现改为 64 位时间戳,彻底消除了 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-6]\\..*"}
)
) >= 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-6]\\..*"}
)
) >= 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-6]\\..*"},只对 v6 及更早版本告警- 同集群里 v7.5.0 节点跑到 836 天也不会触发,不做过滤会误报
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-6]\\..*"}
) @ 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 天节点制定计划内滚动重启方案(升级至 v7.5.0+)"
},
"safety_rules": [
"运行时长类告警必须 join 版本信息(tikv_server_info.version)",
"规则上线前必须用 @timestamp 回放验证命中真实事件",
"存储层/计算层监控要对称,不能只盯 tidb-server",
"795 天临界预警只针对 v6 及更早版本(version=~'v[1-6]\\..*')",
"一级预警(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-6]\\..*'})) >= 700 < 780
"795_day_critical": |
(((time() - process_start_time_seconds{job='tikv'}) / 86400)
and on(instance) (tikv_server_info{version=~'v[1-6]\\..*'})) >= 780 < 795
},
"affected_versions": {
"vulnerable": "v6.x 及更早版本",
"fixed": "v7.5.0 及更高版本(tokio 已升级,timer wheel 使用 64 位时间戳)"
},
"verification": {
"historical_replay": "使用 @timestamp 修饰符在 Prometheus 上回放表达式",
"multi_version_check": "确保同集群内 v7.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 的版本阈值(v7.5.0+)