服务器时间悄悄跑偏排查实战:chrony 漂移原因、NTP 被墙的判读与 TLS 证书过期误判

服务器时间悄悄跑偏排查实战:chrony 漂移原因、NTP 被墙的判读与 TLS 证书过期误判

时间这件事平时没人注意,一旦出问题就是组合拳式的疑难杂症:接口签名验证全部失败、TLS 握手报证书未生效或已过期、日志里同一个请求的先后顺序完全错乱、数据库主从的 binlog 位置对不上、甚至定时任务该跑没跑。而最要命的是,这些症状看起来都跟「时间」没关系,排查方向很容易跑偏。这篇文章讲清楚三件事:怎么发现时间跑偏了、chrony/ntpd 同步失败该怎么读、以及时间错误是怎么伪装成证书问题的。

第一步:确认时间到底差多少,别只看 date

date 的输出是给人看的,排查时要看机器可读的精确偏差。核心命令是 timedatectl,它把时区、NTP 同步状态、以及最重要的 System clock synchronized 一行都列出来了:

timedatectl
# 关键输出解读:
#   Local time / Universal time  本地与 UTC 时间
#   Time zone                    时区,Asia/Shanghai 应该是 CST +0800
#   System clock synchronized    yes 才代表已同步;no 意味着 NTP 没生效
#   NTP service                  active 才代表服务在跑
#   RTC in local TZ              no 才对,硬件时钟应存 UTC

# 查看与管理
timedatectl set-timezone Asia/Shanghai
timedatectl set-ntp true
timedatectl status

如果要看具体偏差了多少,用 chronyc 的 tracking 和 sources:

chronyc tracking
# Reference ID    : NTP 源标识
# Stratum         : 层级,1 是直连原子钟,3-5 是正常网络层级
# System time     : 你的系统时间相对 NTP 的偏移,前面带 + 表示快了
# Last offset     : 上一次测量到的偏移
# RMS offset      : 偏移的均方根,长期看这个值稳定在毫秒级才正常
# Frequency       : 本机时钟的固有漂移速率,单位 ppm
# Skew            : 频率估计误差
# Root delay / Root dispersion : 到源站的往返延迟与累积误差

chronyc sources -v
# 每个源前面有状态字符:
#   ^*  当前选中的同步源(System peer)
#   ^+  备选源,可用但未选中
#   ^-  被排除的源
#   ^?  不可达或未通过质量检查(重点看这个)
#   ^x  被判定为 falseticker(时间明显不对的源)

判读逻辑:如果 sources -v 里所有行都是 ^?,说明一个源都没连上,基本可以确定是 UDP 123 被防火墙挡了;如果全是 ^-,说明连上了但被排除,通常是这个源本身漂移太大或者被标记为不信任;如果只有 ^* 一行而其他都是 ^?,那还没问题,说明有一个源在工作,但要留意单点依赖。

第二类问题:NTP 出网被墙或 DNS 解析失败

国内服务器最常见的场景是:机房默认的 NTP 服务器不可达,或者云厂商内网 NTP 地址变了。表现是 chronyc sources -v 里所有源长期是 ^?,System time 的偏差持续增大。这时候按顺序验证四件事:

# 1. UDP 123 出网是否通(NTP 是 UDP,不是 TCP,别用 telnet 测)
nc -u -z -w3 203.107.6.88 123 && echo "udp123 ok" || echo "udp123 blocked"
# 或者直接用 chrony 的交互式命令探测
chronyc -a 'burst 4/4'
chronyc waitsync 30 0.5

# 2. DNS 是否解析得出 NTP 域名
dig +short ntp.aliyun.com
getent hosts time1.cloud.tencent.com

# 3. 本机防火墙是否放行 ntp 响应(OUTPUT 方向的状态包)
iptables -L OUTPUT -n -v | grep -i ntp
nft list ruleset | grep -i -A3 ntp

# 4. 是否被上游设备限速(NTP 反射攻击防护常导致)
journalctl -u chronyd --since "1 hour ago" | tail -40

国内可用的 NTP 源,建议一次配三到四个不同上游,避免单点:

# /etc/chrony/chrony.conf
pool ntp.aliyun.com iburst
pool ntp1.aliyun.com iburst
server time1.cloud.tencent.com iburst
server ntp.ntsc.ac.cn iburst

# iburst: 启动时快速发 4 个包加速首次同步(不要去掉)
# 允许本地时钟在首次同步时直接跳变(服务器建议开启,避免缓慢追平)
makestep 1.0 3
# 记录时钟漂移,重启后快速收敛
driftfile /var/lib/chrony/chrony.drift
rtcsync
logdir /var/log/chrony

