土法炼钢兴趣小组的算法知识备份

【分布式系统百科】物理时钟的谎言:NTP、PTP 与时钟漂移的工程现实

文章导航

分类入口
distributed-systems
标签入口
#physical-clock#ntp#ptp#clock-drift#truetime#clockbound#leap-second#distributed-systems#time

目录

Part II · 时间、顺序与因果

这是分布式系统百科第二部分的第一篇。第二部分集中讨论分布式系统中最基础也最容易被误解的问题——时间。本文从物理时钟的工程现实切入,下一篇 逻辑时钟 将介绍 Lamport 时钟和向量时钟如何绕开物理时钟的局限。

上一篇:复杂性度量

你在两台数据库副本上各看了一眼系统时间。机器 A 报告 10:00:00.000,机器 B 报告 10:00:00.003。问题:B 的时钟真的比 A 快了 3 毫秒吗?还是 A 实际上比 B 快,只是 NTP 上次校准后漂移了?

如果你的分布式数据库用 wall clock 给事务排序——Spanner 做的就是这件事——那 3 毫秒的不确定性意味着你可能把”后写的值”排到”先写的值”前面,然后外部一致性(external consistency)就被破坏了。

物理时钟是分布式系统里最大的基础设施谎言之一:每台机器都有一个看起来在走的时钟,操作系统提供了 gettimeofday() 这样方便的接口,但没有任何接口会告诉你”这个时间可能偏差 5 毫秒”。本文从石英振荡器的物理特性开始,逐层拆解这个谎言的工程细节。

一、石英振荡器与时钟漂移

服务器时钟怎么走

一台服务器的系统时钟最终依赖一个硬件振荡器(oscillator)。绝大多数服务器使用石英晶体振荡器(quartz crystal oscillator),工作原理是石英晶体的压电效应(piezoelectric effect):对晶体施加电场,晶体以固有频率振动;硬件计数器对振动次数计数,操作系统从计数器推算时间。

问题在于石英晶体的振动频率不是常数。它受温度、电压、老化、机械应力等因素影响。这种频率偏差叫做时钟漂移(clock drift),通常用 ppm(parts per million,百万分之一)度量。

漂移量级

典型的服务器级石英振荡器的漂移率在 10-100 ppm 之间。换算一下:

漂移率 每秒偏差 每分钟偏差 每小时偏差 每天偏差
1 ppm 1 us 60 us 3.6 ms 86.4 ms
10 ppm 10 us 600 us 36 ms 864 ms
50 ppm 50 us 3 ms 180 ms 4.32 s
100 ppm 100 us 6 ms 360 ms 8.64 s

100 ppm 对应每天 8.64 秒。如果你的 NTP 校准间隔是 1024 秒(ntpd 默认的 maxpoll),那两次校准之间,时钟可能漂移 1024 * 100 / 1e6 = 0.1024 秒,即约 100 毫秒。对于需要毫秒级时间一致性的分布式系统来说,这个量级足以制造麻烦。

温度是主要因素

石英振荡器的频率-温度曲线近似三次函数,在”转折温度(turnover temperature)“附近变化最小。普通 AT 切割石英晶体在 25°C 附近最稳定,偏离 25°C 每度大约产生 0.035 ppm 的变化。数据中心的温度通常控制在 20-25°C,但靠近散热出口的机器和角落里的机器温差可以到 5-10°C,对应额外的 0.2-0.35 ppm 漂移。

这意味着同一个机架里的两台机器,仅仅因为物理位置不同,它们的时钟就在以不同的速率漂移。

TCXO 和 OCXO

为了降低温度敏感性,存在两种改进型振荡器:

普通服务器不用 TCXO 或 OCXO——成本上不划算。所以服务器时钟的漂移是一个必须用软件协议(NTP/PTP)来持续修正的工程事实。

二、NTP 协议:网络时间同步的工业标准

基本问题

NTP(Network Time Protocol,网络时间协议)解决的核心问题是:如何通过一个延迟不确定的网络,把远程参考时钟的时间同步到本地?

