Linux 基础体系 · 第 77/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux 时间同步:Clocksource、NTP、chrony、漂移和分布式系统影响

Linux 中“时间不准”不是单一故障。至少要区分四个层次:

  1. 硬件计时来源:内核从哪个时钟设备读取时间,称为 clocksource
  2. 内核时钟接口:进程通过 CLOCK_REALTIMECLOCK_MONOTONIC 等接口看到什么时间。
  3. 时间同步协议:NTP 客户端如何从远端服务器估计本机与参考时钟之间的偏差。
  4. 系统与应用行为:时间跳变、频率调整、虚拟机暂停和网络延迟如何影响日志、证书、租约和数据库。

chrony 是常见的 NTP 实现,但它不是硬件时钟,也不是 clocksource。它通过内核提供的时间调整接口,控制系统时钟逐渐或立即靠近参考时间。


一、先区分三种“时钟”:RTC、内核时钟和时钟源

1. RTC:断电后仍可保存时间的硬件时钟

RTC(Real-Time Clock)通常由主板或虚拟机固件提供,并由电池维持。它的主要作用是:

  • 开机早期为系统提供一个初始时间;
  • 关机时保存一个近似的墙上时钟;
  • 在没有网络时作为启动时间来源。

Linux 运行后,通常不会每次读取 RTC 来提供当前时间。内核会把 RTC 读出的值作为初始化,然后使用连续计时源推进时间。

RTC 可能工作在:

  • UTC 模式:推荐用于 Linux;
  • 本地时间模式:常见于需要与 Windows 共存的环境。

可以查看当前配置:

timedatectl

示例输出:

               Local time: Tue 2025-03-11 14:20:31 CST
           Universal time: Tue 2025-03-11 06:20:31 UTC
                 RTC time: Tue 2025-03-11 06:20:30
                Time zone: Asia/Shanghai (CST, +0800)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

这里的 Local time 是显示时区转换后的结果,不是另一个独立的系统时钟。Linux 内核通常维护 UTC 语义的系统时间,时区主要影响用户空间的显示和格式化。

1.1 本地时间不等于系统时间错误

例如:

date -u
date

可能分别输出:

Tue Mar 11 06:20:31 UTC 2025
Tue Mar 11 14:20:31 CST 2025

两者相差八小时是时区转换,不是 NTP 偏差。排障时应先使用 UTC 比较不同主机,避免把时区问题误判为同步问题。


二、Clocksource:内核用什么硬件推进时间

2.1 定义与职责

clocksource 是 Linux 内核用于读取“经过了多少时间”的底层计时来源。常见候选包括:

  • tsc:处理器时间戳计数器;
  • hpet:High Precision Event Timer;
  • acpi_pm:ACPI PM Timer;
  • kvm-clock:KVM 虚拟机常用的虚拟时钟源;
  • xen 相关时钟源:Xen 虚拟机使用。

查看当前时钟源:

cat /sys/devices/system/clocksource/clocksource0/current_clocksource
cat /sys/devices/system/clocksource/clocksource0/available_clocksource

示例:

tsc
tsc hpet acpi_pm

这意味着当前内核选择了 tsc,系统还检测到了 hpetacpi_pm

clocksource 解决的是:

内核读取当前时间时,依赖哪个计数器以及如何把计数器转换成纳秒。

它不解决:

当前时间是否与 NTP 服务器、GPS 或原子钟一致。

因此,下面两个故障是不同的:

  • clocksource 不稳定:可能导致本机时间推进异常、CPU 使用率升高或计时不可靠;
  • NTP 不同步:本机时钟源可以非常稳定,但稳定地偏离真实时间。

2.2 Clocksource 与 clockevent 不是一回事

Linux 还使用 clockevent 设备生成定时事件,例如调度器 tick、定时器中断和唤醒事件。

可以这样区分:

  • clocksource:回答“现在过了多久”;
  • clockevent:负责“什么时候触发下一次事件”。

NTP 主要校准系统时间的频率和相位,不是选择 clocksource。修改 current_clocksource 可能改变底层计时行为,但不会替代 NTP 同步。

2.3 TSC 为什么通常较快,但不能盲目信任

现代 x86 系统通常优先使用 invariant TSC,即频率不随 CPU P-state 变化的 TSC。它访问成本低,适合高频读取。