makestep 1.0 3 这一行值得单独说。它的含义是「在前 3 次同步中,如果偏差大于 1 秒,就直接把系统时钟跳过去;之后如果偏差大于 1 秒,则用平滑方式(slew,微调时钟频率)慢慢追」。服务器刚启动时时间差往往是几分钟,靠 slew 慢慢追可能要几小时,这期间的日志时间戳全是错的,所以启动阶段允许跳变很重要。但反过来,对于已经跑了很久的生产机,如果时间差了好几分钟,直接 makestep 跳变会让 cron 的任务执行时序错乱,甚至让某些依赖单调递增的中间件出问题,所以运维上有个惯例:先停掉应用再校时,或者低峰期处理。

第三类问题:时间错误伪装成 TLS 证书问题

这是最容易被误判的一类。你收到告警说某个 HTTPS 接口全部握手失败,去看证书,发现有效期明明是明年,但 curl 报的是:

curl -v https://api.example.com/
# curl: (60) SSL certificate problem: certificate is not yet valid
# 或者
# curl: (60) SSL certificate problem: certificate has expired

「not yet valid」这个报错的含义是「当前系统时间早于证书的 notBefore」,「has expired」是「当前系统时间晚于 notAfter」。本地时间一跑偏,这两种报错就会凭空出现——证书本身完全没问题。先做这个交叉验证,一分钟定论:

# 看证书的真实有效期区间
echo | openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null \
  | openssl x509 -noout -dates
# notBefore=Jul  1 00:00:00 2026 GMT
# notAfter =Sep 29 23:59:59 2026 GMT

# 看本机时间
date -u

# 交叉比对:如果 date -u 落在区间外,问题在本机时间,不在证书

另一个高频误判是 Let's Encrypt 的续期。ACME 协议在签发前会校验请求的时间戳,如果本机时间偏差过大(一般超过几分钟),certbot 会报 JWS has an invalid anti-replay nonce 或者 The request signature is invalid,看起来像密钥问题,实际是时间问题。这类报错的排查顺序永远是:先 timedatectl 看同步状态,再 openssl 看证书,最后才怀疑密钥和 ACME 客户端。

第四类问题:容器与虚拟机的时钟源差异

如果你的应用跑在容器里,会多出两层时间问题的来源。第一层是容器直接共享宿主机的内核时钟,所以容器内一般不需要也不能启动 chronyd(会报时钟权限错误)。容器里出现时间偏差,根因一定在宿主机上,去宿主机修。第二层是虚拟机,尤其是 KVM/QEMU 环境,如果宿主机的时钟源没配好,虚拟机的时钟会持续漂移,表现为每天固定慢几秒到几十秒,这种「持续的、方向一致的漂移」用 chronyc tracking 里的 Frequency 值能直接看出来——一个正常机器的固有频率偏差通常是几十 ppm,如果看到成百上千 ppm,说明虚拟化层的时钟中断有问题,需要在宿主机上检查 kvm-clock 是否启用。

# 查看当前内核使用的时钟源
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
cat /sys/devices/system/clocksource/clocksource0/available_clocksource
# KVM 虚拟机应该看到 kvm-clock 在可用列表中并被优先选用

# 虚拟机内部检查时钟漂移速率
chronyc tracking | grep -E "Frequency|Skew"

# 容器内的正确做法:不要装 ntpd,直接确认与宿主机一致
docker run --rm alpine date -u
date -u

第五类问题:应用层自己不信任时间,反而放大故障

服务器时间修好了,业务未必就恢复——因为很多应用在启动时把时间相关的判断缓存住了,后续不重新计算。三个典型例子:

一是 JWT 令牌校验。JWT 的 exp 和 nbf 都是绝对时间戳,服务端校验时会和当前时间比对。如果签发令牌时服务器时间快了 10 分钟,那么这个令牌在正确时间的服务器上会被判定为「未来才生效」,报 nbf 校验失败;反之如果签发时慢了,令牌的有效期会被意外拉长,存在安全风险。修完时间之后,所有已签发的令牌都要作废重发,做法是轮换签名密钥或者把令牌的版本号加一。

二是缓存与限流的滑动窗口。按「当前分钟」做键的限流器(比如键名里带 date +%Y%m%d%H%M),时间一跳变,所有计数器归零或者直接跳到下一个窗口,表现为限流失效或误拦截。这类问题不常被联想到时间上,但如果观察到限流在某个整点前后行为异常,大概率就是这个原因。

三是数据库主从复制。从库用 Seconds_Behind_Master 判断延迟,这个值本质是「主库 binlog 事件的时间戳」与「从库当前时间」的差。如果从库时间比主库快,这个值会显示成奇数或者突然变成一个巨大的数;更重要的是基于 GTID 或者带时间戳的冲突解决策略(比如部分多主方案按 timestamp 判新旧),时间不一致会导致数据覆盖方向错误,这是真正会造成数据损坏的一类。所以主从机器的时钟同步必须纳入强制巡检项。

处理原则很简单:时间校准完成后,把所有「有状态地依赖时间」的组件都重启一遍或者让它们重新读取一次时间。检查清单如下:

# 1. PHP-FPM / 应用进程需要重载才能拿到新时间(部分框架在启动时缓存时区与时间)
systemctl reload php8.2-fpm
systemctl reload nginx

# 2. 数据库连接池里的连接时间戳可能过期,择机重建连接
mysqladmin status

# 3. 检查日志里时间跳变前后的异常,评估影响窗口
journalctl --since "1 hour ago" | grep -iE "time|clock|go away|out of sync"

时区和本地时间造成的「差 8 小时」类问题

有一类和真实时间无关但症状极像的问题:机器时间是对的,只是时区配错了。表现是 date 显示的时间和你心里的时间差整整 8 小时,或者日志里记录的时间比事件发生的真实时刻早了 8 小时。排查第一步不是怀疑 NTP,而是看时区:

timedatectl | grep -i "time zone"
cat /etc/timezone
ls -l /etc/localtime
# /etc/localtime 应该是指向 /usr/share/zoneinfo/Asia/Shanghai 的软链

# 修正
timedatectl set-timezone Asia/Shanghai
# 或手动
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

# 检查硬件时钟是否被误当成本地时间(这会导致重启后时区错乱)
hwclock --show
timedatectl | grep "RTC in local TZ"   # 必须是 no
# 如果是 yes,硬件时钟存的是本地时间,重启或换时区后会双重偏移
timedatectl set-local-rtc 0

另一个高频场景是应用层面的时区。同样的机器时间,PHP 的 date() 输出可能和系统 date 差 8 小时,因为 PHP 有自己的 date.timezone 配置项。这类问题往往是「同一个数据库里存了两套时间」的根源:一台机器上的脚本按本地时间写,另一台按 UTC 写,做数据统计时怎么算都对不上。统一的规矩是:数据库和日志一律存 UTC 时间戳(用 timestamp 或 bigint),只在展示层做时区转换。养成这个习惯之后,「差 8 小时」这类问题会彻底消失。

# 检查 PHP 时区设置
php -i | grep -i "date.timezone"
grep -r "^date.timezone" /etc/php/*/fpm/php.ini

# 检查 MySQL 时区
mysql -e "SELECT @@global.time_zone, @@session.time_zone, NOW(), UTC_TIMESTAMP();"

一套完整的巡检脚本

把上面的检查点串成一个可以直接跑在 cron 里的巡检脚本,输出非空即告警:

#!/bin/bash
# /usr/local/bin/check_clock.sh —— 时间同步巡检
set -uo pipefail
ALERT=""

# 1. NTP 同步状态
SYNC=$(timedatectl show -p NTPSynchronized --value 2>/dev/null)
[ "$SYNC" != "yes" ] && ALERT="${ALERT}NTP 未同步; "

# 2. 偏差超过 1 秒告警
OFFSET=$(chronyc tracking 2>/dev/null | awk '/System time/ {print $4$5$6}')
OFF_SEC=$(echo "$OFFSET" | sed 's/[^0-9.]//g;s/^$/0/')
if awk "BEGIN{exit !(${OFF_SEC:-0} > 1.0)}"; then
  ALERT="${ALERT}时间偏差 ${OFFSET}; "
fi

# 3. 至少有一个可用同步源
GOOD=$(chronyc sources 2>/dev/null | grep -cE '^\^\*')
[ "$GOOD" -lt 1 ] && ALERT="${ALERT}无可用 NTP 源; "

# 4. 时区正确
TZ=$(timedatectl show -p Timezone --value 2>/dev/null)
[ "$TZ" != "Asia/Shanghai" ] && ALERT="${ALERT}时区为 ${TZ}; "

if [ -n "$ALERT" ]; then
  echo "CLOCK ALERT on $(hostname): $ALERT" >&2
  exit 1
fi
echo "clock ok: offset=$OFFSET tz=$TZ"

配进 crontab,注意这个脚本本身不依赖时间准确性,所以时间错乱时照常能跑:

# 每 10 分钟检查一次
*/10 * * * * /usr/local/bin/check_clock.sh || logger -t clock-check "clock drift detected"

总结与排查顺序

遇到任何「多个不相关服务同时报错」的情况,把时间检查放在排查清单的前三项。固定顺序是:timedatectl 看同步状态与时区 → chronyc tracking 看偏移量级 → chronyc sources -v 看源的健康字符 → 全 ^? 就查 UDP 123 出网与 DNS → 时间确认对了再去看 TLS 证书和日志。记住两个反直觉的点:一是 NTP 走 UDP 123,用 telnet 或 curl 测 TCP 是测不出问题的;二是证书报 not yet valid 时,先怀疑本机时间,而不是证书链。把巡检脚本跑起来,这类问题就不会再以「疑难杂症」的形式出现了。

Last modification:September 27th, 2026 at 12:24 pm

Leave a Comment