David Mills 从 1985 年开始设计 NTP,最新版本是 NTPv4(RFC 5905, 2010 年)。NTP 至今仍然是互联网上最广泛使用的时间同步协议。

Stratum 层级

NTP 使用层级(stratum)模型组织时间源:

NTP Stratum 层级架构与时钟同步流程

Stratum 模型的关键设计意图是避免所有客户端都去查询少数几台 Stratum 1 服务器。pool.ntp.org 就是一个 Stratum 2 的志愿者服务器池,在全球有数千台服务器分担负载。

四次报文交换

NTP 客户端通过和服务器交换四个时间戳来估算网络延迟和时钟偏移。过程如下:

  1. 客户端在本地时间 t1 发送请求报文。
  2. 服务器在本地时间 t2 收到请求。
  3. 服务器在本地时间 t3 发送响应。
  4. 客户端在本地时间 t4 收到响应。

其中 t1t4 是客户端时钟读数,t2t3 是服务器时钟读数。由此可以计算:

往返延迟 (round-trip delay):  d = (t4 - t1) - (t3 - t2)
时钟偏移 (clock offset):      θ = ((t2 - t1) + (t3 - t4)) / 2
sequenceDiagram
    participant C as 客户端
    participant S as NTP 服务器

    Note over C: 记录本地时间 t1
    C->>S: NTP 请求报文(携带 t1)
    Note over S: 记录接收时间 t2
    Note over S: 处理请求
    Note over S: 记录发送时间 t3
    S-->>C: NTP 响应报文(携带 t2、t3)
    Note over C: 记录本地时间 t4
    Note over C: 计算往返延迟 d = (t4 - t1) - (t3 - t2)
    Note over C: 计算时钟偏移 theta = ((t2 - t1) + (t3 - t4)) / 2
    Note over C: 调整本地时钟:偏移 theta

上图展示了 NTP 四步同步的完整流程。客户端和服务器各记录两个时间戳(t1/t4 和 t2/t3),通过这四个值计算出往返延迟和时钟偏移。关键假设是上行延迟等于下行延迟,即 (t2 - t1) = (t4 - t3);当这个假设不成立时,theta 的估算就会产生偏差。

θ 就是客户端时钟相对于服务器时钟的估算偏差。这个公式有一个关键假设:上行网络延迟等于下行网络延迟。如果网络路径不对称——比如上行走了一条拥塞的路由,下行走了另一条——θ 的估算就会有偏差。

NTP 没有办法消除这种不对称,它只能通过多次采样、多个时间源和统计滤波来降低影响。

Marzullo 算法

当客户端配置了多个 NTP 服务器时,它需要从多个(可能有偏差甚至故障的)时间源中选出最可信的时间。NTP 使用 Marzullo 算法(Marzullo’s algorithm)的一个变体来做这件事。

Marzullo 算法的核心思想:

  1. 每个时间源报告一个区间 [offset - error, offset + error],表示”真实时间在这个范围内”。
  2. 算法找出被最多区间覆盖的那个交集。
  3. 如果有 n 个时间源,算法能容忍最多 f 个故障源(falseticker),只要 n >= 2f + 1

NTPv4 在 Marzullo 基础上增加了额外的过滤和选择逻辑:先用 clock filter algorithm 从每个服务器的最近 8 个采样中选出最佳样本(延迟最小的),再用 clock selection algorithm(包含 Marzullo 算法)从多个服务器中选出 truechimer(可信源)并剔除 falseticker(异常源),最后用 cluster algorithm 从 truechimer 中选出统计特性最好的一组,再做加权合并。

NTP 精度的现实

在理想条件下(局域网、低延迟、稳定路径),NTP 可以达到亚毫秒精度。但在实际环境中:

环境 典型精度
同一局域网 0.1 - 1 ms
同一数据中心、跨交换机 0.5 - 5 ms
跨广域网 1 - 50 ms
经过 NAT / 防火墙 5 - 100 ms
虚拟机(未做 PV clock 优化) 1 - 100 ms

