2024 年,我在排查一个分布式系统里的诡异 bug:一个请求,在 A 服务打的日志里是 10:00:05 发出的,到了 B 服务的日志里,收到的时间却是 10:00:00——比发出还早了 5 秒。一个请求,怎么可能"先被收到、后被发出"?我对着两份日志看了一下午,试图用因果关系把它们串起来,结果把自己彻底绕晕了。直到我同时登上这两台机器,各敲了一下 date,才发现真相荒诞得可笑:两台机器的系统时钟,本身就差了 8 秒。我一直默认"服务器的时间总是准的",而正是这个从未被怀疑过的前提,让我对着两份【时间戳本身就在说谎】的日志,做了一下午徒劳的因果推理。这件事逼着我把 Linux 的系统时钟、时区、NTP、chrony 这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,分布式系统,A、B 两个服务在不同机器上
事故现象:
- 跨服务追一个请求,B 收到的时间【早于】A 发出的时间
- 日志时间戳自相矛盾,因果关系完全对不上
- 排查一个并发 bug 时,根本无法靠时间戳排序
现场排查:
# 1. 同时在两台机器上敲 date —— 真相一秒暴露
[A 机]$ date
Sat May 18 10:00:13 CST 2024
[B 机]$ date
Sat May 18 10:00:05 CST 2024
# ★ 两台机器的系统时钟,实打实差了 8 秒!
# 2. 看 B 机的时间同步状态
[B 机]$ timedatectl
Local time: Sat 2024-05-18 10:00:05 CST
Universal time: Sat 2024-05-18 02:00:05 UTC
RTC time: Sat 2024-05-18 02:00:05
Time zone: Asia/Shanghai (CST, +0800)
System clock synchronized: no # ★ 没在同步!
NTP service: inactive # ★ NTP 服务没跑!
# 3. 看 chronyd 进程
[B 机]$ systemctl status chronyd
Active: inactive (dead) # ★ chronyd 根本没启动
# 4. 看它最后一次跟时间源对表是什么时候
[B 机]$ chronyc tracking
506 Cannot talk to daemon # 服务没跑,自然问不到
根因(后来想清楚的):
1. B 机的 chronyd 时间同步服务,不知何时挂了 / 没开机自启。
2. ★ 计算机的石英晶振【不是绝对精确】的,每天都会有
秒级的漂移。没有 NTP 持续校准,时钟会一点点偏离。
3. B 机失去同步后,日积月累,慢慢漂掉了 8 秒。
4. ★ 关键认知:分布式系统里,日志时间戳能用来跨机
排因果,有一个【隐含前提】——所有机器时钟一致。
这个前提一旦破了,时间戳就成了误导你的假信息。
5. 我那一下午的因果推理,是建立在一个错误前提上的。
时钟会漂,NTP 不是装好就一劳永逸,它需要一直在跑。
修复 1:先认清——系统时钟、硬件时钟、时区
# === ★ 一台机器上,其实有【两个】时钟 ===
# 1. 系统时钟(System Clock):内核维护的,在内存里走,
# 你 date 看到的、程序拿到的,都是它。开机后由它计时。
# 2. 硬件时钟(RTC / Hardware Clock):主板上一块带纽扣电池
# 的芯片,关机断电也在走。开机时,系统时钟从它那里取初值。
# === 看系统时钟 ===
$ date
Sat May 18 10:00:13 CST 2024
$ date '+%Y-%m-%d %H:%M:%S' # 自定义格式
$ date +%s # ★ Unix 时间戳(秒),跨时区的硬通货
# === 看硬件时钟 ===
$ hwclock # 或 hwclock --show
2024-05-18 10:00:13.123456+08:00
# === ★ timedatectl:看时间的全貌,一条命令全有 ===
$ timedatectl
Local time: Sat 2024-05-18 10:00:13 CST
Universal time: Sat 2024-05-18 02:00:13 UTC
RTC time: Sat 2024-05-18 02:00:13
Time zone: Asia/Shanghai (CST, +0800)
System clock synchronized: yes # ★ 系统时钟有没有在同步
NTP service: active # ★ NTP 服务活着没
# === ★ 时区:时间戳一样,显示出来却不同 ===
# UTC 是全球统一的基准时间。本地时间 = UTC + 时区偏移。
# 中国是 UTC+8(CST / Asia/Shanghai)。
$ timedatectl set-timezone Asia/Shanghai # 设时区
$ ls -l /etc/localtime # 时区由这个软链接指向 /usr/share/zoneinfo/ 决定
# ★ 一个高频坑:服务器时区是 UTC,程序又按本地时间打日志,
# 日志时间比北京时间慢 8 小时 —— 排查时一脸懵。
# === 手动改时间(★ 一般不该手动改,该交给 NTP)===
$ timedatectl set-time '2024-05-18 10:00:00'
$ hwclock --systohc # 把系统时钟【写回】硬件时钟
$ hwclock --hctosys # 反过来:用硬件时钟校准系统时钟
修复 2:NTP 是什么——为什么时钟必须持续校准
# === ★ 为什么时钟会"漂"——这是这次事故的物理根源 ===
# 计算机靠石英晶振计时,晶振频率受温度、老化影响,
# 不可能绝对精确。一台机器每天漂个几秒,完全正常。
# ★ 所以:时钟【必须】有一个外部基准来持续校准 —— 这就是 NTP。
# === NTP(Network Time Protocol):网络时间协议 ===
# 它让机器周期性地向【时间服务器】对表,把本地时钟
# 拉回到准确值。NTP 还会测量网络往返延迟并补偿掉。
# === ★ NTP 的"层级"概念:stratum ===
# stratum 0:原子钟、GPS 这类基准时间源(不直接联网)
# stratum 1:直连 stratum 0 的服务器(顶级 NTP 服务器)
# stratum 2:向 stratum 1 对表的服务器…… 逐级递增
# 你的机器同步后,通常是 stratum 3 / 4。数字越小越权威。
# === ★ NTP 校准时钟的两种方式:step 与 slew ===
# slew(缓步调整):偏差小时,把时钟走快/走慢一点点,
# 慢慢蹭回正确值 —— 时间是【连续】的,不跳变。
# step(直接跳变):偏差大时,直接把时钟"啪"地跳到正确值。
# ★ 为什么这个区别要命:很多程序受不了时间【倒退】。
# 定时器、超时计算、发号器,若时间突然跳回过去,
# 可能算出负的时间差、可能重复发号 —— 所以日常优先 slew。
# === 看你的机器在跟谁对表、用的什么时区文件 ===
$ cat /etc/chrony.conf | grep -E '^(server|pool)'
# server / pool 行就是配置的时间源。
# 国内常用:ntp.aliyun.com、cn.pool.ntp.org
# === 时间同步服务的两代:ntpd(老)与 chronyd(新)===
# CentOS 7+ / RHEL 7+ 默认用 chronyd,它比老 ntpd:
# - 在网络不稳、笔记本休眠这类场景下表现好得多
# - 同步速度更快、收敛更快
# ★ 现在排查时间同步,主角就是 chrony。
修复 3:chrony 实战——这次的修复主角
# === 第一步:确认 chronyd 服务在跑(这次的病灶)===
$ systemctl status chronyd
$ systemctl start chronyd # 启动它
$ systemctl enable chronyd # ★ 设开机自启 —— 这次就是没设它
# ★ 教训:start 只管这一次,enable 才保证重启后还在。
# 这次 B 机大概率是某次重启后,chronyd 没自启,从此失联。
# === ★ chronyc tracking:看同步状态,最核心的命令 ===
$ chronyc tracking
Reference ID : 0A000002 (ntp.aliyun.com)
Stratum : 3 # 当前层级
Ref time (UTC) : Sat May 18 02:05:00 2024
System time : 0.000123 seconds slow of NTP time # ★ 和标准时间差多少
Last offset : -0.000045 seconds
RMS offset : 0.000089 seconds
Frequency : 12.345 ppm slow # 本机晶振漂移率
Skew : 0.234 ppm
Root delay : 0.012345 seconds
Leap status : Normal # ★ Normal 才是健康的
# ★ 重点看:System time 偏差是不是足够小、Leap status 是不是 Normal。
# === ★ chronyc sources:看每个时间源的健康状况 ===
$ chronyc sources -v
210 Number of sources = 4
MS Name/IP address Stratum Poll Reach LastRx Last sample
===============================================================
^* ntp.aliyun.com 2 6 377 23 +12us[...]
^- time.cloudflare.com 3 6 377 25 -1.2ms[...]
^+ cn.pool.ntp.org 2 6 377 21 +89us[...]
^? 10.0.0.99 0 6 0 - +0ns[...]
# ★ 第一列的符号最关键:
# ^* 当前正在用的、被选中的时间源(最重要,得有一个 *)
# ^+ 健康的、可作备选的源
# ^- 被算法排除的源
# ^? 联系不上的源(Reach 是 0 就是连不通)
# ★ Reach 是 8 进制,377 = 最近 8 次全部成功;0 = 全失败。
# === 强制立刻同步一次(别干等它慢慢 slew)===
$ chronyc makestep
# ★ makestep 让 chrony 立刻用 step 方式跳到正确时间 ——
# 修复像这次这种"已经漂了 8 秒"的大偏差,用它最快。
# === 看 chrony 累计帮你校正了多少 ===
$ chronyc sourcestats # 各源的频率/偏移统计
$ chronyc activity # 在线/离线的源各有几个
# === 配置时间源:编辑 /etc/chrony.conf ===
$ vim /etc/chrony.conf
# server ntp.aliyun.com iburst # iburst:启动时快速连发,加速首次同步
# server cn.pool.ntp.org iburst
# makestep 1.0 3 # 前 3 次校准,偏差超 1 秒就直接 step
$ systemctl restart chronyd # 改完配置要重启生效
修复 4:时区的坑——日志时间为什么"不对"
# === ★ 时区错,会让时间"看起来不对",但它和时钟漂移是两回事 ===
# 时钟漂移:时间戳本身错了(这次的事故)。
# 时区错误:时间戳是对的,只是【显示/换算】成了别的时区。
# === 看当前时区 ===
$ timedatectl | grep 'Time zone'
Time zone: Asia/Shanghai (CST, +0800)
$ date +%Z # 只看时区缩写
# === 改时区 ===
$ timedatectl list-timezones | grep -i shang # 查时区名
$ timedatectl set-timezone Asia/Shanghai
# === ★ 程序看到的时区,可能和系统时区不一样 ===
# 进程会读环境变量 TZ;TZ 没设,才用系统默认 /etc/localtime。
$ echo $TZ
$ TZ='America/New_York' date # 临时用别的时区跑一条命令
# ★ 一个高频坑:容器 / 定时任务 / 某个服务自己设了 TZ,
# 或者根本没挂载时区文件 —— 它打的日志时区,和你 ssh 上去
# date 看到的不一样。排查日志时间,先确认它用的哪个时区。
# === ★ 容器里的时间:与宿主机的关系 ===
# 容器【共享宿主机的内核时钟】—— 所以容器里的 UTC 时间
# 和宿主机一致,你【不需要】也【不应该】在容器里跑 NTP。
# 但容器有自己的【时区】:很多基础镜像默认 UTC。
# 解法:启动容器时挂载宿主机时区,或在镜像里设好 TZ:
$ docker run -e TZ=Asia/Shanghai ...
$ docker run -v /etc/localtime:/etc/localtime:ro ...
# === 统一用 UTC 记日志,是个值得考虑的工程实践 ===
# 服务器/程序统一按 UTC 打日志,展示时再转本地时区 ——
# 能彻底躲开"跨时区机器日志对不上"这类问题。
$ date -u # 看 UTC 时间
修复 5:时间跳变带来的次生灾害
# === ★ 时间被"猛地"校准,会引发一连串次生问题 ===
# 这次我用 chronyc makestep 把 B 机时间往前跳了 8 秒,
# 跳之前必须想清楚:谁会被这一跳波及。
# === 受害者 1:依赖时间差计算超时的逻辑 ===
# 很多代码这样算超时:结束时刻 - 开始时刻 > 阈值。
# 若中途时间【倒退】,这个差可能算成负数 ——
# 超时判断、限流、熔断的计时全乱套。
# ★ 这正是程序该用【单调时钟】的原因:
# 单调时钟(CLOCK_MONOTONIC)只增不减、不受 NTP 影响,
# 专门用来测"时间间隔";而 CLOCK_REALTIME(墙上时钟)
# 才会被 NTP 调整。测耗时务必用单调时钟。
# === 受害者 2:定时任务 cron ===
# 时间往前跳,可能【跳过】某个本该触发的时刻;
# 往后跳,可能让某任务在一小时内被【触发两次】。
$ systemctl status crond
# ★ 大幅校时后,核对一下关键定时任务有没有错触发/漏触发。
# === 受害者 3:基于时间的令牌 / 证书 ===
# TLS 证书校验看"当前时间在有效期内吗"。
# 机器时间若错得离谱,会报:
# certificate is not yet valid (机器时间比签发还早)
# certificate has expired (机器时间冲到了过期之后)
$ openssl s_client -connect host:443 2>/dev/null | openssl x509 -noout -dates
# ★ "证书明明没过期却报错",先怀疑【本机时间不对】。
# JWT、TOTP 动态口令同理 —— 它们都强依赖准确的时间。
# === 受害者 4:数据库主从、分布式锁 ===
# 主从机器时间不一致,会让"最后写入时间"这类判断失真;
# 有些分布式锁靠时间戳判过期,时钟不一致直接埋雷。
# === ★ 结论:大幅校时,优先选业务低峰期 ===
# 平时让 chrony 用 slew 缓步微调,别让它积累出大偏差;
# 真要 makestep 大跳,挑没什么流量的时候,跳完巡检一遍。
修复 6:时间同步排查纪律
# === 这次事故暴露的时间认知盲区,定几条纪律 ===
# === 1. ★ 跨机排查前,先确认所有机器时间一致 ===
$ date # 在每台相关机器上都敲一遍
# 时间戳能用来跨机排因果,前提是【时钟一致】。
# 这个前提,排查前必须先验证,而不是默认它成立。
# === 2. ★ 时钟会漂,NTP 必须【一直在跑】 ===
$ timedatectl # 看 synchronized 和 NTP service
$ systemctl is-enabled chronyd # ★ 必须是 enabled,不只是 active
# 这次的根因就是 chronyd 没开机自启,重启后失联。
# === 3. chronyc tracking / sources 是体检的两件套 ===
$ chronyc tracking # 整体偏差大不大、Leap 正不正常
$ chronyc sources -v # 有没有一个 ^* 选中的健康源
# 没有 ^* 源,等于没在真正同步。
# === 4. 时区错和时钟漂是两码事,别混 ===
# 时间差【整小时】(如差 8 小时)-> 多半是时区问题
# 时间差【几秒/几分】且无规律 -> 多半是时钟漂移/没同步
# === 5. 程序测耗时用单调时钟,别用墙上时钟 ===
# 墙上时钟(CLOCK_REALTIME)会被 NTP 往回调,
# 测时间间隔必须用 CLOCK_MONOTONIC。
# === 6. 大幅校时挑低峰,校完巡检 cron / 证书 / 超时逻辑 ===
$ chronyc makestep # 大偏差时手动跳一次
# 跳完核对定时任务、证书校验、连接超时有没有被波及。
# === 7. 排查时间问题的命令链 ===
$ date; date -u # ① 本地时间、UTC 时间
$ timedatectl # ② 同步状态、时区、NTP 服务
$ systemctl status chronyd # ③ 同步服务活着没、自启没
$ chronyc tracking # ④ 和标准时间偏差多少
$ chronyc sources -v # ⑤ 时间源健康吗、有没有 ^*
# 按这个顺序,时间问题基本能定位。
命令速查
需求 命令
=============================================================
看系统时间 / UTC 时间 date / date -u
看时间全貌(含同步状态) timedatectl
看硬件时钟 hwclock
设时区 timedatectl set-timezone Asia/Shanghai
看同步服务状态 systemctl status chronyd
设同步服务开机自启 systemctl enable chronyd
看与标准时间的偏差 chronyc tracking
看时间源健康状况 chronyc sources -v
立即强制校准时间 chronyc makestep
看 Unix 时间戳 date +%s
口诀:跨机排查先 date 对表 -> 时钟一致才有因果可言
chronyd 必须 enable -> tracking/sources 是体检两件套
避坑清单
- 计算机晶振会漂移,时钟必须靠 NTP 持续校准,不是装好就一劳永逸
- chronyd 要 enable 设开机自启,只 start 不 enable,重启后就失联
- 跨机排查前先在每台机器敲 date,时钟一致才能用时间戳排因果
- timedatectl 看 System clock synchronized 和 NTP service 两个字段
- chronyc sources 里必须有一个 ^* 选中的健康源,否则等于没同步
- 时间差整小时多半是时区问题,差几秒几分多半是时钟漂移
- 容器共享宿主机内核时钟,不要在容器里跑 NTP,但要单独设时区
- 程序测耗时要用单调时钟 CLOCK_MONOTONIC,墙上时钟会被 NTP 回调
- 证书"没过期却报 not yet valid",先怀疑本机时间不对
- 大幅 makestep 校时挑业务低峰,校完巡检 cron、证书、超时逻辑
总结
这次"两台机器日志时间对不上"的事故,纠正了我一个埋得极深、深到我从来没意识到它是个"假设"的认知:我一直默认,服务器上的时间,是【准的】、是【可信的】、是一个像物理常数一样不必怀疑的基准。正因为这个假设太底层、太理所当然,当我面对那两份矛盾的日志——一个请求在 B 服务的日志里,收到时间居然早于它在 A 服务的发出时间——我的第一反应,不是怀疑时间本身,而是怀疑我对业务逻辑的理解。我花了整整一个下午,试图用各种刁钻的并发场景、各种异步回调的可能性,去解释这个"先收到、后发出"的悖论;我把代码翻来覆去地读,就是没想过,问题根本不在代码里,而在于我用来做推理的那把"尺子"——时间戳——本身就是歪的。复盘到根上,我才真正理解了一件以前只是"知道"、却从未"信服"的事实:计算机的时钟,是会漂移的。一台机器靠主板上的石英晶振来计时,而晶振的振荡频率,会受到温度、电压、元件老化的影响,不可能绝对精确。这意味着,任何一台机器的时钟,只要放任不管,就一定会以每天几秒的速度,缓慢地、不可逆地偏离真实时间。正因为如此,服务器上才【必须】运行一个时间同步服务——NTP——让机器周期性地向权威的时间服务器"对表",把漂掉的时钟一次次拉回正轨。我那台 B 机的真正病灶,简单得令人哭笑不得:它的时间同步服务 chronyd,在某一次系统重启之后,就再也没有启动起来——因为它当初被 start 了,却从来没有被 enable 设成开机自启。从那一刻起,B 机就成了一座时间上的孤岛,失去了一切外部校准,它的晶振带着它,日复一日地、悄无声息地漂掉了 8 秒。而这 8 秒,在分布式系统里,足以摧毁一切基于时间戳的因果推理。这次事故让我刻进脑子里的第一条铁律是:在动用日志时间戳去跨机器追踪一个请求、还原一串因果之前,我必须先做一件事——登上每一台相关的机器,各敲一下 date,亲眼确认它们的时钟是一致的。用时间戳排因果,有一个绝对不能省略的隐含前提,就是"所有机器的时间是同步的";这个前提,是需要被【验证】的,而不是被【默认】的。第二条收获,是我顺势把整个时间体系彻底捋清了:我分清了系统时钟和硬件时钟是两个不同的东西;我搞懂了 NTP 校准时钟有 slew(缓步蹭回去)和 step(直接跳过去)两种方式,而后者对那些受不了时间倒流的程序——定时器、超时计算、发号器——是潜在的灾难;我也终于明白,程序里测量一段代码耗时,绝不能用会被 NTP 回拨的"墙上时钟",而要用只增不减的"单调时钟"。我还把时区问题和时钟漂移这两件常被混为一谈的事彻底分开了:时区错,错的是时间的"显示",时间戳本身是对的,症状通常是差一个整小时;而时钟漂移,错的是时间戳"本身",症状是差几秒几分且毫无规律。这次从一个看似不可能的"先收到后发出"的悖论出发,我最大的收获,是亲手拆掉了脑子里那个"服务器时间天然可信"的隐形地基,换上了一个清醒得多的认识:时间,在一台没有被 NTP 持续守护的机器上,是一个会缓慢撒谎的变量;而当你用一把本身就在变形的尺子去丈量世界时,你量出的每一个结论,都是错的。
—— 别看了 · 2026