但早期处理器、某些固件、跨插槽系统或虚拟化环境可能存在:

  • 不同 CPU 的 TSC 不同步;
  • TSC 频率推断错误;
  • 虚拟机迁移后 TSC 语义变化;
  • 宿主机暂停或恢复造成时间推进异常。

Linux 会根据硬件能力和校验结果选择时钟源。生产环境不应仅因为某个候选名称“看起来更精确”就强制切换。

可以检查内核启动和运行时线索:

dmesg | grep -Ei 'clocksource|tsc|hpet|kvm-clock|timekeeping'

如果看到类似:

clocksource: timekeeping watchdog on CPU...
clocksource: tsc unstable

说明内核认为当前计时源存在稳定性问题。此时应结合硬件、虚拟化平台和内核日志处理,而不是直接执行 chronyc makestep;后者只能修正墙上时钟偏差,不能修复底层计数器质量。


三、Linux 暴露给程序的时间接口

3.1 CLOCK_REALTIME

CLOCK_REALTIME 表示 Unix 时间,即自 1970-01-01 00:00:00 UTC 起的时间。常见接口包括:

clock_gettime(CLOCK_REALTIME, &ts);

它用于:

  • 日志时间戳;
  • 文件时间;
  • TLS 证书有效期判断;
  • 数据库中的业务时间;
  • NTP 调整后的当前墙上时间。

它可能发生跳变。管理员执行 date -s、NTP 进行 step 校时,都会使该时钟突然向前或向后变化。

3.2 CLOCK_MONOTONIC

CLOCK_MONOTONIC 表示从某个启动相关起点开始持续递增的时间,适合计算超时和耗时:

struct timespec a, b;
clock_gettime(CLOCK_MONOTONIC, &a);
/* 执行操作 */
clock_gettime(CLOCK_MONOTONIC, &b);

它不会因为设置墙上时间而直接跳到过去或未来,因此适合:

  • 网络超时;
  • 重试间隔;
  • 锁等待时间;
  • 请求耗时统计;
  • 连接保活期限。

不过,“不会被设置时间直接跳变”不等于“完全不受 NTP 影响”。Linux 可以通过调整时钟频率,使 CLOCK_MONOTONIC 的推进速度略快或略慢。这样系统可以在不突然跳变的情况下逐渐收敛。

3.3 CLOCK_MONOTONIC_RAWCLOCK_BOOTTIME

CLOCK_MONOTONIC_RAW 更接近未经 NTP 频率校正的底层硬件计时结果,适合研究硬件计时行为,但通常不适合业务超时。

CLOCK_BOOTTIME 类似 CLOCK_MONOTONIC,但包含系统 suspend 的时间。需要计算“机器实际离开启动状态多久”时,它比 CLOCK_MONOTONIC 更合适。

一个典型错误是用 CLOCK_REALTIME 计算超时:

deadline = realtime_now + 30;
while (realtime_now < deadline) {
    /* 等待 */
}

如果期间发生向后校时,循环可能比预期多等待;如果发生向前校时,可能提前结束。正确做法通常是使用单调时钟计算相对期限。


四、NTP 到底估计什么

4.1 四个时间戳

一次 NTP 交互通常包含四个时间点:

  • t1:客户端发送请求的时间;
  • t2:服务器收到请求的时间;
  • t3:服务器发送响应的时间;
  • t4:客户端收到响应的时间。

设:

  • 客户端时钟为 CC
  • 服务器时钟为 SS
  • 服务器相对客户端快 θ=SC\theta = S-C
  • 网络去程延迟为 d1d_1
  • 网络回程延迟为 d2d_2
  • 服务器处理时间为 p=t3t2p=t_3-t_2

理想情况下:

t2t1=d1+θt_2-t_1 = d_1+\theta

t4t3=d2θt_4-t_3 = d_2-\theta

NTP 使用近似公式计算偏差:

θ=(t2t1)+(t3t4)2\theta = \frac{(t_2-t_1)+(t_3-t_4)}{2}

并计算往返网络延迟:

δ=(t4t1)(t3t2)\delta=(t_4-t_1)-(t_3-t_2)

直觉是:如果去程和回程延迟大致相等,两个方向的网络误差平均后会抵消。

4.2 完整算例

假设客户端记录:

t1 = 100.000 s
t4 = 100.120 s

服务器记录:

t2 = 100.050 s
t3 = 100.060 s

则:

θ=(100.050100.000)+(100.060100.120)2=0.0500.0602=0.005 s\theta = \frac{(100.050-100.000)+(100.060-100.120)}{2} = \frac{0.050-0.060}{2} = -0.005\text{ s}

服务器时间相对客户端的估计偏差为 -5 ms,也就是客户端比服务器快约 5 ms,客户端应向后调整。

延迟为:

δ=(100.120100.000)(100.060100.050)=0.1200.010=0.110 s\delta=(100.120-100.000)-(100.060-100.050) =0.120-0.010 =0.110\text{ s}

往返网络延迟约为 110 ms。

4.3 偏差与网络不对称的边界

如果去程延迟为 20 ms,回程延迟为 80 ms,真实偏差为 0,则客户端仍会估计出:

θestimated=20802=30 ms\theta_{\text{estimated}} = \frac{20-80}{2}=-30\text{ ms}

因此 NTP 不能仅靠四个时间戳消除路径不对称。跨公网、经过拥塞链路、无线网络或复杂负载均衡时,网络不对称会形成系统性误差。

这也是为什么:

  • 延迟较低且稳定的时间源更有价值;
  • 不能只看某一次 NTP 响应;
  • 客户端通常需要多个源并进行筛选;
  • offsetdelayjitter 应结合时间序列观察。

4.4 Stratum、offset、delay、jitter

NTP 中的几个常见字段含义不同:

  • stratum:距离参考时钟的层级,不是“时间精度”的直接排名;
  • offset:估计的时钟偏差;
  • delay:网络往返延迟;
  • jitter:偏差测量的波动程度,反映测量稳定性;
  • reach:近期探测是否成功的可达性状态,具体展示形式依实现而异。

Stratum 1 通常直接连接 GPS、原子钟或其他参考时钟;Stratum 2 从 Stratum 1 同步。较低的 stratum 通常更接近参考源,但低 stratum 不保证网络路径一定更好。


五、NTP 如何修正系统时钟:step 与 slew

假设本机时间与参考时间相差 30 秒,客户端有两种基本处理方式。

5.1 Step:立即跳变

直接把系统时间设置为目标值:

当前时间 12:00:00
参考时间 12:00:30
step 后   12:00:30

优点是收敛快,适合:

  • 刚启动时系统时间严重错误;
  • 数据库、证书或认证必须尽快恢复;
  • 尚未向应用提供服务的初始化阶段。

风险是时间不连续:

  • 向前跳会让某些定时任务立即到期;
  • 向后跳会使日志顺序逆转;
  • 应用若错误使用 CLOCK_REALTIME 计算超时,可能异常;
  • 分布式租约和缓存过期判断可能被破坏。

5.2 Slew:调整频率,逐渐追赶

不直接把时间拨到目标点,而是暂时让时钟走快或走慢。

设时间偏差为 OO,允许的校正频率上限为 rr,则理论上最短校正时间约为:

TOrT \approx \frac{|O|}{r}

例如偏差为 20 秒,最大校正速率假设为 500 ppm:

r=500×106r=500\times10^{-6}

T=20500×106=40000 sT=\frac{20}{500\times10^{-6}}=40000\text{ s}

约为 11.1 小时。实际实现可能受内核限制、当前频率偏差和客户端策略影响,不能把这个公式当成所有版本的精确完成时间。

Slew 的优点是时间连续,适合已在运行的业务;缺点是大偏差收敛慢。

5.3 chrony 的启动和运行策略

常见配置:

pool pool.ntp.org iburst
makestep 1.0 3
rtcsync

含义通常是:

  • pool:从 DNS 池获取多个时间源;
  • iburst:首次探测时快速发送一组请求,缩短初始收敛时间;
  • makestep 1.0 3:启动早期允许在偏差超过 1 秒时进行 step,且限制在前若干次更新;
  • rtcsync:在 Linux 上让系统定期把校准后的时间信息用于 RTC 同步。

不同 chrony 版本对选项细节和默认值可能有差异,生产环境应查看:

man chrony.conf

已运行系统上手工执行:

sudo chronyc makestep

会立即要求进行 step。它不是“更强的同步”,而是“允许立即跳变”。执行前应确认业务能承受墙上时间变化,并在变更记录中保留原因和验证结果。


六、漂移:为什么机器会稳定地越来越不准

6.1 漂移的定义

时钟漂移(clock drift)是本机振荡器频率相对理想频率的偏差,常用 ppm(百万分之一)表示。