这些数字来自 NTP 社区的长期运维经验和 Mills 在 Computer Network Time Synchronization(2006/2010)中的讨论。具体值取决于网络拓扑、操作系统的时间戳精度、NTP 实现(ntpd vs chrony)等因素。

注意:NTP 报告的 offsetjitter 值不是”误差上界”。NTP 没有给你一个形式化的置信区间。你看到 offset = 0.5ms,但真实偏差可能是 2ms——因为这一轮的网络延迟恰好对称,但下一轮可能不对称。

ntpd 和 chrony

NTP 的两个主流实现是 ntpd(参考实现)和 chrony。在现代 Linux 环境中,chrony 通常是更好的选择:

Red Hat、Fedora、Ubuntu 等主流发行版已经把默认 NTP 实现从 ntpd 切换到 chrony。

三、PTP(IEEE 1588):硬件时间戳与亚微秒精度

NTP 的软件瓶颈

NTP 的时间戳是在软件层打的:操作系统内核在处理网络包时记录时间。但从网卡收到数据包到内核记录时间戳之间,会经过中断处理、驱动程序、协议栈等环节,引入微秒到毫秒级的不确定性抖动(jitter)。这个抖动是 NTP 精度的硬上限。

PTP(Precision Time Protocol,精密时间协议),即 IEEE 1588 标准,通过把时间戳打在网卡硬件上来消除这个瓶颈。

PTP 的工作原理

PTP 在概念上和 NTP 类似——也是通过多次报文交换来估算延迟和偏移——但有几个关键区别:

  1. 硬件时间戳(hardware timestamping):支持 PTP 的网卡(比如 Intel i210、Mellanox ConnectX 系列)在 PHY 层或 MAC 层直接记录数据包的发送/接收时间,精度可达纳秒级。这消除了操作系统软件栈引入的抖动。

  2. 主从架构:PTP 使用 Best Master Clock Algorithm(BMCA)在网络中选出一个 Grandmaster Clock,其他节点作为 Slave 同步到 Grandmaster。与 NTP 的多源选择不同,PTP 在同一个域内只有一个权威时间源。

  3. Delay Request-Response 机制:和 NTP 的四次交换类似,但 PTP 还支持 Peer Delay 机制(P2P 模式),可以逐跳测量链路延迟,而不是端到端测量。

  4. 透明时钟(Transparent Clock):PTP 感知的网络交换机可以记录数据包在交换机内部的驻留时间(residence time),并把这个时间写入 PTP 报文的修正字段。这样即使交换机引入了排队延迟,Slave 也能修正。

PTP 在数据中心的部署

PTP 的精度取决于整条链路是否都支持硬件时间戳。在一个完全支持 PTP 的数据中心网络中(所有交换机都是 PTP 透明时钟或边界时钟),同步精度可以达到 亚微秒(sub-microsecond) 级别,通常在 10-100 纳秒 范围。

flowchart TD
    GPS["GPS 卫星 / 原子钟<br/>(参考时间源)"]
    GM["Grandmaster Clock<br/>(PTP 主时钟,Stratum 1)"]
    BC1["边界时钟 BC-1<br/>(核心交换机层)"]
    BC2["边界时钟 BC-2<br/>(汇聚交换机层)"]
    TC1["透明时钟 TC-1<br/>(接入交换机)"]
    TC2["透明时钟 TC-2<br/>(接入交换机)"]
    S1["Slave 1<br/>(服务器,硬件时间戳网卡)"]
    S2["Slave 2<br/>(服务器,硬件时间戳网卡)"]
    S3["Slave 3<br/>(服务器,软件时间戳)"]
    S4["Slave 4<br/>(服务器,硬件时间戳网卡)"]

    GPS --> GM
    GM --> BC1
    BC1 --> BC2
    BC1 --> TC1
    BC2 --> TC2
    TC1 --> S1
    TC1 --> S2
    TC2 --> S3
    TC2 --> S4

    style GPS fill:#e8f5e9
    style GM fill:#fff3e0
    style BC1 fill:#e3f2fd
    style BC2 fill:#e3f2fd
    style TC1 fill:#f3e5f5
    style TC2 fill:#f3e5f5

