2024 年,我排查一个特别莫名其妙的线上问题。我们有两个服务,A 服务调用 B 服务,A 每次请求都会带一个签了时间戳的令牌过去,B 收到后会校验:这个令牌的时间戳,得在"当前时间"前后几分钟的窗口内,太旧或太新都判为非法。这套机制跑了大半年一直好好的,可那几天,B 突然开始零星地拒绝 A 的请求,报错是"令牌时间戳超出允许范围"。我第一反应是 A 的代码出了 bug,把令牌签错了。我让 A 打印出它签令牌时用的时间戳——分毫不差,就是当下的时间。我又去看 B,B 的校验逻辑也一个字没改过。一个签的是"现在",一个校验的也是拿"现在"去比,两边都说自己用的是"现在",可就是对不上,而且对不上的那个差值,稳定在 5 分钟左右。我盯着这个"两边都没错、结果却不对"的现象想了很久,最后才动手做了一件我一开始完全没想到要做的事:我分别 SSH 上 A 和 B 两台机器,各敲了一下 date。屏幕上的两个时间,差了整整 5 分钟。原来 A 口中的"现在"和 B 口中的"现在",根本就不是同一个时刻——这两台机器,对"现在几点了"这件事,各执一词。这件事逼着我把服务器时间、时区、UTC、时钟漂移、NTP 同步这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,A、B 两个服务分别部署在两台云服务器
事故现象:
- A 调 B,B 零星报"令牌时间戳超出允许范围"
- ★ A 签令牌用的是"当前时间",B 校验也用"当前时间"
- 两边代码都没改过,差值稳定在 5 分钟左右
现场排查:
# 1. ★ 两台机器各敲一下 date —— 时间居然对不上
#(A 服务器)
$ date
Thu Apr 11 14:32:07 CST 2024
#(B 服务器,几乎同时敲的)
$ date
Thu Apr 11 14:27:09 CST 2024
# ^^^^^^^^ ★ 比 A 慢了 5 分钟!
# 2. ★ 看两台机器有没有在做时间同步
#(B 服务器)
$ systemctl status chronyd
Active: inactive (dead) # ★ chronyd 根本没起
$ chronyc tracking
506 Cannot talk to daemon # ★ 时间同步服务没在跑
# 3. ★ 对比 UTC 时间,确认不是时区问题
#(A)
$ date -u
Thu Apr 11 06:32:07 UTC 2024
#(B)
$ date -u
Thu Apr 11 06:27:09 UTC 2024
# ★ 连 UTC 都差 5 分钟 —— 是【时刻本身】不准,
# 不是时区显示的问题
# 4. ★ 看 B 这台机器的时间设置全貌
$ timedatectl
Local time: Thu 2024-04-11 14:27:11 CST
Universal time: Thu 2024-04-11 06:27:11 UTC
Time zone: Asia/Shanghai (CST, +0800)
System clock synchronized: no # ★ 没在同步
NTP service: inactive # ★ NTP 服务没开
根因(后来想清楚的):
1. ★ 服务器的系统时间,【不是绝对准的】。它靠机器
里的晶振计时,而晶振有微小误差 —— 每天会悄悄
快几秒或慢几秒。这叫【时钟漂移】。
2. 漂移每天就一点点,但【日积月累】。B 这台机器
装好之后,就【从没配过时间同步】,跑了大半年,
一点点偏,最后偏出了 5 分钟。
3. ★ A 和 B 各自漂移,方向和幅度还不一样,于是
两台机器对"现在几点",产生了 5 分钟的分歧。
4. A 签令牌盖的是【A 的现在】,B 拿【B 的现在】去
校验那个时间戳 —— 两个"现在"差 5 分钟,令牌
自然就被判成"时间不对"。
5. ★ 根治办法:让所有服务器都通过 NTP,持续和
权威时间源同步,把各自的漂移随时拉回来。
两边代码都没错,错的是两台机器对"现在"的认知不一致。
修复 1:"时间不对",先分清是哪一种不对
# === ★ "服务器时间不对"其实是好几种不同的毛病 ===
# === 三种"时间不对",方向完全不同 ===
# 1. ★ 时区不对:
# 时刻(UTC)是准的,但显示出来的"几点"错了。
# 比如机器实际在用 UTC 时区,你以为是北京时间,
# 看到的时间就比预期少 8 小时。
# -> 这是【显示】问题,改时区即可(修复 3)。
#
# 2. ★ 时刻不对(本文这次):
# 连 UTC 时间本身都错了 —— 机器对"此刻是宇宙的
# 哪一瞬间"的认知就是偏的。
# -> 这是【真不准】,要做时间同步(修复 4)。
#
# 3. ★ 时间会"漂":
# 现在看着准,但它在慢慢偏。今天准,一个月后
# 又偏了。-> 要的是【持续同步】,不是校一次。
# === ★ 一招分清"时区错"还是"时刻错" ===
# 拿这台机器的 UTC 时间,和一个你确信准的时间比:
$ date -u
Thu Apr 11 06:27:09 UTC 2024
# 同时,看手机/另一台可信机器的 UTC 时间。
# ★ 判断:
# - UTC 是对的、只是本地显示的钟点不对 -> 时区问题。
# - ★ 连 UTC 都不对 -> 时刻问题(真不准),本文重点。
# 我这次:两台机器 date -u 差 5 分钟 —— 是时刻不对。
# === ★ 为什么"用 UTC 比"很重要 ===
# 直接比 date(本地时间),你分不清差异是"时区不同"
# 还是"时刻不准"造成的 —— 两台机器一个东八区一个
# UTC,date 差 8 小时,但它俩时刻可能是完全一致的。
# ★ UTC 是全球统一的"绝对时刻",去掉了时区这层
# 干扰。比时间准不准,永远比 UTC。
# === 认知 ===
# ★ "时间不对"是个笼统说法。动手修之前,先用
# date -u 把它定性:是时区(显示问题),还是
# 时刻(真不准)。两者的修法完全不同。
修复 2:看懂时间——date、UTC、时区、硬件时钟
# === ★ 一台 Linux 上,其实有"好几个时间" ===
# === timedatectl:一条命令看时间全貌 ===
$ timedatectl
Local time: Thu 2024-04-11 14:27:11 CST
Universal time: Thu 2024-04-11 06:27:11 UTC
RTC time: Thu 2024-04-11 06:27:10
Time zone: Asia/Shanghai (CST, +0800)
System clock synchronized: no
NTP service: inactive
# ★ 这里面藏着【四个】概念,逐个说清:
# === ★ 概念 1:UTC —— 全球统一的绝对时刻 ===
# 协调世界时。它不带任何时区,是全人类共用的
# "此刻"。所有时区的时间,都是 UTC 加减一个偏移。
$ date -u # 看本机的 UTC 时间
# === ★ 概念 2:时区 —— UTC 到本地的"换算规则" ===
# 时区决定了"在 UTC 基础上加几小时"。东八区(北京)
# = UTC + 8。同一个 UTC 时刻,在不同时区显示为
# 不同的钟点,但它们是【同一瞬间】。
$ cat /etc/timezone 2>/dev/null # 有的发行版有这文件
$ ls -l /etc/localtime # 它是个软链,指向时区文件
lrwxrwxrwx /etc/localtime -> /usr/share/zoneinfo/Asia/Shanghai
# === ★ 概念 3:Local time —— UTC 套上时区后的本地时间 ===
# 就是 date 默认显示的那个。CST 表示当前时区缩写。
$ date
Thu Apr 11 14:27:11 CST 2024
# === ★ 概念 4:RTC / 硬件时钟 —— 主板上那个独立的表 ===
# RTC(Real Time Clock)是主板上一块【靠纽扣电池
# 供电、独立走时】的硬件钟。机器关机时,系统时间
# 没了,靠的就是它在默默计时;开机时,系统从 RTC
# 读一个初始时间。
$ hwclock --show # 看硬件时钟
# ★ 系统时间(软件,内核维护)和 RTC(硬件)是
# 两个钟,可能不一致。开机后系统时间走自己的。
# === ★ 这次要盯的是哪个 ===
# 本文这次是【时刻不准】—— 要盯的是 UTC / Local time
# 这个"系统时间",以及它和真实世界差多少。
# RTC 一般不用手动管,同步好系统时间后让它自动
# 写回 RTC 即可。
# === 认知 ===
# ★ 先有 UTC(绝对时刻),套上时区规则,得到 Local
# time(你看到的钟点);另有个 RTC 硬件钟管关机
# 期间的计时。理清这四个,才不会把问题归错地方。
修复 3:时区不对——这是显示问题,改时区就行
# === ★ 如果定性是"时区错"(UTC 准、钟点偏)===
# === 第一步:看当前时区 ===
$ timedatectl | grep "Time zone"
Time zone: UTC (UTC, +0000)
# ★ 比如这里是 UTC —— 而你期望是北京时间,那看到的
# 钟点就会比实际少 8 小时。这是典型的时区没设对。
# === ★ 第二步:列出可选时区,设成对的 ===
$ timedatectl list-timezones | grep -i shanghai
Asia/Shanghai
# 设置时区(中国大陆统一用 Asia/Shanghai):
$ timedatectl set-timezone Asia/Shanghai
# ★ 立即生效,date 看一下:
$ date
Thu Apr 11 14:27:11 CST 2024 # ★ 钟点对了
# === ★ 它背后做了什么 ===
# set-timezone 其实就是把 /etc/localtime 这个软链,
# 重新指向对应的时区文件:
$ ls -l /etc/localtime
/etc/localtime -> /usr/share/zoneinfo/Asia/Shanghai
# ★ 老办法是手动 ln -sf 改这个软链 —— 现在用
# timedatectl 更稳妥,别手动改了。
# === ★ 一个关键澄清:改时区,不改变 UTC 时刻 ===
# 改时区,只是换了"UTC -> 本地"的换算规则。机器
# 认知的那个【绝对时刻(UTC)】一秒都没动。
# ★ 所以:如果你的问题是"时刻不准"(UTC 都错了),
# 改时区【一点用都没有】—— 别走错路。改时区只
# 解决"显示的钟点不对"。
# === 容器里的时区坑 ===
# Docker 容器默认常常是 UTC。容器里日志时间比宿主
# 少 8 小时,多半是这个。解决:
# - 启动时挂载:-v /etc/localtime:/etc/localtime:ro
# - 或镜像里设 TZ 环境变量 / 装 tzdata。
# === 认知 ===
# ★ 时区问题是"翻译问题",时刻是准的,只是翻译成
# 钟点时用错了规则。改时区即可,且不影响时刻。
修复 4:时刻不准——为什么要 NTP,以及怎么配
# === ★ 时刻真不准:根子是"时钟漂移",解药是 NTP ===
# === ★ 为什么服务器时间会"自己变不准" ===
# 服务器靠主板上的晶体振荡器(晶振)计时。晶振并不
# 完美 —— 它的振荡频率有微小误差,还受温度影响。
# ★ 结果:机器的时钟会【漂移】—— 每天悄悄快几秒
# 或慢几秒。一天没感觉,一个月、半年累积下来,
# 就能偏出好几分钟。我这台 B 偏 5 分钟,就是
# 半年没同步、一点点漂出来的。
# === ★ 解药:NTP —— 持续和权威时间源对表 ===
# NTP(网络时间协议):机器定期通过网络,去问一个
# 权威时间服务器"现在到底几点",然后【平滑地】
# 把自己的时钟校准过去。
# ★ 关键词是"持续"和"平滑":它不是校一次就完,
# 而是一直在校,随时把漂移拉回来。
# === ★ CentOS 7+ 用 chrony(比老的 ntpd 更好)===
# 装并启动 chrony:
$ yum install -y chrony
$ systemctl enable --now chronyd
$ systemctl status chronyd
Active: active (running) # ★ 跑起来了
# === ★ 看它在跟哪些时间源对表 ===
$ chronyc sources -v
210 Number of sources = 4
MS Name/IP address Stratum ... LastRx
^* ntp1.aliyun.com 2 ... 23
^- ntp.ntsc.ac.cn 1 ... 45
# ★ 名字前的 ^* = 当前正在用的、被选中的时间源。
# === ★ 看同步状态(最关键的一条命令)===
$ chronyc tracking
Reference ID : ntp1.aliyun.com
Stratum : 3
System time : 0.000234 seconds slow of NTP time
# ^^^^^^^^ ★ 和标准时间只差 0.2 毫秒 —— 同步好了
Last offset : +0.000012 seconds
# ★ System time 那一行,告诉你本机和标准时间差多少。
# 越接近 0 越好。
# === ★ 配置时间源(可选,改 /etc/chrony.conf)===
# 国内服务器,建议用国内的 NTP 源,更快更稳:
$ cat /etc/chrony.conf
server ntp1.aliyun.com iburst # 阿里云 NTP
server ntp.ntsc.ac.cn iburst # 国家授时中心
# iburst:启动时快速连发几个包,加快首次同步。
$ systemctl restart chronyd # 改完重启生效
# === ★ 救急:想立刻把时间拉正(别等 chrony 慢慢校)===
# chrony 默认是"平滑校准",偏差大时校得慢。要立刻
# 强制对齐(适合偏差很大、又能容忍时间跳变时):
$ chronyc makestep
# ★ makestep 会让系统时间【直接跳】到正确值。
# ⚠ 注意:时间"跳变"对某些程序不友好(见修复 5),
# 生产上能平滑就平滑,救急才用 makestep。
# === 认知 ===
# ★ 服务器时间天生会漂,这不是故障是物理规律。所以
# 每台服务器都【必须】配 NTP 持续同步 —— 这是
# 装好系统就该做、却最容易被漏掉的一步。
修复 5:正确解法——让全集群时间一致且不跳变
# === ★ 解法:每台都同步,而且要同步得"温柔" ===
# === ★ 解法 1:每台服务器都装 chrony 并开机自启 ===
$ yum install -y chrony
$ systemctl enable --now chronyd
$ timedatectl set-ntp true # 确认 NTP 同步开关是开的
# ★ 重点是 enable —— 保证【重启后】 chronyd 还会自己
# 起来。只 start 不 enable,重启一次就又不同步了。
# === ★ 解法 2:集群里所有机器,用【同一组】时间源 ===
# 分布式系统里,A、B、C... 所有节点,chrony.conf 里
# 配【相同的 NTP 源】。这样它们都向同一个权威看齐,
# 彼此之间的时间自然就一致了。
# ★ 大集群可以自建一两台内网 NTP 服务器,其余机器
# 都同步它 —— 既快,又保证全集群口径统一。
# === ★ 解法 3:警惕"时间跳变"对程序的伤害 ===
# 校时有两种方式,后果不同:
# - slew(平滑):把时钟走快/走慢一点点,慢慢蹭到
# 正确值。★ 时间始终单调递增,程序无感。chrony
# 日常就是这么干的。
# - step(跳变):时间【直接跳】过去。如果是往回跳,
# ★ 会出现"时间倒流" —— 有些程序(算耗时、
# 依赖时间单调的逻辑)会算出负数、会错乱。
# ★ 原则:日常交给 chrony 平滑校;只在偏差过大、
# 且评估过影响后,才用 makestep 跳一次。
# === ★ 解法 4:容器与虚拟机的时间 ===
# - 容器:容器【不自己管时间】,它共享宿主机的时钟。
# ★ 所以同步好【宿主机】的时间就行,别在容器里
# 单独装 NTP。容器里要管的只是【时区】(挂载
# /etc/localtime)。
# - 虚拟机:VM 的时钟容易因宿主调度而漂得更厉害,
# 更要确保 NTP 同步开着。
# === ★ 解法 5:把时间偏差纳入监控 ===
# 时间不准是【悄无声息】的,不监控根本发现不了,
# 等出事(像我这次)才暴露。监控两件事:
# - chronyd 服务是否 active。
# - chronyc tracking 里的 System time 偏差,
# 超过比如 100ms 就告警。
$ chronyc tracking | grep "System time"
# ★ 让监控定期采这个值。
# === ★ 解法 6:依赖时间的业务,留够容错窗口 ===
# 像我这次的令牌时间戳校验:即便配了 NTP,机器间
# 仍可能有毫秒级的微小差异。★ 业务逻辑里,时间
# 窗口别卡得太死(别用"正负 1 秒"),留够余量
# (比如正负几分钟),容忍合理的时钟误差。
# === 验证 ===
$ timedatectl # System clock synchronized: yes
$ chronyc tracking # System time 偏差接近 0
$ # 在集群每台机器上同时 date -u,应几乎完全一致
# ★ 三项都对,集群的"时间一致性"才算真的建立起来。
修复 6:时间排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ 服务器时间不是绝对准的,晶振有误差会持续漂移 ===
# === 2. ★ "时间不对"先定性:date -u 比 UTC,分清时区错还是时刻错 ===
$ date -u
# === 3. 时区错是显示问题,timedatectl set-timezone 改即可,不影响时刻 ===
# === 4. ★ 时刻错(UTC 都不准)要靠 NTP 持续同步,不是改时区 ===
# === 5. ★ 每台服务器都装 chrony 并 enable 开机自启,只 start 重启就失效 ===
$ systemctl enable --now chronyd
# === 6. ★ 看同步状态用 chronyc tracking,看 System time 偏差 ===
# === 7. 分布式集群所有节点用同一组 NTP 源,保证彼此时间一致 ===
# === 8. ★ 校时优先平滑(slew),时间跳变尤其往回跳会让程序错乱 ===
# === 9. 容器不自己管时间共享宿主机时钟,同步好宿主机即可,容器只管时区 ===
# === 10. 排查"时间相关的诡异问题"的步骤链 ===
$ date ; date -u # ① 看本地时间和 UTC
$ 多机器同时 date -u # ② 机器间 UTC 是否一致
$ timedatectl # ③ 看时区/同步状态
$ systemctl status chronyd # ④ 时间同步服务在跑吗
$ chronyc tracking # ⑤ 偏差多大
# 按这个顺序,"时间不对"基本能定性、能根治。
命令速查
需求 命令
=============================================================
看本地时间 date
看 UTC 时间(判断准不准) date -u
看时间设置全貌 timedatectl
看硬件时钟(RTC) hwclock --show
列出可选时区 timedatectl list-timezones
设置时区 timedatectl set-timezone Asia/Shanghai
开启 NTP 同步开关 timedatectl set-ntp true
安装时间同步服务 yum install -y chrony
启动并设开机自启 systemctl enable --now chronyd
看时间源 chronyc sources -v
看同步状态和偏差 chronyc tracking
强制立刻校正(会跳变) chronyc makestep
看 chrony 配置 cat /etc/chrony.conf
口诀:服务器时间会漂不是绝对准,时间不对先 date -u 比 UTC 分清时区错还是时刻错
时区错改 timezone,时刻错靠 chrony 持续同步,每台都 enable 自启集群用同一组源
避坑清单
- 服务器时间不是绝对准的,它靠晶振计时晶振有误差会持续漂移,每天悄悄快或慢几秒累积成大偏差
- 时间不对先定性,date -u 比 UTC 分清是时区错显示问题,还是时刻错连 UTC 都不准的真不准
- 比时间准不准要比 UTC 不要比本地时间,UTC 是全球统一绝对时刻去掉了时区这层干扰
- 时区错是翻译问题时刻是准的,timedatectl set-timezone 改即可,改时区不改变 UTC 时刻
- 时刻错连 UTC 都不准要靠 NTP 持续同步把漂移拉回来,改时区对时刻不准一点用都没有
- 每台服务器都要装 chrony 并 enable 开机自启,只 start 不 enable 重启一次就又不同步了
- 看时间同步状态用 chronyc tracking,看 System time 那一行的偏差,越接近 0 越好
- 分布式集群所有节点配同一组 NTP 源,都向同一个权威看齐彼此时间才会一致
- 校时优先平滑 slew 让时钟走快走慢蹭过去,时间跳变尤其往回跳会让依赖时间单调的程序错乱
- 容器不自己管时间共享宿主机时钟,同步好宿主机即可别在容器里装 NTP,容器只需管时区
总结
这次"两个服务都说自己用的是'现在'、结果却对不上"的事故,纠正了我一个深得我自己都从未察觉的预设。在我的脑子里,"现在几点"这件事,是这个世界的一个客观事实,是一个唯一的、所有机器都共享的真理。一台服务器上的时间,在我看来,就像它读出来的圆周率一样,是一个不需要怀疑、不会出错的常量。正因为我把"时间"当成了一块绝对可靠的基石,所以当 A 服务签令牌、B 服务校验令牌都说"我用的是当前时间"时,我的推理就被锁死在了一条路上:既然"当前时间"是唯一的、是它俩共享的同一个东西,那么对不上,就一定是它俩当中,有一个在处理这个时间的【逻辑】上写错了。我反复去查 A 的签名代码,反复去查 B 的校验代码,因为在我的认知里,错只可能出在"逻辑"这一层——"时间"本身,是不可能错的,它是我推理时脚下那块我从不曾低头看过的地。复盘到根上,我才明白,我脚下那块地,本身就是斜的。服务器的时间,根本不是什么从天而降的客观真理。它就是机器主板上一小块晶体,通电之后嗡嗡地振荡,系统数着这个振荡的次数,来推算时间。可这世上没有完美的晶振——它的振荡频率有微小的误差,还会随着温度变化而变化。于是每一台服务器,其实都在用一块走得略微不准的表;它每天都会悄悄地快上几秒,或者慢上几秒。这个误差小到一天之内根本无从察觉,可它会日积月累。我那台 B 服务器,从装好系统的那天起,就从来没有人为它配过时间同步,它就这样揣着一块没人对过的表,自顾自地走了大半年——最后,偏出了整整 5 分钟。A 和 B,是两台独立的机器,各自揣着各自那块独立的、以各自的方式在偏的表。它们口中的"现在",从来就不是同一个"现在"。我一直以为它俩在共享同一个时间真理,可真相是,这世上根本不存在一个它俩自动共享的时间——除非有人特意去建立这种共享。NTP 做的,恰恰就是这件被我彻底忽略的事:它让每一台机器,定期地、持续地,去和一个权威的时间源对表,把自己悄悄漂掉的那点误差,一次次地拉回来。没有 NTP,"全集群时间一致"压根不会自动发生;它不是默认状态,它是一项需要主动配置、并且需要持续维持的工程。这次最大的收获,是我意识到,我的排查之所以一头撞进死胡同,不是因为我查得不够仔细,而是因为我把一个本该被审视的东西,供奉成了不可怀疑的前提。我默认"时间是对的、是统一的",于是我所有的怀疑,都只敢在这个前提【之上】的那一层逻辑里打转,而那个真正出问题的地方,恰恰就藏在这个前提【本身】里。一个被你当成"公理"的东西,是不会进入你的排查视野的——你不会去验证一条你认定为公理的东西。所以下一次,当两边的逻辑我都查过、确认都没错,问题却依然存在的时候,我不会再把这归结为"一定是哪里还藏着个 bug 没找到"。我会停下来,做一件更根本的事:把我这次推理所依赖的那些"不言自明的前提",一条一条地、明明白白地列出来,然后逐一问自己——这一条,我【验证过】吗?还是我只是【假设】它成立?——很多解不开的问题,根源不在我们反复检查的那些地方,而在那个我们从一开始就认定"它不可能有问题"、因而从未真正看过它一眼的地方。
—— 别看了 · 2026