如果本机每天快 20 秒:

drift=2086400×106231.5 ppm\text{drift}= \frac{20}{86400}\times10^6 \approx231.5\text{ ppm}

如果没有同步机制,偏差会近似随时间线性增长:

O(t)=O0+DtO(t)=O_0+D t

其中:

  • O(t)O(t):时刻 tt 的时间偏差;
  • O0O_0:初始偏差;
  • DD:频率漂移;
  • tt:经过的时间。

温度、电压、老化、CPU 电源状态、虚拟化调度和主板质量都会影响 DD

6.2 漂移文件解决什么问题

chrony 通常将估计出的频率偏差保存到 driftfile,例如:

driftfile /var/lib/chrony/chrony.drift

文件不是“保存当前时间”,而是保存类似“本机时钟通常快或慢多少”的历史估计。下次启动时,chrony 可以先使用该估计值,使系统在等待网络采样期间少走弯路。

它不能替代网络同步,因为:

  • 机器温度可能变化;
  • 虚拟机可能迁移;
  • 服务器硬件可能更换;
  • 长时间离线后误差仍会累积。

6.3 漂移与 clocksource 的关系

漂移可能来自底层振荡器,也可能被计时源和虚拟化实现放大。切换 clocksource 可能改变测量误差,但不会自动获得外部绝对时间。

可以把过程理解为:

硬件/虚拟计数器
        ↓
Linux clocksource 读取并换算
        ↓
CLOCK_REALTIME / CLOCK_MONOTONIC
        ↑
chrony 根据 NTP 样本调整相位和频率

chrony 观测的是系统时间表现,内核负责把调整应用到时间维护逻辑中。


七、chrony 的组件、状态和数据流

chrony 通常包含:

  • chronyd:后台守护进程;
  • chronyc:通过本地控制接口查询和控制 chronyd;
  • 配置文件:定义服务器、池、监听地址、允许客户端和校时策略;
  • driftfile:保存频率估计;
  • 可选的密钥、NTS 数据和日志文件。

典型数据流如下:

flowchart LR
    A[硬件或虚拟 clocksource] --> B[Linux timekeeping]
    B --> C[CLOCK_REALTIME]
    B --> D[CLOCK_MONOTONIC]
    E[NTP 服务器] -->|UDP 123 / NTS| F[chronyd]
    F -->|样本筛选与偏差估计| G[内核频率/相位调整]
    G --> B
    H[chronyc] -->|本地控制接口| F
    I[RTC] -->|启动时初始化| B
    C --> J[日志/证书/业务时间]
    D --> K[超时/耗时/重试]

关键路径是:

  1. 系统启动时从 RTC 或其他固件来源得到初始时间;
  2. chronyd 向多个时间源发送 NTP 请求;
  3. 收集 offsetdelay 和稳定性信息;
  4. 筛选可接受的时间源;
  5. 通过内核时间调整接口执行 step 或 slew;
  6. 内核同时影响 CLOCK_REALTIME 和经过校频的单调时钟;
  7. 应用分别使用墙上时间或单调时间。

7.1 查看同步状态

chronyc tracking
chronyc sources -v
chronyc sourcestats -v

示例:

Reference ID    : 192.0.2.10
Stratum         : 2
Ref time (UTC)  : Tue Mar 11 06:20:18 2025
System time     : 0.000012345 seconds fast of NTP time
Last offset     : -0.000004321 seconds
RMS offset      : 0.000021000 seconds
Frequency       : 12.345 ppm fast
Leap status     : Normal

这些字段应这样理解:

  • System time:当前系统时间相对 chrony 估计参考时间的偏差;
  • Last offset:最近一次测量的偏差;
  • RMS offset:一段时间内的均方根偏差,反映总体稳定性;
  • Frequency:chrony 估计的系统频率修正;
  • Leap status:闰秒状态,不是普通同步成功标志。

sources -v 中常见状态:

  • ^*:当前选中的源;
  • ^+:可接受的候选源;
  • ^-:可访问但因算法筛选未采用;
  • ^?:没有足够有效测量,例如网络不可达或响应不合格;
  • x:被判断为错误源;
  • ~:源的波动过大。

这些符号是 chrony 的常见展示方式,具体字段应以安装版本的 chronyc sources -v 说明为准。


八、配置一个最小但可验证的 chrony 客户端