上图展示了 PTP 在数据中心的典型部署拓扑。GPS 或原子钟为 Grandmaster 提供参考时间,边界时钟(Boundary Clock)在每一跳重新同步时钟,透明时钟(Transparent Clock)记录报文在交换机内部的驻留时间并写入修正字段。最终 Slave 节点结合硬件时间戳和修正字段实现亚微秒精度的同步。

边界时钟与透明时钟的工程差异

PTP 网络中的交换机有两种角色,选择哪种直接影响部署复杂度和同步精度:

边界时钟(Boundary Clock,BC) 在每一跳终结 PTP 会话,重新作为 Master 向下游发起同步。每台 BC 交换机独立维护自己的时钟状态,参与 BMCA 选举。优点是隔离了上下游的网络延迟抖动;缺点是每增加一跳,时钟误差会累积——每台 BC 自身的振荡器漂移和同步误差都会叠加到下游。

透明时钟(Transparent Clock,TC) 不终结 PTP 会话,只做一件事:测量 PTP 报文在自己内部的驻留时间(从入端口到出端口的延迟),然后把这个延迟值累加到报文的 correctionField 字段中。Slave 在收到报文后,用 correctionField 的值修正链路延迟计算,消除了交换机内部排队延迟的影响。

工程实践中的选择依据:

维度 边界时钟 透明时钟
误差累积 每跳累积,多跳后精度下降 不累积(驻留时间被修正)
配置复杂度 高(每台参与 BMCA,需配置域、优先级) 低(即插即用,无需 PTP 配置)
故障隔离 好(上游故障不直接传递) 差(依赖 Grandmaster 到 Slave 的端到端链路)
适用层级 核心/汇聚层(需要故障隔离) 接入层(跳数多,需要避免误差累积)

硬件时间戳的必要性:PTP 的精度优势完全依赖硬件时间戳。如果链路中有一台不支持硬件时间戳的交换机或服务器,该跳的时间戳精度退化到软件水平(微秒到毫秒级),抵消了其他跳的硬件精度。Intel i210、Mellanox ConnectX-5 及以上系列网卡支持 PTP 硬件时间戳;大部分廉价网卡不支持。

GPS 失联后的保持(Holdover):当 Grandmaster 的 GPS 信号丢失时,时钟精度将随时间退化。退化速率取决于 Grandmaster 的本地振荡器质量:

flowchart LR
    subgraph GPS 正常
        A1["误差 < 100ns"]
    end
    subgraph GPS 失联 0-1 小时
        A2["OCXO: 误差增长 ~1us<br/>TCXO: 误差增长 ~100us"]
    end
    subgraph GPS 失联 1-24 小时
        A3["OCXO: 误差 ~10us<br/>TCXO: 误差 ~1ms+"]
    end
    subgraph GPS 失联 >24 小时
        A4["OCXO: 误差 ~100us<br/>TCXO: 不可信"]
    end

    A1 --> A2 --> A3 --> A4

上图展示了 GPS 失联后时钟误差的增长轨迹。配备 OCXO 的 Grandmaster 可以在 GPS 失联后 24 小时内将误差控制在百微秒级别;配备 TCXO 的设备在数小时内就可能退化到毫秒级,不再满足亚微秒同步的要求。这就是为什么电信和金融场景的 Grandmaster 通常配备 OCXO 甚至铷原子钟作为保持振荡器。

但 PTP 的部署成本远高于 NTP:

目前 PTP 主要用在金融交易系统(高频交易对时间戳精度有硬性要求)、电信网络(5G 基站同步)和部分大型云厂商的内部基础设施。

NTP vs PTP 对比

维度 NTP PTP (IEEE 1588)
时间戳方式 软件(内核) 硬件(网卡 PHY/MAC)
典型精度 0.1 - 50 ms 10 - 100 ns
网络要求 普通 IP 网络 需要 PTP 感知交换机
硬件要求 PTP 网卡 + Grandmaster
部署复杂度
成本
适用场景 通用服务器 金融、电信、精密测量

