2024 年,我维护的一套系统跑在两台服务器上,A 机和 B 机。某天开始,一个本该每天只跑一次的定时对账任务,偶尔会"跑两次";更怪的是,把两台机器的日志按时间排到一起看,事件的先后顺序是乱的——B 机上明明后发生的事,时间戳却比 A 机早。我第一反应是程序有并发 bug,查了半天代码,逻辑没问题。后来我把两台机器的时间摆在一起一看,愣住了:A 机和 B 机的系统时间,差了将近 8 分钟。我心想这还不简单,时区配错了呗,timedatectl 一看,两台都是 Asia/Shanghai,时区一个字没错。我又猜是哪台机器时间被人手动改过,于是 date -s 把 B 机的时间硬掰回来,跟 A 机对齐,心想这下总好了。结果过了几天,B 机的时间又慢了——这次慢了 3 分钟。我盯着这个"改了又慢、慢了再改"的循环想了很久,最后才反应过来:我一直把"时间不对"当成一个"被改错了的配置",所以我才会一遍遍地去"改回正确值"。可时间根本不是配置——它是一个会自己流动、而且会流得不准的东西。这件事逼着我把 Linux 的系统时钟、硬件时钟、时区、NTP 时间同步这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,两台服务器 A 机、B 机,跑同一套分布式系统
事故现象:
- 每天只该跑一次的定时任务,偶尔跑两次
- 两台机器日志混排,事件先后顺序错乱
- ★ 一查:A 机和 B 机系统时间差了将近 8 分钟
- 时区都是 Asia/Shanghai,没配错;手动改对后过几天又慢了
现场排查:
# 1. 对一下两台机器的时间
$ ssh A 'date'; ssh B 'date'
A: Thu May 16 14:23:07 CST 2024
B: Thu May 16 14:15:11 CST 2024 # ★ B 机慢了约 8 分钟
# 2. 时区是不是配错了
$ timedatectl
Time zone: Asia/Shanghai (CST, +0800) # ★ 时区没错
# 两台一看都是 Asia/Shanghai
# 3. ★ 看有没有时间同步服务在跑
$ systemctl status chronyd
Loaded: ... chronyd.service; disabled # ★ 没启用!
Active: inactive (dead) # ★ 根本没跑
$ timedatectl | grep -i ntp
NTP synchronized: no # ★ 时间没在同步
# 4. ★ 看硬件时钟,和系统时钟也对不上
$ hwclock --show
2024-05-16 14:11:50 # ★ 硬件时钟更慢
根因(后来想清楚的):
1. ★ 时间不是"配置",是会自己流动的东西。机器靠
石英晶振计时,晶振有误差,跑久了系统时间必然
会【漂】—— 这台快一点、那台慢一点,天经地义。
2. B 机慢 8 分钟,不是被谁改错了,是它的时钟【漂】了
—— 长时间没有任何东西帮它校准。
3. ★ date -s 手动改时间,是【一次性】的:改完那一刻
对了,之后它继续按自己的速率漂,几天后照样不准。
4. 真正该做的,是装一个【时间同步服务】(chrony),
让它【持续地、平滑地】把系统时间往标准时间校。
5. 这台机器的 chronyd 压根没启用 —— 没有任何东西
在帮它校时,它只能一个人越漂越远。
6. ★ 时区(timezone)和时间(UTC 时刻)是两回事:
时区对,只代表"显示"对;真正错的是底层的 UTC。
时间会漂是常态,不校时才是故障。
修复 1:时间为什么会"自己变慢"——时钟会漂
# === ★ 先纠正最核心的误解:时间不是"配置" ===
# === 我以为的时间 vs 真实的时间 ===
# 我以为:系统时间是一个【设定值】,设对了就一直对,
# 它要是不对,一定是被谁"改错"了。
# ★ 真相:机器没有"绝对准"的时间源。它靠主板上一颗
# 【石英晶振】按固定节拍计数来推算时间。晶振的频率
# 有【固有误差】,还受温度影响 —— 于是机器的时间
# 【一定会漂】:每天慢几秒或快几秒,日积月累,
# 几周就能差出好几分钟。
# === ★ "漂"是常态,不是故障 ===
# 两台一模一样的服务器,放在那儿不管,
# ★ 它们的时间【必然】会慢慢分开 —— 一台偏快、
# 一台偏慢。这不是哪台"坏了",这是物理规律。
# 所以我那个问题问错了:不该问"B 机时间为什么不对",
# 该问"B 机的时间,为什么【没有人帮它校】"。
# === 看这台机器漂得有多快 ===
# 如果装了 chrony,可以直接看它估算的漂移率:
$ chronyc tracking | grep -i 'freq\|skew'
Frequency : 12.3 ppm slow # ★ 慢 12.3 ppm
# ppm = 百万分之一。12.3 ppm 意味着:
# 每 100 万秒,就慢 12.3 秒 —— 约【每天慢 1 秒】。
# ★ 别小看每天 1 秒,几个月不管就是几分钟。
# === 没装 chrony 时,粗略估漂移 ===
# 和一个可信时间源比一下当前差多少:
$ date; curl -sI https://www.baidu.com | grep -i '^date:'
# ★ 本机时间 和 HTTP 响应头里的 Date 一对比,
# 差多少一目了然。
# === ★ 一句话认知 ===
# 时间会漂,是每台机器的天性。
# ★ 一台机器时间准不准,取决于"有没有东西在持续
# 校它",而不取决于"它一开始设得对不对"。
修复 2:date -s 手动改的坑——一次性,还会让时间"跳变"
# === ★ 第二个认知:date -s 改时间,治标且有害 ===
# === 坑 1:date -s 是"一次性"的 ===
$ date -s '2024-05-16 14:23:07'
# ★ 这条命令只做一件事:把系统时间【此刻】掰到这个值。
# 做完,机器就继续按它自己那只【会漂的晶振】走。
# 所以我那个"改了又慢"的循环说得通了:我每次 date -s
# 只是把漂移【清零】了一次,可漂移的【源头】还在,
# 过几天它自然又漂回去。★ 手动改,永远改不完。
# === ★ 坑 2:date -s 会造成时间"跳变",这很危险 ===
# 正常的时间,是【单调、连续】往前走的。
# date -s 会让时间【瞬间跳一下】 —— 往前跳,或者
# ★【往后跳】(把快了的机器调慢,时间就倒退了)。
# 时间一旦【倒退】或【大跳】,很多程序会出问题:
# - 定时任务:可能【漏跑】或【重复跑】(我就中了)
# - 基于时间的缓存/令牌:过期判断瞬间错乱
# - 数据库/日志:时间戳非单调,事件顺序乱套
# - 有些程序甚至会因为"时间倒流"直接崩溃
# === ★ 对比:平滑校时(slew)vs 跳变校时(step)===
# 时间同步服务(chrony)校时,默认用的是【平滑校时】:
# - 不直接跳,而是【悄悄地把时钟走快一点点 / 慢一点点】,
# 让它在一段时间内【平滑地】追上标准时间。
# - ★ 时间始终单调向前,程序完全无感。
# 跳变校时(step):像 date -s 一样直接跳。
# - chrony 只在【一开机、偏差太大】时才用跳变,
# 平时一律用平滑校时。
# === 结论 ===
# ★ date -s 只该用在【应急救火】:偏差大到必须立刻纠正。
# 它不是"校时方案",日常校时必须交给 chrony。
# 真要手动同步一次,用这个比 date -s 温和:
$ chronyc makestep # 让 chrony 立即校一次(装了 chrony 才行)
修复 3:系统时钟 vs 硬件时钟——机器上其实有两个钟
# === ★ 一台机器上,其实有【两个】时钟 ===
# === 两个时钟分别是谁 ===
# 1. 系统时钟(System Clock):
# - 操作系统【运行时】用的时间,date 看到的就是它。
# - 它存在【内存】里,由内核维护,机器一关机就没了。
# 2. ★ 硬件时钟(Hardware Clock / RTC):
# - 主板上一个【独立的小钟】,有纽扣电池供电。
# - 机器【关机、断电】时,靠它默默走着、记着时间。
# === ★ 两个时钟怎么交接 ===
# - 开机时:系统从【硬件时钟 RTC】读一个初始时间,
# 作为系统时钟的起点。
# - 关机时:系统通常会把【系统时钟】写回 RTC。
# - 运行中:两个钟【各走各的】,会慢慢产生偏差。
# === 分别查看这两个时钟 ===
$ date # 系统时钟
Thu May 16 14:23:07 CST 2024
$ hwclock --show # ★ 硬件时钟 RTC
2024-05-16 14:11:50 # ★ 和系统时钟差了 11 分钟
# === ★ 为什么这事重要 ===
# 如果你只用 date -s 改了【系统时钟】,没动 RTC,
# 那么机器一【重启】,系统又从那个【还是错的 RTC】
# 读时间 —— ★ 你白改了,一重启就打回原形。
# === 手动让两个时钟互相对齐 ===
$ hwclock --systohc # 把系统时钟 -> 写进硬件时钟
$ hwclock --hctosys # 把硬件时钟 -> 灌进系统时钟
# === ★ 好消息:用 chrony 就不用操心这个 ===
# chrony 在持续校准【系统时钟】的同时,会【定期】
# 把校准好的时间【自动写回 RTC】。
# ★ 所以只要 chrony 在跑,这"两个钟"的事你基本
# 不用管 —— 它都替你对齐了。
$ timedatectl | grep -i 'rtc'
RTC time: Thu 2024-05-16 14:23:05 # 装了 chrony 后,RTC 也准了
修复 4:时区不是时间——timezone 和 UTC 要分清楚
# === ★ 把"时区"和"时间"彻底分开 ===
# === 它们是两个层次的东西 ===
# 1. UTC 时刻:这是【真正的时间】,全球统一的那个
# 绝对时刻。机器内部、时间戳、文件 mtime,本质上
# 都是用 UTC 在记。
# 2. ★ 时区(timezone):只是一个【显示规则】 ——
# "把那个 UTC 时刻,加 8 小时显示给中国用户看"。
# ★ 时区,改的是【怎么把时间显示出来】,
# 它【根本不改变】底层那个真正的 UTC 时刻。
# === 一个例子说清楚 ===
# 同一个 UTC 时刻 06:23:07:
# - 时区设 Asia/Shanghai -> 显示 14:23:07
# - 时区设 UTC -> 显示 06:23:07
# ★ 显示的数字不同,但它们是【同一个时刻】。
# 改时区,不会让你"穿越";改 UTC,才是真改了时间。
# === ★ 于是要分清两类故障 ===
# 故障 A:时区错了。比如时区被设成了 UTC。
# - 现象:date 显示的时间,比"墙上的钟"差了 8 小时。
# - ★ 本质:时间是【对的】,只是显示规则错了。
# - 修:timedatectl set-timezone Asia/Shanghai
# 故障 B:★ UTC 时刻错了。就是我这次的情况。
# - 现象:差几分钟、十几分钟这种【不整齐】的数。
# - ★ 本质:底层真实时间就是错的 —— 这才是大问题。
# - 修:靠 chrony 同步。
# === ★ 一个快速判断法 ===
# 时间差,如果是【整 8 小时】(或别的整小时)
# -> 八成是【时区】问题。
# 时间差,如果是【几分几秒】这种零碎值
# -> 八成是【UTC 时间漂了】,是真·时间不准。
# 我这次差 8 分钟,零碎值 —— 一开始就该想到不是时区。
# === 查时区 / 改时区 ===
$ timedatectl # 看当前时区
$ timedatectl list-timezones | grep Shanghai
$ timedatectl set-timezone Asia/Shanghai # 设时区(只影响显示)
修复 5:正确解法——装 chrony,让它持续平滑校时
# === ★ 真正的解法:交给时间同步服务 chrony ===
# === 为什么是"服务",而不是"命令" ===
# 时间会【持续地漂】,所以校时也必须是【持续地做】。
# 一条命令只能校一次,漂移立刻又开始。
# ★ 只有一个【常驻后台的服务】,不停地和标准时间源
# 对比、不停地微调,才能让时间【一直】准。
# 这个服务,在 CentOS 7+ 上就是 chrony。
# === 第一步:装上并启用 chrony ===
$ yum install -y chrony
$ systemctl enable --now chronyd # ★ 开机自启 + 立即启动
$ systemctl status chronyd
Active: active (running) # ★ 跑起来了
# === ★ 第二步:配置时间源(NTP 服务器)===
$ vi /etc/chrony.conf
# 把同步源指向可靠的 NTP 服务器(国内用阿里云的):
server ntp1.aliyun.com iburst
server ntp2.aliyun.com iburst
server ntp3.aliyun.com iburst
# - iburst:★ 启动时【连发几个包】快速完成首次同步,
# 不然首次对时要等好几分钟。
$ systemctl restart chronyd
# === ★ 第三步:验证同步状态 ===
$ chronyc sources -v
^* ntp1.aliyun.com 2 6 377 23 +1.2ms ... # ★ ^* = 正在用这个源
# ★ 行首符号:^* 当前同步源 / ^+ 备选可用 / ^? 不可用
$ chronyc tracking
Reference ID : ntp1.aliyun.com
System time : 0.000041 seconds slow ... # ★ 偏差已是微秒级
Stratum : 3
$ timedatectl | grep -i ntp
NTP synchronized: yes # ★ 同步成功
# === ★ 第四步:首次偏差大,让它立刻校一次 ===
# 我这台慢了 8 分钟,偏差太大,chrony 默认平滑校
# 会校很久。让它立即跳一次:
$ chronyc makestep
# ★ makestep:允许 chrony 这次用"跳变"把大偏差一次纠正。
# 之后日常的小漂移,它会自动用平滑校时处理。
# === ★ 内网多台机器:自建一台 NTP server ===
# 大量内网机器都去连公网 NTP,不优雅也不稳。
# 更好的做法:让一台机器做内网 NTP server,
# 其他机器都同步它 —— 这样【所有机器时间一致】,
# 分布式系统最需要的就是"彼此一致",哪怕都偏一点点。
# (在那台 server 的 chrony.conf 里加 allow 网段即可)
# === ★ 关键认知 ===
# 装好 chrony 这一刻,前面修复 2/3/4 的麻烦【全消失】:
# - 不用再手动 date -s(它持续校)
# - 不用管 RTC(它自动写回)
# - 多台机器时间天然一致(都同步同一个源)
修复 6:时间同步排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ 时间会漂是常态,不校时才是故障 ===
# 别问"时间为什么不对",要问"为什么没东西在校它"。
# === 2. ★ 每台服务器都必须跑时间同步服务 ===
$ systemctl is-enabled chronyd # 必须是 enabled
$ timedatectl | grep -i ntp # NTP synchronized: yes
# === 3. date -s 只用于应急,不是校时方案 ===
# 它一次性、还会造成时间跳变,日常校时交给 chrony。
# === 4. ★ 分清时区问题 和 UTC 时间问题 ===
# 差整小时 -> 多半时区错;差零碎分秒 -> 真·时间漂了。
# === 5. 记住有两个时钟:系统时钟 和 硬件时钟 RTC ===
$ date ; hwclock --show # 两个都看一眼
# === 6. ★ 分布式系统:多机时间【必须一致】 ===
# 让所有机器同步【同一个】时间源,内网最好自建 NTP。
# === 7. 时间敏感的程序,别假设时间单调/绝对准 ===
# 算时间间隔,优先用单调时钟(CLOCK_MONOTONIC),
# 它不受校时跳变影响。
# === 8. 排查"机器时间不对"的步骤链 ===
$ date # ① 看当前时间差多少
$ timedatectl # ② 看时区 + NTP 同步状态
$ systemctl status chronyd # ③ 时间同步服务在跑吗
$ chronyc sources -v # ④ 同步源连上了吗
$ chronyc tracking # ⑤ 看实际偏差和漂移率
$ chronyc makestep # ⑥ 偏差大就立即校一次
# 按这个顺序,时间不准基本能定位、能根治。
命令速查
需求 命令
=============================================================
看系统时间 date
看时区和 NTP 同步状态 timedatectl
看硬件时钟 RTC hwclock --show
看时间同步服务状态 systemctl status chronyd
装并启用 chrony yum install -y chrony; systemctl enable --now chronyd
看 chrony 同步源 chronyc sources -v
看实际偏差和漂移率 chronyc tracking
偏差大时立即校一次 chronyc makestep
改时区(只影响显示) timedatectl set-timezone Asia/Shanghai
系统时钟写回硬件时钟 hwclock --systohc
口诀:时间会漂是常态,不校时才是故障;date -s 只应急别当方案
装 chrony 持续平滑校时,差整小时多半是时区差零碎分秒是真漂了
避坑清单
- 时间不是配置而是会自己流动的东西,机器靠石英晶振计时,有误差就必然会漂
- 一台机器时间准不准,取决于有没有东西在持续校它,而非一开始设得对不对
- date -s 是一次性的,改完时钟继续按原速率漂,几天后照样不准,治标不治本
- date -s 会造成时间跳变甚至倒退,定时任务漏跑重复跑、缓存令牌判断错乱
- chrony 默认用平滑校时,悄悄调快调慢让时间单调向前,程序无感,远比跳变安全
- 机器上有两个时钟,系统时钟在内存关机即失,硬件时钟 RTC 有电池关机仍走
- 只改系统时钟没动 RTC,一重启又从错的 RTC 读时间,白改;chrony 会自动写回 RTC
- 时区只是显示规则不改变底层 UTC 时刻,差整小时多半是时区,差零碎分秒是真漂了
- 每台服务器都必须装并启用 chronyd,配可靠 NTP 源加 iburst 加速首次同步
- 分布式系统多机时间必须一致,让所有机器同步同一个源,内网最好自建 NTP server
总结
这次"两台机器时间差了 8 分钟、定时任务重复跑、日志顺序错乱"的事故,纠正了我一个埋得很深、自己却从没察觉的认知错误:我一直把"时间",当成了一种"配置"。在我的潜意识里,系统时间和一个 IP 地址、一个端口号、一个路径没什么两样——它是一个我设定下去的、静态的值。我设对了,它就该一直对;它要是不对了,那一定是某个环节出了错,是它"被改坏了"。正是这个"时间是配置"的心智模型,决定了我整场排查的姿势:我做的所有事情,本质上都是在"寻找那个把它改错的人或事",然后"把它改回正确的值"。我用 date -s 把 B 机的时间硬生生掰回去,就像我修正一个写错的配置项一样,做完还很有成就感。可几天之后,它又慢了。我于是再改一次。我陷在一个"改回正确值——它又偏离——再改回正确值"的循环里,却始终没有停下来问一句:一个静态的配置项,改对了之后,它怎么会自己"偏离"呢?复盘到根上,我才终于看清,时间从来就不是一个"配置",它是一个"过程"。机器里没有一个地方"存着"正确的时间等我去设定;机器只是靠主板上一颗石英晶振,一下一下地数着节拍,用这个节拍去"推算"现在是几点。而这颗晶振,和世界上所有的晶振一样,有它自己的固有误差,还会随温度变化——于是这台机器推算出来的时间,就【一定】会漂。它每天慢一点点,慢得悄无声息,可几个星期累积下来,就是实实在在的 8 分钟。所以 B 机的时间不对,根本不是"被谁改错了",而是它从被装好的那天起,就【从来没有人帮它校准过】——它只是一个人,孤独地、忠实地,按着自己那颗不那么准的晶振,越漂越远。我用 date -s 做的事,不过是每隔几天,把它漂出去的距离粗暴地清零一次,而那个让它持续漂移的源头,我一次都没碰过。我治的是症状,而且每次只治几天。真正的解法,根本不是某一条更高明的"改时间"的命令,而是【换一类东西去做这件事】——不是用一条一次性的命令,而是用一个常驻的服务。chrony 这样的时间同步服务,它做的不是"把时间设成某个值",而是"持续地、永不停歇地,把这台机器的时间,一点一点、平滑地,往全世界统一的标准时间上拽"。它承认时间会漂,所以它就一直校;它知道时间不能跳,所以它校得悄无声息,让时钟只是走快一丝或走慢一丝,而不是猛地跳一下——因为它清楚,一次时间的倒退或大跳,就足以让我的定时任务漏跑或重跑,就足以让两台机器的日志再也排不到一起。这次最大的收获,是我意识到,有些东西,它看起来像一个"值",但它的本质是一个"流"。一个值,你设定它、纠正它;而一个流,你只能去持续地【治理】它。我过去太习惯于用对待"值"的方式,去对待这个世界上所有的东西了——找到错的,改成对的,完事。可面对一个会自己流动、自己偏离的东西,"改成对的"这个动作本身就是无效的,因为你改对的那一刻,它就又开始流走了。对待这一类东西,唯一有效的姿势,是放弃"一劳永逸地设对它"的幻想,转而建立一个"持续地、自动地纠正它"的机制。下一次,当我再遇到一个"改了又错、错了再改"的循环时,我会立刻警觉:这大概不是我没找到那个正确的值,而是我错把一个"流"当成了"值"——我该做的,不是更用力地去改它,而是停下来,为它装上一个能持续校准它的东西。
—— 别看了 · 2026