现代发行版中,包名通常是 chrony,但服务名存在差异:

  • Debian/Ubuntu 常见服务名:chrony.service
  • RHEL、Rocky、AlmaLinux、Fedora 常见服务名:chronyd.service

先确认实际 Unit:

systemctl list-unit-files | grep -E '^chrony|^chronyd'

安装和启动命令因发行版而异,例如 Debian 系:

sudo apt install chrony
sudo systemctl enable --now chrony

RHEL 系:

sudo dnf install chrony
sudo systemctl enable --now chronyd

检查服务:

systemctl status chrony --no-pager
# 或
systemctl status chronyd --no-pager

客户端配置可类似:

pool time.example.net iburst
makestep 1.0 3
rtcsync
driftfile /var/lib/chrony/chrony.drift

修改配置后:

sudo systemctl reload chrony
# 若该 Unit 不支持 reload,则使用:
sudo systemctl restart chrony

是否支持 reload 可验证:

systemctl show chrony -p CanReload

不要假设所有发行版的服务名、配置路径和 reload 行为完全一致。常见路径是:

/etc/chrony/chrony.conf
/etc/chrony.conf

可以通过包管理器或 Unit 文件确认:

systemctl cat chrony
systemctl cat chronyd

8.1 验证不能只看服务是 active

chronyc tracking
chronyc sources -v
chronyc waitsync 60

active (running) 只表示进程存活,不表示已经选出有效时间源。waitsync 60 的用途是等待同步状态满足条件,适合启动检查或自动化脚本;退出码和具体行为应参考本机 chronyc 手册。

还应检查 UDP 123 的网络路径:

sudo ss -lunp | grep ':123'

客户端通常需要能访问远端 UDP 123;作为 NTP 服务器时,还需要防火墙允许受信任客户端访问 UDP 123。


九、作为局域网 NTP 服务器时的安全边界

服务端配置示例:

pool upstream-a.example.net iburst
pool upstream-b.example.net iburst

allow 192.0.2.0/24
deny all

这里的 allow 表示允许指定网络中的客户端查询;具体访问控制语法和默认行为依 chrony 版本而定,应结合 man chrony.conf 检查。

生产网络需要注意:

  • 只向受信任网段提供 NTP 服务;
  • 防火墙限制 UDP 123 的来源;
  • 不要把内部 NTP 服务暴露到公网;
  • 上游时间源应配置多个,并避免全部落在同一网络故障域;
  • 需要认证时,评估 NTS、对称密钥等机制的版本和部署条件。

NTP 反射放大攻击依赖开放的 UDP 服务和伪造源地址。即使某个配置在实验网络可用,也不应直接复制到互联网可达的服务器。

local 等选项可以让服务器在没有可用上游时继续对外提供一个本地参考时间。这适用于隔离网络中的特殊场景,但它不代表时间仍然正确。若上游故障而服务器继续“看起来同步”,可能把错误时间扩散给整个集群,必须配合明确的故障策略和监控。


十、systemd 服务管理与启动顺序

时间同步服务本身由 systemd 管理,但“服务启动成功”与“系统已经同步”是两个状态。

查看依赖和启动顺序:

systemctl list-dependencies chrony.service
systemctl show chrony.service \
  -p After -p Wants -p Requires -p ConditionCapability

不同发行版的 Unit 名称可能是 chrony.servicechronyd.service

应用服务通常不应仅依赖:

After=chrony.service

After= 只表达启动顺序,不保证 chrony 已经完成网络采样或时间收敛。若业务确实要求启动前达到时间条件,可以设计一个显式的同步等待服务,或者在应用自身启动检查中执行等价验证。

例如,业务 Unit 可以表达对时间同步服务的依赖:

[Unit]
Wants=chrony.service
After=chrony.service network-online.target

但这仍然不自动表示“已同步”。更严格的方案是:

  1. systemd 启动 chrony;
  2. 一个一次性服务执行 chronyc waitsync
  3. 业务服务 After=Requires= 该一次性服务;
  4. 等待失败时阻止依赖它的服务启动。

这会增加启动延迟,并且网络长期不可用时可能阻塞系统服务,因此只适用于确有必要的场景。很多业务更适合先启动,再在应用层对认证、租约和时间质量做降级处理。


十一、诊断时间同步故障的路径

11.1 先区分时区、RTC 和系统时间

date
date -u
timedatectl
hwclock --show