四、时钟跳变:NTP 校准如何破坏你的程序

Step 校准与 Slew 校准

NTP 有两种方式修正时钟偏移:

Step 校准会让系统时间瞬间向前或向后跳变。这对依赖时间的程序逻辑意味着:

时钟向前跳

系统时间突然跳前,可能导致:

时钟向后跳

系统时间突然跳回更早的时刻,问题更严重:

真实案例:VM 暂停导致时钟跳变

虚拟机是时钟跳变的重灾区。当 hypervisor 暂停(pause)一个 VM——可能是为了热迁移(live migration)、快照、或者宿主机 CPU 过载——VM 内部的时钟会停走。VM 恢复后,系统时钟可能瞬间跳前几百毫秒甚至几秒。

KVM 上的 kvm-clock 和 Xen 上的 xen-clocksource 通过半虚拟化时钟源(paravirtual clock)减轻了这个问题,但并没有完全消除。在容器化环境中,容器共享宿主机的时钟,如果宿主机时钟跳变,所有容器同时受影响。

五、闰秒的噩梦

什么是闰秒

闰秒(leap second)是国际电信联盟(ITU)为了让 UTC(协调世界时)和地球自转保持同步而插入(或理论上删除)的一秒。由于地球自转速率不均匀且长期减速,自 1972 年以来已经插入了 27 次闰秒。

闰秒意味着一年中某一天的最后一分钟会有 61 秒:23:59:59 之后是 23:59:60,然后才是 00:00:00

2012 年 6 月 30 日:Reddit 和半个互联网宕机

2012 年 6 月 30 日 UTC 午夜,插入了一个正闰秒。接下来发生的事情在基础设施工程界广为人知:

Linux 内核中 NTP 闰秒处理的一个 bug(与 hrtimer 子系统的交互问题)导致 futexCLOCK_REALTIME 超时计算错误,大量使用 futex 的程序(包括 Java 应用、MySQL)开始疯狂自旋,CPU 使用率飙到 100%。

受影响的系统包括 Reddit、Mozilla、Foursquare、Yelp,以及大量运行 Linux 2.6.x 和 3.x 内核的服务器。修复方法是执行 date -s "$(date)" 强制重设时间,或者重启。

这个 bug 在 Linux 内核 3.4 中被修复(commit 6b43ae8a,John Stultz),但它给整个行业留下了深刻教训。

闰秒涂抹(Leap Second Smearing)

Google 在经历了多次闰秒事故后,提出了闰秒涂抹(leap smear)方案:不在某一秒突然插入额外一秒,而是在闰秒前后的一段时间窗口内(Google 使用 24 小时),把这额外一秒均匀地”涂抹”到每一秒上,让每一秒略微变长。

具体来说,Google 的 24 小时线性涂抹方案中,闰秒当天的每一秒会比标准秒长 1/86400 ≈ 11.57 微秒。对绝大多数应用来说,这个偏差可以忽略。

time.google.com 提供的就是涂抹后的时间。AWS 也使用类似的涂抹方案(通过 Amazon Time Sync Service)。

但闰秒涂抹引入了一个新问题:在涂抹窗口内,使用涂抹时间的服务器和使用标准 UTC 的服务器之间会有偏差。如果你的系统跨越了这两类时间源,闰秒期间的时间一致性会更差。

闰秒的未来

2022 年 11 月,第 27 届国际计量大会(CGPM)决议在 2035 年前废除闰秒。这意味着未来 UTC 和 UT1(基于地球自转的时间标准)之间的差异将被允许累积,不再通过闰秒修正。这个决议消除了未来的闰秒风险,但在 2035 年之前,闰秒仍然可能被插入。

六、Spanner TrueTime 与 AWS Clockbound

TrueTime:用硬件换取时间置信区间

