NTP 时间不同步导致 Pod 反复重启排查
NTP 时间不同步导致 Pod 反复重启排查
起因是收到 k8s 集群多个节点上的 node-feature-discovery-worker Pod(DaemonSet)重启告警,但集群当时负载并不高,不应该是负载过高导致的
排查过程
1. 看容器上一次退出的日志
node-feature-discovery-worker 容器:
E0808 03:45:10.665134 1 main.go:83] "error while running" err="failed to advertise features (via CRD API): failed to get NodeFeature object: Unauthorized"
报错本质是:这个组件调用一次 apiserver 失败就直接 os.Exit,没有内部重试机制,于是被 kubelet 判定为容器异常退出,反复重启。问题变成了:为什么会间歇性认证不了 apiserver?
2. 查 apiserver 日志,定位到鉴权失败
$ kubectl logs -n kube-system kube-apiserver-master-01 --since=48h | grep -c "not valid yet"
501
$ kubectl logs -n kube-system kube-apiserver-master-03 --since=48h | grep -c "not valid yet"
63
$ kubectl logs -n kube-system kube-apiserver-master-02 --since=48h | grep -c "not valid yet"
0
具体报错:
E0808 07:36:19.677228 authentication.go:75] "Unable to authenticate the request" err="[invalid bearer token, service account token is not valid yet]"
为什么 "not valid yet" 直接指向时间问题
这句话是整个排查的转折点。要理解它为什么几乎只可能是时钟问题,需要知道 k8s ServiceAccount Token 的校验机制:
- kubelet 在节点上通过 TokenRequest API 向 apiserver 申请一个短期 Token,这个 Token 是一个 JWT,里面带
iat(issued at,签发时间)和exp(过期时间)。 - apiserver 在校验 Token 时,会拿自己当前的系统时间和 Token 里的
iat/exp比较。
设想一下:kubelet 刚刚(这一秒)才向 apiserver 申请到这个 Token,Token 里的 iat 就是当前时刻。然后 nfd-worker 拿着这个刚出炉的 Token 去访问 apiserver,apiserver 却说"这个 Token 还没到生效时间"。
一个"现在才签发"的 Token 怎么会"还没生效"?唯一的解释是:
apiserver 校验时用的"当前时间"比 Token 签发时用的"当前时间"要慢。
也就是说——签发 Token 的那台 apiserver(或 kubelet 所在节点)时钟偏快,校验 Token 的这台 apiserver 时钟偏慢。两边对"现在几点"的认知不一致,才会出现"刚签发的 Token 在另一边看来是未来时间签的"。普通的 RBAC 配置错误、Token 过期、网络问题都不会产生"not valid yet"这种措辞——这个词组本身就是时间问题的指纹。
三台 master 的报错次数差异巨大,且 master-02 完全没有——这个分布进一步印证了上面的推断:如果只是单纯的 RBAC/配置问题,三台 apiserver 的报错应该差不多;但这里三台差异巨大,且有一台完全为 0,这种"和具体 apiserver 实例强相关"的分布,几乎只可能是每台 apiserver 自己的本地状态(时钟)不同导致的。配置是共享的、Token 是同一个,唯一不同的就是各台机器的墙上时钟。
3. 对比三台 master 的真实时间
在同一秒内连续 SSH 到三台机器执行 date -u:
参考时间(本地) 2026-08-08 07:40:49.92
master-01 2026-08-08 07:39:20.46 慢 ~89 秒
master-02 2026-08-08 07:40:46.24 慢 ~4 秒
master-03 2026-08-08 07:39:37.07 慢 ~72 秒
三台机器的 timedatectl status 全部显示:
System clock synchronized: no
偏差量和 apiserver 报错次数完全对应:偏差越大,"not valid yet" 报错越多。
4. 顺藤摸瓜查 NTP 配置
集群的时间服务器均指向内网的一个时间服务器
内网时间服务器是使用chrony,排查发现chrony服务出现问题,所以集群节点时间同步失败
5. 为什么集群时间同步告警没有触发
集群里配置了“如果集群节点时间偏差10s触发告警”这条告警规则,但是实际上并没有触发:
- alert: NodeClockSkewDetected
expr: |
(
node_timex_offset_seconds > 10
and
deriv(node_timex_offset_seconds[5m]) >= 0
)
or
(
node_timex_offset_seconds < -10
and
deriv(node_timex_offset_seconds[5m]) <= 0
)
for: 10m
直接在 Prometheus 里查故障期间 node_timex_offset_seconds 的真实值:
$ curl -s 'http://localhost:9090/api/v1/query?query=max_over_time(node_timex_offset_seconds{nodename="master-01"}[2d])'
{"value":[..., "0.001798053"]}
$ curl -s 'http://localhost:9090/api/v1/query?query=min_over_time(node_timex_offset_seconds{nodename="master-01"}[2d])'
{"value":[..., "-0.047216027"]}
全程都在 ±0.05 秒以内,从未超过 10 秒的阈值——即便同一时段内 master-01 的真实墙上时钟已经偏差到 89 秒。
再查 node_timex_sync_status(1=已同步,0=未同步)在同一时间段的 range 数据:
[[1786109306,"0"],[1786111106,"0"], ... ,[1786174106,"0"],
[1786175906,"1"],[1786177706,"1"], ...]
在我们修复 NTP 之前的 18 小时窗口里全部是 0,修复后立刻变成 1。
再查 node_timex_maxerror_seconds(内核估计的最大误差上限):
故障期间:稳定钉死在 16(Linux 内核对"未同步"状态设的误差上限硬顶值)
修复后:0.003 ~ 0.9 之间波动(正常范围)
根因:node_timex_offset_seconds 反映的不是"墙上时钟和真实时间差多少",而是"上一次 NTP 客户端成功从服务器拿到应答、并通过 adjtimex() 写入内核的修正量"。由于 systemd-timesyncd 从来没有成功连上过服务器(一直超时),它从来没有调用过 adjtimex() 写入任何真实偏移,这个指标就一直停留在陈旧的接近 0 的值上,跟真实偏差完全脱节。告警规则选错了指标,覆盖不到"NTP 彻底失联"这种最坏场景,只能覆盖"NTP 在正常工作、只是缓慢漂移"的场景。
修复
1. 修复 NTP 服务器
恢复内网时间服务器上的 chrony
2. 全量节点重新同步
对集群节点执行 systemctl restart systemd-timesyncd,让它们重新握手(此前一直是失败状态,需要主动触发重试,不会自愈得很快)
3. 补齐告警盲区
原有规则加上 node_timex_sync_status == 1 前提,只在"已同步但仍缓慢漂移"场景下工作;再新增一条不依赖节点自证的规则,直接拿 Prometheus 采集样本时刻的时间戳跟节点上报的系统时间做差:
- alert: NodeClockNotSynced
expr: node_timex_sync_status == 0
for: 15m
labels:
owner: system
severity: warning
annotations:
summary: "节点 {{ $labels.nodename }} 时钟未同步"
description: "节点 {{ $labels.nodename }} 的 NTP/chrony 已经超过15分钟处于未同步状态(node_timex_sync_status=0),请检查时间服务是否可达。"
# 不依赖节点自身 NTP 子系统的"自证":直接用 Prometheus 采集样本时刻的时间戳
# 与节点上报的系统时间做差,哪怕本机 NTP/chrony 状态汇报有误(比如同步到了
# 错误的服务器,或 sync_status 卡在错误的值),也能独立测出真实的时钟偏差。
- alert: NodeClockSkewVsPrometheus
expr: |
abs(
node_time_seconds{job="node-exporter"}
-
timestamp(node_time_seconds{job="node-exporter"})
) > 10
for: 10m
labels:
severity: warning
owner: system
annotations:
summary: "节点 {{ $labels.nodename }} 时钟与监控系统时间不一致"
description: "节点 {{ $labels.nodename }} 的系统时间和 Prometheus 采集时刻的时间相差 {{ printf \"%.1f\" $value }} 秒,与本机 NTP/chrony 是否报告“已同步”无关,说明真实时钟已经偏移,请检查 NTP/chrony 配置。"
NodeClockSkewVsPrometheus 的关键价值:哪怕节点自己的 NTP 子系统汇报的状态是错的(比如同步到了错误的服务器、或者 sync_status 卡死),只要节点真实系统时间和 Prometheus 服务器的时间对不上,就一定能测出来。