date -u 正确而 date 显示异常,优先检查时区:

timedatectl list-timezones | grep Shanghai
sudo timedatectl set-timezone Asia/Shanghai

修改时区不会校准 UTC 系统时间,只改变显示和本地时间转换。

11.2 再确认服务与选源状态

systemctl is-enabled chrony.service
systemctl is-active chrony.service
chronyc activity
chronyc sources -v
chronyc tracking

常见故障模式:

服务未运行

System has not been booted with systemd ...

这说明当前环境可能是容器、精简 init 或非 systemd 系统。容器通常不能独立调整宿主机内核时间,应在宿主机同步。

所有源都是 ^?

优先检查:

  • DNS 是否能解析;
  • UDP 123 是否被防火墙阻断;
  • NTP 服务器是否可达;
  • 上游是否因访问控制拒绝;
  • 虚拟机时间是否被宿主机管理机制覆盖。

日志:

journalctl -u chrony.service -b --no-pager
# 或
journalctl -u chronyd.service -b --no-pager

^*,但偏差持续变大

可能原因包括:

  • 本机频率漂移很大;
  • 虚拟机被暂停或过度调度;
  • 宿主机和客户机同时调整时间;
  • 时钟源不稳定;
  • NTP 源路径存在显著不对称;
  • 系统时间被其他程序反复设置。

检查谁在修改时间:

ps aux | grep -E '[n]tp|[c]hronyd|[s]ystemd-timesyncd|[t]imedatectl'
journalctl -b | grep -Ei 'time|clocksource|chrony|ntp'

一台机器不应同时运行多个独立的时间同步守护进程。常见冲突包括 chronydntpdsystemd-timesyncd 同时工作。

11.3 使用 adjtimex 观察内核调整状态

在许多发行版中可使用:

adjtimex --print

输出可能包含:

       mode: 0
     offset: 0
  frequency: 123456
      status: 0

这里的 frequency 通常使用内核特定单位,不应直接当作 ppm 解读;chrony 的 Frequency 展示通常更适合运维阅读。adjtimex 适合确认内核是否存在频率、相位或状态调整,但字段单位和权限应以本机 man 2 adjtimex 为准。


十二、强制设置时间的风险与恢复

12.1 date -s 不是同步方案

例如:

sudo date -s '2025-03-11 14:30:00'

这会直接改变 CLOCK_REALTIME,适合紧急恢复或没有 NTP 的临时环境,不适合日常校时。风险包括:

  • 时间向后移动;
  • 日志顺序错乱;
  • 定时任务触发异常;
  • 证书、Kerberos 和数据库验证失败;
  • 集群节点间时间关系瞬间改变。

如果必须执行,应先:

  1. 记录当前 UTC 时间和业务状态;
  2. 确认需要跳变的范围和方向;
  3. 通知会受影响的服务;
  4. 校时后重启或重新加载依赖时间的组件;
  5. 通过 chronyc tracking 和应用指标验证;
  6. 记录操作原因和恢复结果。

12.2 hwclock --systohc 的边界

sudo hwclock --systohc --utc

它把系统时间写入 RTC。只有当系统时间已经可靠同步后才应执行。若系统时间本身错误,这个命令会把错误持久化到下一次启动。

反方向的:

sudo hwclock --hctosys

会用 RTC 覆盖系统时间,通常只在启动初始化或明确的恢复流程中使用。正在运行的生产系统不应随意执行。


十三、闰秒、UTC 和“时间必须连续”的矛盾

UTC 可能通过闰秒调整,使原子时与地球自转时间保持一定关系。不同系统、内核、NTP 实现和应用对闰秒的处理方式可能不同,常见策略包括:

  • 在特定时刻插入或重复一秒;
  • 通过 leap smear 在一段时间内平滑改变频率;
  • 在系统内部采用不重复的时间尺度,再由外部转换。

因此,分布式系统不能默认“所有机器对闰秒的行为完全相同”。尤其要避免:

  • CLOCK_REALTIME 作为高精度计时器;
  • 假设每个 UTC 秒都唯一且严格递增;
  • 用本地日志时间戳单独推导事件先后。

NTP 的 Leap status 可以提示闰秒状态,但它不说明应用一定采用了某种闰秒策略。跨系统比较时,应明确时间尺度和供应商实现。


十四、虚拟机和容器中的特殊问题

14.1 虚拟机有两层时间维护