Google Spanner(Corbett et al., 2012, “Spanner: Google’s Globally-Distributed Database”)的核心创新之一是 TrueTime API。它不返回一个时间点,而是返回一个区间:

TT.now() -> TTinterval { earliest, latest }

这个区间的语义是:调用时刻的真实物理时间一定落在 [earliest, latest] 之间。区间宽度 ε = latest - earliest 就是时间不确定性(uncertainty)。

TrueTime 的不确定性来自两层: 1. 本地振荡器的漂移:GPS/原子钟上次校准后到现在的漂移量。 2. 通信延迟:从时间主服务器(time master)到本地的通信延迟。

Spanner 论文报告的 ε 通常在 1-7 毫秒之间,平均约 4 毫秒。在 GPS 天线遮挡或原子钟校准周期较长时,ε 会上升。

TrueTime 的硬件基础

每个 Google 数据中心部署了多台 time master 服务器,分为两类:

每台应用服务器上运行一个 timeslave 守护进程(daemon),定期从多台 time master 同步时间,并使用类似 Marzullo 的算法选择可信源、估算不确定性区间。

Commit-Wait

TrueTime 本身只是一个时间 API。Spanner 利用它实现外部一致性(external consistency)的方式叫做 commit-wait:

  1. 事务准备提交时,获取 TT.now() 的返回值 [earliest, latest],选择 s = latest 作为提交时间戳。
  2. 等待直到 TT.now().earliest > s——也就是等到确定真实时间已经越过了 s
  3. 然后才真正提交。

等待时间约为 (两倍不确定性),平均约 7-8 毫秒。这个等待保证了:如果事务 T1 在事务 T2 开始之前提交,则 T1 的提交时间戳一定小于 T2 的提交时间戳。这就是外部一致性。

代价很明显:每次写事务都要等 7-8 毫秒。对于读写混合的 OLTP 工作负载,这个代价在多数场景下可以接受。但如果你的事务吞吐量需求非常高且延迟敏感,这个等待就成了瓶颈。

Clockbound:AWS 的开源替代

2021 年,AWS 开源了 Clockbound(GitHub: aws/clock-bound),这是一个类似 TrueTime 的时钟置信区间实现,但不依赖专用硬件。

Clockbound 的工作方式:

  1. ClockBound daemon 运行在每台 EC2 实例上,通过 chrony 与 Amazon Time Sync Service 同步。
  2. daemon 维护一个时钟误差模型(clock error bound),综合考虑:
    • chrony 报告的网络同步误差
    • 本地石英振荡器的漂移率上界
    • 上次同步后经过的时间
  3. 应用程序通过 Unix domain socket 查询 ClockBound daemon,获得类似 TrueTime 的区间:[earliest, latest]

与 TrueTime 的关键区别: - TrueTime 依赖 GPS + 原子钟,不确定性通常 1-7 ms。 - Clockbound 依赖 NTP(Amazon Time Sync),不确定性通常在几毫秒到几十毫秒。 - Clockbound 是开源的,可以在任何 Linux 系统上运行。 - Clockbound 不保证和 TrueTime 一样小的不确定性窗口。

Clockbound 目前提供 Rust 和 C 的客户端库。如果你在 AWS 上运行分布式数据库,并且需要时钟置信区间来实现类似 Spanner 的外部一致性,Clockbound 是一个可行的开源起点——但你需要验证在你的环境中,不确定性窗口是否足够小。

这部分内容在本百科后续的 Spanner 深度解析 中会结合事务协议进一步展开。

七、度量本地时钟漂移

理论讲完了,来看一个实际的问题:你怎么知道自己服务器上的时钟在以什么速率漂移?

下面这个 Go 程序通过对比 Go 运行时提供的单调时钟(monotonic clock)和墙上时钟(wall clock)在一段时间内的差异,来观察操作系统是否对墙上时钟做了调整(NTP slew 或 step)。这不是直接测量石英振荡器的漂移率——要做到那个,需要一个外部参考时钟——但它可以让你看到 NTP 调整对墙上时钟的实际影响。

package main

import (
    "fmt"
    "time"
)

