Skip to content

集群三钟对表:系统时钟、主板 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 两个方向都能写,但只有一个是对的:

bash
# 正解:系统时间 → RTC
hwclock --systohc

# 错误方向:RTC → 系统时间
hwclock --hctosys        # 手册原话:should never be used on a running system

为什么 --hctosys 在运行中的机器上是错的,两条理由都很硬:

  1. 跳变会破坏文件系统时间戳。系统时间往回跳一秒,正在写日志/正在 fsync 的进程可能看到"文件修改时间在未来",某些文件系统和数据库会因此报错;
  2. 漂移补偿会被算两遍。--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

第三行就是"口径"。生成它的正规方式是:

bash
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 1chrony 自己测 RTC 的偏差与速率并记录,超过阈值就 trim能,chronyc rtcdata 直接读出 ppmtrim 精度约 1 秒,最小有效阈值就是 1 秒

两者不能同时用(文档明确:rtcsync cannot be used with rtcfile)。

怎么选:

ini
# 只求"RTC 别乱" —— 最省事
rtcsync

# 想拿到 RTC 漂移数据、判断纽扣电池有没有老化 —— 更有信息量
rtcfile /var/lib/chrony/rtc
rtcautotrim 1

rtcdata 的输出是判断硬件问题的关键证据:

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 秒越大越省流量,也越迟钝
Reach8 位可达性寄存器,八进制打印,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 的源,结论很干脆——这台源连自身的稳定性都没建立起来,留着只会污染源选择。

一次真实读数的对照(公网池与国内源混配):

源类型OffsetFreq SkewStd Dev结论
某公网池成员 A+193 ms55699 ppm7316 us不可用
某公网池成员 B−92 ms29072 ppm6379 us不可用
某公网池成员 C−42 ms63000 ppm7213 us不可用
国内源 A−5372 us253 ppm1488 us可用
国内源 B+343 us455 ppm981 us可用

注意这批"不可用"的源,Std Dev 只有几毫秒——单次测量看着挺准,长期频率却完全不可信。这正是 Freq Skew 这一列存在的意义:判断的不是"这一次准不准",而是"这个源能不能长期带你走"。

结论是一条反直觉的原则:少而稳的 3 个源,远好过 7 个一半是噪声的源。源越多,多数表决越容易被拉成"集体偏",而且每个坏源都在消耗 minsources 的信噪比。

6.4 两个配置细节 ​

  • 公网服务器不要用小于 64 秒的轮询。文档明确:短于 minpoll 6(64 秒)的间隔用在公网服务器上会被视为滥用,只有往返时延 < 10 ms 的局域网才允许亚秒级;
  • minsources 默认是 1。想让"至少两台一致才更新时钟",把它提到 2 或 3,代价是源少的时候时钟不更新(比被单台坏源带偏要好)。

七、BMC 时间:一次写对,之后靠兜底 ​

bash
ipmitool sel time get                        # 读
ipmitool sel time set "11/14/2026 19:08:07"  # 写

四个实践要点:

  1. 时间格式按 locale,手册写的是 "using the locale's date / time format"。最稳的是 LC_ALL=C 下用 %m/%d/%Y %H:%M:%S;报格式错就先 locale 看当前语言环境,别硬试;
  2. 手册建议先清 SEL 再设时间。因为已有条目的时间戳是按旧时钟记的,混在一起就没法用。但注意 sel clear 不可逆,生产节点上别随手清——先确认没有人正在依赖这批历史事件;
  3. SEL 里出现 Pre-Init Time-stamp 就是证据:说明 BMC 时钟从来没被设过,那些事件的时间戳不可信。这是判断"要不要管这台机时间"的最快办法;
  4. 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”。
bash
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 shwclock --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 出现该补——这三条都不该靠人盯。

延伸阅读 ​

相关文章 ​

AI-HPC 智算超算专家 · Contact: openaihpc@gmail.com