虚拟机时间通常受两层影响:

物理机/云平台时钟
        ↓
虚拟化时钟设备
        ↓
客户机 Linux clocksource
        ↓
客户机 chrony

如果宿主机、虚拟化工具和客户机 chrony 同时调整客户机时间,可能形成竞争:

  • 客户机刚 slew,宿主机又注入时间;
  • 虚拟机暂停后恢复,客户机一次性出现大偏差;
  • 迁移到不同宿主机后时钟频率发生变化。

应根据平台文档决定时间责任边界。常见做法是保留客户机内的 chrony,同时确保宿主机时间可靠,并避免多个工具无协调地设置客户机墙上时钟。

14.2 容器不能替代宿主机同步

Linux 的系统时间属于内核,而不是普通容器文件系统。容器内运行 chronyd 通常不能独立维护一个只属于该容器的系统时间;修改时间还需要 CAP_SYS_TIME,这会扩大权限风险。

因此:

  • 容器通常使用宿主机提供的时间;
  • 不要为了“容器内时间同步”轻易授予 CAP_SYS_TIME
  • Kubernetes 节点应先保证宿主机时间质量;
  • 应用超时仍应使用单调时钟。

十五、分布式系统为什么依赖时间质量

15.1 日志排序不是可靠的因果排序

两个节点分别记录:

节点 A:10:00:00.100 写入订单
节点 B:09:59:59.900 收到复制事件

不能据此断定 B 的事件发生在 A 之前。原因可能是:

  • 节点 B 慢 500 ms;
  • 网络延迟;
  • 日志缓冲;
  • 不同时间精度;
  • NTP 测量误差。

墙上时间适合人类检索和跨系统关联,但不应单独承担严格因果排序。需要因果关系时,应使用:

  • 数据库提交序列;
  • 单调递增 ID;
  • Lamport clock;
  • 向量时钟;
  • 共识日志中的索引。

15.2 租约和 TTL 的安全条件

设服务端在真实时间 T0T_0 发出租约,租期为 LL。客户端根据自己的墙上时钟认为租约在:

TC=T0+LT_C = T_0 + L

过期。但客户端时钟相对真实时间最大误差为 ϵ\epsilon,网络和处理的不确定性为 δ\delta。为了避免客户端在服务端已经认为租约过期后仍继续使用资源,需要保守地使用:

Tlocal deadlineT0+LϵδT_{\text{local deadline}} \leq T_0 + L - \epsilon - \delta

如果系统不能估计 ϵ\epsilon,就不能仅凭“所有节点都启用了 NTP”证明租约安全。

完整例子:

  • 服务端租约为 30 秒;
  • 客户端时钟可能比真实时间慢 2 秒;
  • 网络和处理不确定性为 1 秒。

客户端至少应提前:

2+1=3 秒2+1=3\text{ 秒}

停止使用资源,即按照约 27 秒的本地有效期限处理,而不是毫无余量地使用 30 秒。

这不是 chrony 的配置问题,而是分布式协议必须显式建模的时钟不确定性。

15.3 证书、Kerberos 和签名验证

TLS 证书包含 NotBeforeNotAfter。客户端时间过早可能认为证书尚未生效,时间过晚可能认为证书已过期。

Kerberos 对票据有效期和时钟偏差也有明确要求。常见现象包括:

certificate is not yet valid
certificate has expired
Clock skew too great

这类错误不一定表示证书或密码错误,必须同时检查:

date -u
chronyc tracking

15.4 数据库和消息系统的时间问题

时间异常可能导致:

  • 按时间窗口查询漏数据或重复数据;
  • CDC、归档和保留策略提前或延后执行;
  • 消息 TTL 提前过期;
  • 事件时间与处理时间混淆;
  • 基于时间戳的去重失效。

业务数据应区分:

  • 事件发生时间
  • 消息到达时间
  • 数据库提交时间
  • 处理耗时

前两者可以使用 UTC 墙上时间,但耗时和超时应使用单调时钟,严格排序则应依赖序列或共识顺序。


十六、常见误解与对应反例

误解一:NTP 正常就表示系统所有计时都正确

反例:CLOCK_REALTIME 与 NTP 偏差很小,但 clocksource 因虚拟化异常而间歇性跳变,应用的高精度计时仍可能异常。

应分别观察:

cat /sys/devices/system/clocksource/clocksource0/current_clocksource
chronyc tracking