// measureClockDrift 对比单调时钟与墙上时钟的增量差异。
// 如果 NTP 在观察期间做了 slew 调整,两者的增量会不同。
// 返回墙上时钟相对于单调时钟的偏移(正值表示墙上时钟被加快)。
func measureClockDrift(interval time.Duration) (wallDelta, monoDelta time.Duration, drift time.Duration) {
    // time.Now() 在 Go 1.9+ 中同时包含 wall clock 和 monotonic clock 读数
    start := time.Now()
    time.Sleep(interval)
    end := time.Now()

    // Sub 使用单调时钟计算差值(如果两个 Time 都包含单调读数)
    monoDelta = end.Sub(start)

    // 手动从 wall clock 字段计算差值
    wallDelta = end.Truncate(0).Sub(start.Truncate(0))
    // Truncate(0) 剥离单调时钟读数,强制 Sub 使用 wall clock

    drift = wallDelta - monoDelta
    return
}

func main() {
    fmt.Println("开始测量时钟漂移(单调时钟 vs 墙上时钟)")
    fmt.Println("每 10 秒采样一次,共采样 6 次")
    fmt.Println("如果 NTP 正在做 slew 调整,drift 不会是 0")
    fmt.Println()

    for i := 0; i < 6; i++ {
        wallDelta, monoDelta, drift := measureClockDrift(10 * time.Second)
        driftPPM := float64(drift.Nanoseconds()) / float64(monoDelta.Seconds()) / 1000.0
        fmt.Printf("采样 %d: wall=%v  mono=%v  drift=%v  (%.3f ppm)\n",
            i+1, wallDelta, monoDelta, drift, driftPPM)
    }
}

程序说明:

运行环境:Go 1.21+,Linux。在 macOS 上同样有效,但单调时钟的实现细节略有不同。

八、工程实践:单调时钟、墙上时钟与超时

两种时钟

操作系统提供两种时钟,分布式系统工程师必须区分它们:

特性 墙上时钟(Wall Clock) 单调时钟(Monotonic Clock)
Linux 系统调用 clock_gettime(CLOCK_REALTIME) clock_gettime(CLOCK_MONOTONIC)
是否受 NTP 调整 是(step 和 slew 都会影响) 否(不受 NTP step 影响)
是否受闰秒影响
是否单调递增 不保证(可能跳回) 保证(同一进程内只增不减)
跨机器可比 有意义(虽然不精确) 无意义(每台机器的零点不同)
用途 显示时间、日志时间戳、持久化时间 度量经过的时间、超时、性能计量

规则

基于以上分析,以下几条规则适用于任何需要处理时间的分布式系统代码:

规则 1:计算超时和经过时间,用单调时钟。

不要用 gettimeofday()System.currentTimeMillis() 来计算”还剩多少时间”。用 clock_gettime(CLOCK_MONOTONIC) / Go 的 time.Since() / Java 的 System.nanoTime() / Rust 的 Instant::now()

规则 2:记录”事件发生在什么时候”给人看,用墙上时钟。

日志时间戳、审计记录、用户界面显示的时间,这些场景需要墙上时钟,因为人类需要的是”几点几分”,不是”距离系统启动 37284.229 秒”。

规则 3:跨机器比较时间戳,要考虑时钟误差。

如果你的系统需要跨机器比较事件的先后顺序(比如 LWW 冲突解决),直接比较墙上时钟的时间戳是不可靠的。你要么: - 接受一定窗口内的不确定性(like Spanner’s commit-wait) - 使用逻辑时钟或混合时钟(参见 逻辑时钟) - 使用因果一致性模型,不依赖全局时间排序

规则 4:不要假设时间戳单调递增。

即使你只在单台机器上生成时间戳,NTP step 校准也可能让墙上时钟回跳。如果你用时间戳做 ID 或排序键,必须处理”时间戳相等或倒退”的情况。常见做法:在时间戳后附加逻辑计数器,保证即使物理时间相同或回退,组合键仍然递增。这正是混合逻辑时钟(HLC,Hybrid Logical Clock)的核心思想。

