集群三钟对表:系统时钟、主板 RTC 与 BMC 的时间口径与巡检
一句话:集群里的时间问题很少是"时钟不准",多半是"三个时钟各说各话、口径没人定"。系统时钟、主板 RTC、BMC 时钟是三个独立器件,只有把口径写进文件、把方向定成单向、把巡检做成脚本,才不会在某个作业超时或某条告警里才发现差了 8 小时。
一、一台服务器上,其实是三个时钟
| 时钟 | 是什么 | 断电/断网后还走吗 | 谁在维护 | 精度 |
|---|---|---|---|---|
| 系统时钟 | 内核维护的时间,本质是"自 1970-01-01 UTC 起的秒数" | 不走,重启即丢 | chrony / NTP | 受 NTP 源约束 |
| 主板 RTC | 带电池或电容的独立硬件,也叫 CMOS 时钟、BIOS 时钟,内核里叫 persistent clock | 走,所以开机时用它初始化系统时钟 | 几乎没人管 | 秒级读数,长期漂 |
| BMC 时钟 | 带外管理芯片自己的时钟 | 走(BMC 一直供电) | 基本没人管 | 漂得比 RTC 更快 |
三者服务不同的东西:
- 系统时钟决定作业记账、日志时间戳、
make依赖判定、TLS 证书有效期校验——它才是"业务时间"; - 主板 RTC 的唯一职责是机器没开时替你记时间,开机时把值交给系统时钟当起点;
- BMC 时钟只影响带外那一侧:SEL 事件时间戳、传感器告警、SNMP trap 与告警邮件时间。它和操作系统之间没有自动同步通道。
一个关键认知:RTC 里只存了"日期 + 时间",它没有时区概念——时钟本身不带"我这是 UTC 还是本地时间"的标记,这个约定完全靠外部文件记录(下一节)。而 BMC 连这个约定文件都没有,它只是把某个数字读出来给你看。
二、方向只能是单向:系统时间 → RTC / BMC
hwclock 两个方向都能写,但只有一个是对的:
# 正解:系统时间 → RTC
hwclock --systohc
# 错误方向:RTC → 系统时间
hwclock --hctosys # 手册原话:should never be used on a running system为什么 --hctosys 在运行中的机器上是错的,两条理由都很硬:
- 跳变会破坏文件系统时间戳。系统时间往回跳一秒,正在写日志/正在
fsync的进程可能看到"文件修改时间在未来",某些文件系统和数据库会因此报错; - 漂移补偿会被算两遍。
--hctosys会按 adjtime 里记录的漂移率去"修正"读到的 RTC 值,而这个 RTC 值本身可能已经被别的东西(内核的 11 分钟模式、另一个操作系统)改动过,于是补偿加在错的基础上。
还有一条前置条件经常被忽略:hwclock --systohc 之前,系统时间必须是可信的。如果 chrony 还在追一个偏 190 ms 的公网源,你去 --systohc,等于把这个偏差写进硬件——把误差刻进晶振,后面每次开机都从错的地方起步。
所以顺序永远是:先把时间源修对 → 再写 RTC → 最后管 BMC。
三、/etc/adjtime:口径必须落盘,否则谁先写谁定规矩
hwclock 手册里有一段话值得逐字读:
如果既不给
--utc也不给--localtime,就用 adjtime 文件里上次写入时记录的那个;如果 adjtime 文件不存在,默认是 UTC。
也就是说,这台机现在读数"恰好正确",可能只是因为默认值恰好对上了。一旦有人执行 hwclock --localtime --systohc,或者某个脚本用 -l,RTC 就被写成当地时间,此后每次 hwclock -r 都会偏 8 小时,而且没有任何文件告诉你当初是怎么写的。
/etc/adjtime 的格式是三行 ASCII:
Line 1: 漂移率(秒/天) 上次调整的时间戳(自 1969 UTC 的秒数) 0.0
Line 2: 上次校准的时间戳(没有校准过就写 0)
Line 3: UTC 或 LOCAL第三行就是"口径"。生成它的正规方式是:
timedatectl set-local-rtc 0 # 声明 RTC 按 UTC 存,写 /etc/adjtime
cat /etc/adjtime # 第三行应为 UTC如果文件已经丢了、又要把 RTC 定成本地时间,hwclock --localtime --adjust 会在文件不存在时创建一个 LOCAL 口径、漂移率为 0 的 adjtime(此时它并不会真的去调整时钟)。
还有一个容易被忽略的依赖:chronyd 自己也会解析这个文件(配置项 hwclockfile,默认 /etc/adjtime),用来判断 RTC 到底存的是 UTC 还是本地时间。文件缺失时,chrony 对 RTC 的读写同样只能靠猜。所以 adjtime 不是 hwclock 的私事,它是全系统的约定载体。
顺带纠正一个常见误解:现代 util-linux 的 hwclock --systohc 不会再自动重算漂移率(v2.31 起改为必须显式加 --update-drift),也不会读 RTC。它只做两件事:把系统时间写进去、更新 adjtime 里的时间戳。指望它"顺便学一下 RTC 漂多少",是旧版本的习惯。
四、UTC 还是本地:显示口径的不对称才是坑
手册里对 LOCAL 与 UTC 的对比讲得很直白:
- RTC 存本地时间时,夏令时切换会出现不一致:Linux 正在运行,写进 RTC 的值会跟着切;Linux 没在运行,从 RTC 读到的值不会跟着切;
- RTC 存 UTC 时,Linux 只在显示层换算,夏令时是透明的。
所以规范做法是 RTC 存 UTC。而在我们这种全年 +08:00、无夏令时的时区,两种存法都不会真出错——但混乱来自另一个地方:显示口径不对称。
| 时钟 | 存储口径 = UTC 时 | 存储口径 = 本地时 |
|---|---|---|
主机 RTC(hwclock -r) | 显示 19:11(它把 UTC 换算成本地再打印,手册原话:The time shown is always in local time, even if you keep your Hardware Clock in UTC) | 显示 19:11 |
BMC(ipmitool sel time get) | 显示 11:11(它不做换算,直接吐存储值) | 显示 19:11 |
这张表就是全部矛盾的来源:hwclock 有换算能力,BMC 没有。
- 主机 RTC 存 UTC 是"双赢":存储规范、显示仍等于
date; - BMC 只能二选一:存 UTC 就规范但读出来差 8 小时,存本地就"读出来等于
date"。
如果运维习惯是"看到 sel time get 就等于 date",那就让 BMC 存本地时间,并把这条写进规范。真正的错误不是选哪个,而是同一批机器两种都有、且没人记录。
五、chrony 管 RTC:rtcsync 和 rtcfile 是互斥的两个选项
这是最容易配错的一处,因为两个选项都能"让 RTC 保持准",但机制和能力完全不同:
| 配置 | 机制 | 能观测 RTC 漂移吗 | 精度 |
|---|---|---|---|
rtcsync | 在 Linux 上只是启用内核的 11 分钟模式:内核在时钟纪律进入同步状态后,每 11 分钟把系统时间拷进 RTC;chrony 自己不跟踪 RTC 漂移 | 不能 | 秒级 |
rtcfile + rtcautotrim 1 | chrony 自己测 RTC 的偏差与速率并记录,超过阈值就 trim | 能,chronyc rtcdata 直接读出 ppm | trim 精度约 1 秒,最小有效阈值就是 1 秒 |
两者不能同时用(文档明确:rtcsync cannot be used with rtcfile)。
怎么选:
# 只求"RTC 别乱" —— 最省事
rtcsync
# 想拿到 RTC 漂移数据、判断纽扣电池有没有老化 —— 更有信息量
rtcfile /var/lib/chrony/rtc
rtcautotrim 1rtcdata 的输出是判断硬件问题的关键证据:
RTC is fast by : -1.632736 seconds # RTC 快了多少
RTC gains time at : -107.623 ppm # 每秒gain/lose多少
Number of samples : 10
Sample span period : 549 # 采样跨度107 ppm 换算过来约 9 秒/天——这种量级就不是"软件没同步",而是晶振或电池该换了。没有 rtcfile 的话,你只能靠 hwclock -r 和 date 反复对表去估,估不出 ppm。
六、chrony 的三份输怎么读:tracking / sources / sourcestats
6.1 tracking:先看误差上界,不是只看 offset
System time : 0.000006523 seconds slow of NTP time
Last offset : -0.000006747 seconds
RMS offset : 0.000035822 seconds
Frequency : 3.225 ppm slow
Residual freq : -0.000 ppm
Skew : 0.129 ppm
Root delay : 0.013639022 seconds
Root dispersion : 0.001100737 seconds
Update interval : 64.2 seconds文档给的精度上界公式值得背下来:
clock_error <= |system_time_offset| + root_dispersion + 0.5 * root_delay这条式子解释了为什么"offset 只有 8 ms"也不能高枕无忧:如果 root delay 是 246 ms,单这一项就贡献 123 ms 的可能误差上界。offset 是"上次测出来的偏差",上界是"你凭什么相信这个偏差"。
6.2 sources:三列决定一个源能不能用
#* ntp1.example.net 2 6 377 23 -923us[ -924us] +/- 43ms| 列 | 含义 | 判断 |
|---|---|---|
S | 选择状态:* 当前最优、+ 参与合并、- 可选但未用、x falseticker、~ 抖动过大、? 不可选 | 只有 * 和 + 真正在给时钟做贡献 |
Poll | 轮询间隔的以 2 为底的指数,6 就是 64 秒 | 越大越省流量,也越迟钝 |
Reach | 8 位可达性寄存器,八进制打印,377 = 最近 8 次全收到 | 不是 377 就说明在丢包或不通 |
Last sample 后的 +/- | 该源的 root distance,即测量误差边界 | 几百毫秒的基本可以判死 |
6.3 sourcestats:看频率估计是否可信
Name/IP Address NP NR Span Frequency Freq Skew Offset Std Dev| 列 | 含义 |
|---|---|
NP | 当前保留的样本数(回归用的点) |
NR | 残差同号的连续段数——太小说明"直线已经拟合不上了" |
Span | 最老到最新样本的时间跨度 |
Frequency | 该源的残余频率估计,ppm |
Freq Skew | 频率估计的误差边界,ppm |
Offset | 该源的估计偏差 |
Std Dev | 样本标准差 |
判据在配置里:maxupdateskew 默认 1000 ppm,超过这个阈值的频率估计会被认为不可靠、不该采用。所以看到 Freq Skew 是几万 ppm 的源,结论很干脆——这台源连自身的稳定性都没建立起来,留着只会污染源选择。
一次真实读数的对照(公网池与国内源混配):
| 源类型 | Offset | Freq Skew | Std Dev | 结论 |
|---|---|---|---|---|
| 某公网池成员 A | +193 ms | 55699 ppm | 7316 us | 不可用 |
| 某公网池成员 B | −92 ms | 29072 ppm | 6379 us | 不可用 |
| 某公网池成员 C | −42 ms | 63000 ppm | 7213 us | 不可用 |
| 国内源 A | −5372 us | 253 ppm | 1488 us | 可用 |
| 国内源 B | +343 us | 455 ppm | 981 us | 可用 |
注意这批"不可用"的源,Std Dev 只有几毫秒——单次测量看着挺准,长期频率却完全不可信。这正是 Freq Skew 这一列存在的意义:判断的不是"这一次准不准",而是"这个源能不能长期带你走"。
结论是一条反直觉的原则:少而稳的 3 个源,远好过 7 个一半是噪声的源。源越多,多数表决越容易被拉成"集体偏",而且每个坏源都在消耗 minsources 的信噪比。
6.4 两个配置细节
- 公网服务器不要用小于 64 秒的轮询。文档明确:短于
minpoll 6(64 秒)的间隔用在公网服务器上会被视为滥用,只有往返时延 < 10 ms 的局域网才允许亚秒级; minsources默认是 1。想让"至少两台一致才更新时钟",把它提到 2 或 3,代价是源少的时候时钟不更新(比被单台坏源带偏要好)。
七、BMC 时间:一次写对,之后靠兜底
ipmitool sel time get # 读
ipmitool sel time set "11/14/2026 19:08:07" # 写四个实践要点:
- 时间格式按 locale,手册写的是 "using the locale's date / time format"。最稳的是
LC_ALL=C下用%m/%d/%Y %H:%M:%S;报格式错就先locale看当前语言环境,别硬试; - 手册建议先清 SEL 再设时间。因为已有条目的时间戳是按旧时钟记的,混在一起就没法用。但注意
sel clear不可逆,生产节点上别随手清——先确认没有人正在依赖这批历史事件; - SEL 里出现
Pre-Init Time-stamp就是证据:说明 BMC 时钟从来没被设过,那些事件的时间戳不可信。这是判断"要不要管这台机时间"的最快办法; Node busy是 SEL 写操作串行化导致的。BMC 的 SEL 队列一次只处理一个写请求,短时间内连发第二条会被直接拒。处理方式是退避重试(间隔 10 秒),而不是并发重试;带外监控正在轮询 SEL 时最容易撞上。
还有两个"必然回退"的场景,要写进流程:刷 BIOS/BMC 固件之后、整机断电放置一段时间之后,BMC 时间常回落到旧值甚至 1970。所以 BMC 时间不是"设一次就完事",要么在 BMC 自带的 NTP 设置里配源(多数管理界面「Date / Time」页有,这是正解),要么用脚本日校兜底。
八、把巡检固化成脚本,避开 cron 的两个坑
要固化的动作有三件:读三个钟 → 判断偏差 → 超阈值就纠正并留痕。做成脚本而不是塞进 crontab 一行,是因为 cron 有两个必踩的坑:
%在 crontab 的命令部分会被转成换行。写date '+%m/%d/%Y'会直接把命令截断,必须写成\%。稳妥做法是命令里一个%都不出现,全部放进脚本;/etc/cron.d/下文件必须root:root且 644,否则 cron 静默忽略,只在 syslog 里留一句 “WRONG FILE OWNER”。
cat > /usr/local/sbin/time-audit.sh <<'EOF'
#!/bin/bash
# 三钟体检:系统时钟 / 主板 RTC / BMC。口径约定:RTC 存 UTC,BMC 存本地
# 用法: time-audit.sh [-f] -f 表示偏差超阈值时自动纠正
set -u
FIX=0; [ "${1:-}" = "-f" ] && FIX=1
sys=$(date +%s)
rtc_s=$(date -d "$(hwclock -r)" +%s 2>/dev/null || echo 0)
drift=$(( rtc_s - sys )); [ "$drift" -lt 0 ] && drift=$(( -drift ))
adj=$(sed -n '3p' /etc/adjtime 2>/dev/null || echo "MISSING")
echo "adjtime=${adj:-MISSING} RTC_drift=${drift}s"
if [ "$adj" != "UTC" ] && [ "$adj" != "LOCAL" ]; then
echo "WARN: /etc/adjtime 缺失或口径未定义" >&2
fi
if [ "$drift" -gt 5 ] && [ "$FIX" = "1" ]; then
hwclock --systohc && logger -t time-audit "RTC corrected (drift ${drift}s)"
fi
bmc=$(ipmitool sel time get 2>/dev/null | awk '{print $1" "$2}')
echo "BMC=${bmc:-unreadable}"
if [ -n "$bmc" ] && [ "$FIX" = "1" ]; then
for i in 1 2 3; do
ipmitool sel time set "$(date '+%m/%d/%Y %H:%M:%S')" 2>/dev/null && {
logger -t time-audit "BMC set ok"; break; }
logger -t time-audit "BMC set failed try=$i"; sleep 10
done
fi
EOF
chmod 755 /usr/local/sbin/time-audit.sh
cat > /etc/cron.d/time-audit <<'EOF'
17 3 * * * root /usr/local/sbin/time-audit.sh -f
EOF
chmod 644 /etc/cron.d/time-audit先不带参数跑一次看报告,确认判断逻辑对,再加 -f。
验收合格线(建议写进交付基线):
| 项 | 合格线 | 超了怎么办 |
|---|---|---|
tracking 的 offset | ±10 ms(公网源的现实上限) | 换源,看 sourcestats 的 Skew |
hwclock -r vs date | ≤ 1 s | hwclock --systohc,且先确认 adjtime 口径 |
ipmitool sel time get vs date | ≤ 1 s(BMC 存本地的口径下) | 脚本重设,确认 SEL 里没有 Pre-Init Time-stamp |
/etc/adjtime 第三行 | UTC | 缺失或 LOCAL 都要修 |
| RTC 漂移率 | ≤ 1 s/周 | 超了查电池/晶振,写入没用 |
九、边界:这套方法到哪儿为止
- 公网 NTP 的天花板就是毫秒级。246 ms 的往返时延摆在那里,offset 再好也只能到十几毫秒。要微秒级必须上 PTP + 硬件时间戳,这不是换源能解决的(PTP 精确时间同步实战 讲的是那一层);
- 相对一致性比绝对准确更重要。几十台节点各打各的公网源,彼此相对偏差可能到几十毫秒,比"整体慢 8 ms"危害大得多。没有内网授时节点时,挑一台对着公网源,其余节点指向它(chrony 里
allow给网段),这个动作能把节点间偏差压到 1 ms 内; - 闰秒:
leapsectz right/UTC让 chrony 按 tz 库正确处理闰秒;如果有大量长作业,还要考虑闰秒插入时的"时间跳一秒"对作业超时判定的影响,服务端可用smoothtime抹平; - 不要在运行中的节点上做大跳变。1 秒的偏移用 slewing 慢慢追;非要 step,先确认没有正在跑的作业依赖单调时间;
- 虚拟化环境的时钟:虚拟机用
kvm-clock/tsc时钟源时,steal time 会让偏移变大,检查chronyc tracking的Frequency是否异常;容器共享宿主时钟,不需要也不应该自己跑 chronyd; - 镜像里就该带上这些约定。golden image 阶段把 chrony 配置、
/etc/adjtime口径、BMC 校时脚本一起固化,比交付后再一台台补要省事得多(黄金镜像流水线、集群交付自动化)。
收口三句
- 三个钟,一个方向:系统时间永远是源头,RTC 和 BMC 只能被写完,从它们往回拽就是错的;
- 口径必须落盘:
/etc/adjtime第三行那一个词,决定了以后每一台接管这台机的人是看到 19:11 还是 11:11; - 巡检要机械化:
Freq Skew超过maxupdateskew的源该扔,hwclock -r和date差超过 1 秒该写,Pre-Init Time-stamp出现该补——这三条都不该靠人盯。
延伸阅读
- PTP 精确时间同步实战:从 NTP 毫秒级到亚微秒的集群时间基线
- 黄金镜像流水线:从标准化到可复现的节点交付
- 集群交付自动化:批量部署与验收的工程化路径
- Slurm 作业提交与管理指南
- PXE 排障手册:从 DHCP 到内核启动的完整链路