误解二:stratum 越低,网络时间一定越好

反例:一个 Stratum 2 源跨越高延迟、强不对称公网;另一个 Stratum 3 源在同一机房,延迟和抖动更小。后者可能给出更稳定的本地时间估计。

误解三:active (running) 就是已同步

反例:chronyd 进程正在运行,但所有源都是 ^?,UDP 123 被防火墙丢弃。进程健康不等于时间状态健康。

误解四:时间校准后,单调时钟完全不变

反例:NTP 的频率校正会改变 CLOCK_MONOTONIC 的推进速率,只是不会像 CLOCK_REALTIME 那样因手工设置而直接跳变。需要原始硬件速率时才考虑 CLOCK_MONOTONIC_RAW

误解五:把所有节点时间强行设置成同一个值就实现了同步

反例:命令传播有网络延迟,节点执行时间不同;振荡器频率也不同。一次性设置只能消除当时的相位偏差,不能消除之后的漂移,必须持续测量和校频。

误解六:把 chrony 放进每个容器就能解决集群时间

反例:容器共享宿主机内核时间,普通容器没有 CAP_SYS_TIME。正确边界通常是同步宿主机,在应用中使用正确的时钟接口。


十七、生产监控应关注“时间质量”,而非只监控进程

至少应监控:

  • 当前选中的 NTP 源;
  • 同步状态和 Leap status
  • System time 偏差;
  • Frequency 变化趋势;
  • 源的可达性;
  • delayjitter
  • 时间跳变事件;
  • 各节点之间的最大时间差;
  • 虚拟机暂停、迁移和宿主机时钟告警。

监控告警应区分:

  1. 服务故障:chronyd 进程退出;
  2. 源故障:没有可选上游;
  3. 质量下降:偏差、延迟或抖动超阈值;
  4. 业务风险:证书、Kerberos、租约或数据库窗口已经受影响。

只监控 systemctl is-active chronyd 会漏掉第 2 至第 4 类问题。

时间同步变更也应纳入生产运行手册:

  • 修改上游地址前验证 DNS、路由和防火墙;
  • 修改 makestep 前评估业务对跳时的容忍度;
  • 变更后检查 sourcestracking 和应用错误;
  • 记录是否发生 step;
  • 若多个节点同时大幅校时,先暂停依赖严格时序的批处理或租约续期操作;
  • 在复盘中区分时区错误、NTP 不可达、时钟源异常和应用错误使用墙上时间。

十八、一个端到端的检查流程

下面流程适合单台现代 Linux 主机,命令需要相应权限的步骤使用 sudo

# 1. 看显示时区、UTC、RTC 和 NTP 服务状态
timedatectl

# 2. 看内核当前使用的 clocksource
cat /sys/devices/system/clocksource/clocksource0/current_clocksource

# 3. 看 chrony 是否在运行
systemctl is-active chrony.service 2>/dev/null || \
systemctl is-active chronyd.service

# 4. 看时间源是否可达并被选中
chronyc sources -v

# 5. 看偏差、频率和闰秒状态
chronyc tracking

# 6. 看近期服务日志
journalctl -u chrony.service -b --no-pager 2>/dev/null || \
journalctl -u chronyd.service -b --no-pager

判断顺序应是:

  1. date -u 是否符合预期;
  2. 时区是否只是显示问题;
  3. clocksource 是否稳定;
  4. chronyd 是否运行;
  5. 是否有 ^* 或可接受的候选源;
  6. System time 是否持续收敛;
  7. 是否存在其他程序、宿主机或人工命令反复设置时间;
  8. 应用是否错误使用 CLOCK_REALTIME 计算期限。

结语

clocksource 决定 Linux 如何读取和推进底层时间,NTP 决定如何从远端样本估计绝对时间偏差,chrony 则负责把估计结果应用为 step 或 slew,并持续学习本机的频率漂移。

真正可靠的时间体系必须同时满足三个条件:

  • 底层计时源稳定;
  • NTP 源可达、可信且具有足够的冗余;
  • 应用根据用途选择墙上时钟、单调时钟或显式的分布式顺序机制。

时间同步降低的是时间偏差,不会自动提供因果顺序,也不会让租约、证书和数据库逻辑天然安全。分布式系统若需要这些保证,必须把时钟误差、网络延迟、跳时行为和故障恢复路径明确写进协议设计中。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。