规则 5:监控 NTP 偏移和同步状态。

如果你的服务依赖物理时钟精度(数据库事务排序、分布式锁续期、证书验证),你应该: - 监控 chronyc trackingntpq -p 的输出 - 对 offsetroot dispersion 设告警阈值 - 在时钟偏移超过阈值时让节点退出服务,而不是继续运行带着错误时钟

# chrony: 检查当前同步状态
chronyc tracking

# 关键输出字段:
# System time     : 0.000002345 seconds fast of NTP time
# Last offset     : +0.000001234 seconds
# RMS offset      : 0.000003456 seconds
# Root delay      : 0.001234567 seconds
# Root dispersion : 0.000456789 seconds
# Leap status     : Normal

规则 6:超时逻辑要容忍时钟跳变。

在选举超时(election timeout)、心跳间隔(heartbeat interval)、租约续期(lease renewal)这类对系统正确性有影响的超时逻辑中: - 使用单调时钟 - 设置合理的上下界——如果检测到两次心跳之间经过的时间不合理(比如负数或远超预期),应该当作异常处理,而不是盲目信任时钟

Raft 论文中的选举超时就依赖于本地时钟的单调性。如果时钟跳变导致超时计算错误,可能引发不必要的 leader 选举,影响可用性。

九、总结与边界

物理时钟在分布式系统中的地位很矛盾:每个节点都有时钟,操作系统也提供了方便的 API,但没有任何 API 能告诉你”这个时间有多准”。石英振荡器的漂移、NTP 的网络对称性假设、闰秒的内核 bug、虚拟机的时钟暂停——每一层都可能引入偏差,而且这些偏差对上层应用不透明。

核心收获:

  1. 石英振荡器漂移 10-100 ppm 是普遍事实,两次 NTP 校准之间的漂移可达毫秒级。
  2. NTP 提供毫秒级精度,但没有给出形式化的误差上界;其精度取决于网络路径的对称性。
  3. PTP 用硬件时间戳实现亚微秒精度,但需要全链路 PTP 感知设备,成本和复杂度高。
  4. NTP step 校准会让墙上时钟跳变,可以破坏超时逻辑、时间戳排序和证书验证。
  5. 闰秒触发过真实的大规模故障,闰秒涂抹是当前的工业实践。
  6. TrueTime 和 Clockbound 提供时钟置信区间,让上层可以对不确定性做显式处理——但需要对应的硬件或时间同步基础设施。
  7. 工程规则:超时用单调时钟,显示用墙上时钟,跨机器比较时间戳要考虑误差

物理时钟的局限直接催生了逻辑时钟的研究。下一篇 逻辑时钟 将讨论 Lamport 时钟和向量时钟如何在不依赖物理时钟精度的前提下追踪因果关系——以及它们自身的代价。


参考资料

规范与标准

论文与书籍

工程实现与事故


上一篇:复杂性度量 | 下一篇:逻辑时钟

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-13 · distributed

【分布式系统百科】混合逻辑时钟与 TrueTime:在物理和逻辑之间找到平衡

物理时钟对不齐,逻辑时钟丢物理信息,向量时钟太重。HLC 用物理时间 + 逻辑计数器找到了平衡。但 Google 选了另一条路:用原子钟和 GPS 把物理误差压到几毫秒。这篇文章从 HLC 的算法正确性证明、CockroachDB 源码实现、TrueTime 工程架构,一直讲到 AWS Clockbound 的开源方案——在物理和逻辑之间,每种选择都是一笔工程账。

2026-04-13 · distributed-systems

【分布式系统百科】共识问题的精确定义:Agreement、Validity、Termination

共识到底在解决什么问题?Agreement、Validity、Termination 三个性质的精确含义是什么?Safety 和 Liveness 的区分为什么如此关键?FLP 不可能定理对工程实践意味着什么?本文从形式化定义出发,逐步展开共识的变体、原子广播的等价性,以及状态机复制这个最重要的应用。


By .