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(Temperature-Compensated Crystal Oscillator,温度补偿晶振):通过电路补偿温度引起的频率变化,精度可达 0.5-2 ppm。成本比普通晶振高数倍,常见于网络设备。
- OCXO(Oven-Controlled Crystal Oscillator,恒温晶振):把晶体放在恒温槽里,精度可达 0.01-0.1 ppm。成本更高,功耗更大,通常用于电信基站和精密测量设备。
普通服务器不用 TCXO 或 OCXO——成本上不划算。所以服务器时钟的漂移是一个必须用软件协议(NTP/PTP)来持续修正的工程事实。
二、NTP 协议:网络时间同步的工业标准
基本问题
NTP(Network Time Protocol,网络时间协议)解决的核心问题是:如何通过一个延迟不确定的网络,把远程参考时钟的时间同步到本地?
David Mills 从 1985 年开始设计 NTP,最新版本是 NTPv4(RFC 5905, 2010 年)。NTP 至今仍然是互联网上最广泛使用的时间同步协议。
Stratum 层级
NTP 使用层级(stratum)模型组织时间源:
- Stratum 0:参考时钟(reference clock)。GPS 卫星、铯原子钟、铷原子钟、无线电授时信号(如 DCF77)等。这些不是网络设备,它们通过 PPS(Pulse Per Second,秒脉冲)信号或串口连接到 Stratum 1 服务器。
- Stratum
1:直接连接参考时钟的服务器,也叫”一级时间服务器”。例如
time.nist.gov、time.google.com。 - Stratum 2:从 Stratum 1 同步的服务器。大型企业和 ISP 通常运行自己的 Stratum 2 服务器。
- Stratum 3 及以下:从上一层同步,层级每增加一层,精度理论上下降。NTP 最多支持到 Stratum 15,Stratum 16 表示”未同步”。
Stratum
模型的关键设计意图是避免所有客户端都去查询少数几台 Stratum 1
服务器。pool.ntp.org 就是一个 Stratum 2
的志愿者服务器池,在全球有数千台服务器分担负载。
四次报文交换
NTP 客户端通过和服务器交换四个时间戳来估算网络延迟和时钟偏移。过程如下:
- 客户端在本地时间
t1发送请求报文。 - 服务器在本地时间
t2收到请求。 - 服务器在本地时间
t3发送响应。 - 客户端在本地时间
t4收到响应。
其中 t1 和 t4
是客户端时钟读数,t2 和 t3
是服务器时钟读数。由此可以计算:
往返延迟 (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 算法的核心思想:
- 每个时间源报告一个区间
[offset - error, offset + error],表示”真实时间在这个范围内”。 - 算法找出被最多区间覆盖的那个交集。
- 如果有
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 报告的 offset 和
jitter 值不是”误差上界”。NTP
没有给你一个形式化的置信区间。你看到
offset = 0.5ms,但真实偏差可能是
2ms——因为这一轮的网络延迟恰好对称,但下一轮可能不对称。
ntpd 和 chrony
NTP 的两个主流实现是 ntpd(参考实现)和 chrony。在现代 Linux 环境中,chrony 通常是更好的选择:
- chrony 收敛更快:从大偏移恢复到稳定状态,chrony 通常只需要几分钟,ntpd 可能需要几十分钟甚至更久。
- chrony 对间歇性网络更友好:笔记本、虚拟机这类网络不稳定的环境,chrony 表现更好。
- chrony 支持硬件时间戳:在有 PTP 硬件支持的网卡上,chrony 可以利用硬件时间戳提高精度。
Red Hat、Fedora、Ubuntu 等主流发行版已经把默认 NTP 实现从 ntpd 切换到 chrony。
三、PTP(IEEE 1588):硬件时间戳与亚微秒精度
NTP 的软件瓶颈
NTP 的时间戳是在软件层打的:操作系统内核在处理网络包时记录时间。但从网卡收到数据包到内核记录时间戳之间,会经过中断处理、驱动程序、协议栈等环节,引入微秒到毫秒级的不确定性抖动(jitter)。这个抖动是 NTP 精度的硬上限。
PTP(Precision Time Protocol,精密时间协议),即 IEEE 1588 标准,通过把时间戳打在网卡硬件上来消除这个瓶颈。
PTP 的工作原理
PTP 在概念上和 NTP 类似——也是通过多次报文交换来估算延迟和偏移——但有几个关键区别:
硬件时间戳(hardware timestamping):支持 PTP 的网卡(比如 Intel i210、Mellanox ConnectX 系列)在 PHY 层或 MAC 层直接记录数据包的发送/接收时间,精度可达纳秒级。这消除了操作系统软件栈引入的抖动。
主从架构:PTP 使用 Best Master Clock Algorithm(BMCA)在网络中选出一个 Grandmaster Clock,其他节点作为 Slave 同步到 Grandmaster。与 NTP 的多源选择不同,PTP 在同一个域内只有一个权威时间源。
Delay Request-Response 机制:和 NTP 的四次交换类似,但 PTP 还支持 Peer Delay 机制(P2P 模式),可以逐跳测量链路延迟,而不是端到端测量。
透明时钟(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 透明时钟或边界时钟
- 需要 Grandmaster Clock 设备(通常带 GPS 接收器或原子钟)
- 运维复杂度高:BMCA 选举、域配置、故障切换
目前 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 有两种方式修正时钟偏移:
- Slew(渐进调整):通过微调时钟频率(加快或减慢振荡器的计数速率),让时钟”渐渐”追上或等待正确时间。Linux 上的默认 slew 速率约 500 ppm,即每秒最多调整 0.5 毫秒。修正 1 秒的偏差需要约 2000 秒(33 分钟)。
- Step(跳变校准):直接把系统时间跳到正确值。ntpd 默认在偏差超过 128 毫秒时使用 step 校准。
Step 校准会让系统时间瞬间向前或向后跳变。这对依赖时间的程序逻辑意味着:
时钟向前跳
系统时间突然跳前,可能导致:
- 超时提前触发:一个设置了 5 秒超时的 RPC 调用,如果时钟向前跳了 3 秒,剩余超时变成 2 秒——如果这个 RPC 使用的是墙上时钟(wall clock)计算的截止时间。
- 定时器批量到期:cron job、延迟队列、TTL 过期检查可能在同一瞬间大量触发。
- 日志时间戳跳跃:日志分析和监控系统看到一个时间间隙,可能触发误报。
时钟向后跳
系统时间突然跳回更早的时刻,问题更严重:
- 时间戳倒退:如果你的系统用
time.Now()给事件生成 ID 或排序键,时钟回跳后生成的 ID 会比之前的小。这可以破坏任何假设”时间戳单调递增”的逻辑。 - 超时永不到期:用”目标时间 = 当前时间 + 超时间隔”方式设置的超时,如果当前时间跳回了,目标时间会变成”未来很远”,超时不会触发。
- 证书/Token 验证失败:TLS 证书的
notBefore检查、JWT 的nbf/exp检查都依赖墙上时钟。时钟跳回可能让一个有效证书突然变成”尚未生效”。 - 数据库事务排序错误:Last-Write-Wins(LWW)策略下,时钟回跳后写入的值会被认为”更早”而被覆盖。
真实案例: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 子系统的交互问题)导致
futex 的 CLOCK_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 服务器,分为两类:
- GPS master:配备 GPS 接收器,从 GPS 卫星获取 UTC 时间。GPS 本身的精度在纳秒级。
- Armageddon master:配备铯原子钟(或铷原子钟),作为 GPS 信号中断时的备份。“Armageddon” 这个名字暗示即使 GPS 卫星系统出了问题(或被干扰),这些服务器依然能提供准确时间。
每台应用服务器上运行一个 timeslave 守护进程(daemon),定期从多台 time master 同步时间,并使用类似 Marzullo 的算法选择可信源、估算不确定性区间。
Commit-Wait
TrueTime 本身只是一个时间 API。Spanner 利用它实现外部一致性(external consistency)的方式叫做 commit-wait:
- 事务准备提交时,获取
TT.now()的返回值[earliest, latest],选择s = latest作为提交时间戳。 - 等待直到
TT.now().earliest > s——也就是等到确定真实时间已经越过了s。 - 然后才真正提交。
等待时间约为 2ε(两倍不确定性),平均约 7-8
毫秒。这个等待保证了:如果事务 T1 在事务 T2 开始之前提交,则
T1 的提交时间戳一定小于 T2
的提交时间戳。这就是外部一致性。
代价很明显:每次写事务都要等 7-8 毫秒。对于读写混合的 OLTP 工作负载,这个代价在多数场景下可以接受。但如果你的事务吞吐量需求非常高且延迟敏感,这个等待就成了瓶颈。
Clockbound:AWS 的开源替代
2021 年,AWS 开源了 Clockbound(GitHub:
aws/clock-bound),这是一个类似 TrueTime
的时钟置信区间实现,但不依赖专用硬件。
Clockbound 的工作方式:
- ClockBound daemon 运行在每台 EC2 实例上,通过 chrony 与 Amazon Time Sync Service 同步。
- daemon 维护一个时钟误差模型(clock error
bound),综合考虑:
- chrony 报告的网络同步误差
- 本地石英振荡器的漂移率上界
- 上次同步后经过的时间
- 应用程序通过 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.9 开始,
time.Now()同时记录墙上时钟和单调时钟。time.Time.Sub()在两个Time值都有单调读数时使用单调时钟做减法。 Truncate(0)是 Go 标准库文档中推荐的剥离单调读数的方法,强制后续Sub使用墙上时钟。- 如果 NTP 正在做 slew
调整(加快或减慢墙上时钟频率),
wallDelta和monoDelta之间会出现差异。 - 如果 NTP 做了 step 校准(时钟跳变),你会看到一个很大的 drift 值。
运行环境: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 tracking 或 ntpq -p
的输出 - 对 offset 和
root 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、虚拟机的时钟暂停——每一层都可能引入偏差,而且这些偏差对上层应用不透明。
核心收获:
- 石英振荡器漂移 10-100 ppm 是普遍事实,两次 NTP 校准之间的漂移可达毫秒级。
- NTP 提供毫秒级精度,但没有给出形式化的误差上界;其精度取决于网络路径的对称性。
- PTP 用硬件时间戳实现亚微秒精度,但需要全链路 PTP 感知设备,成本和复杂度高。
- NTP step 校准会让墙上时钟跳变,可以破坏超时逻辑、时间戳排序和证书验证。
- 闰秒触发过真实的大规模故障,闰秒涂抹是当前的工业实践。
- TrueTime 和 Clockbound 提供时钟置信区间,让上层可以对不确定性做显式处理——但需要对应的硬件或时间同步基础设施。
- 工程规则:超时用单调时钟,显示用墙上时钟,跨机器比较时间戳要考虑误差。
物理时钟的局限直接催生了逻辑时钟的研究。下一篇 逻辑时钟 将讨论 Lamport 时钟和向量时钟如何在不依赖物理时钟精度的前提下追踪因果关系——以及它们自身的代价。
参考资料
规范与标准
- RFC 5905: “Network Time Protocol Version 4: Protocol and Algorithms Specification”, Mills et al., 2010
- IEEE 1588-2019: “Standard for a Precision Clock Synchronization Protocol for Networked Measurement and Control Systems”
- ITU-R TF.460-6: “Standard-frequency and time-signal emissions”
论文与书籍
- Mills, David L. Computer Network Time Synchronization: The Network Time Protocol on Earth and in Space, 2nd ed., CRC Press, 2010 — NTP 的权威参考,涵盖 Marzullo 算法、时钟滤波、选择算法的完整描述
- Corbett, James C., et al. “Spanner: Google’s Globally-Distributed Database.” ACM Transactions on Computer Systems 31.3 (2013) — TrueTime API 和 commit-wait 的原始论文
- Anceaume, Emmanuelle, and Bustos-Jiménez, Javier, and Gambs, Sébastien. “Clock Synchronization in Distributed Systems.” 2006 — 分布式时钟同步的形式化分析
- Lamport, Leslie. “Time, Clocks, and the Ordering of Events in a Distributed System.” Communications of the ACM 21.7 (1978) — 逻辑时钟的开创性论文
- Marzullo, Keith. “Maintaining the Time in a Distributed System.” PhD thesis, Stanford University, 1984 — Marzullo 算法的原始来源
工程实现与事故
- chrony 项目文档: https://chrony-project.org/documentation.html
- AWS Clockbound: https://github.com/aws/clock-bound — 开源时钟置信区间实现
- John Stultz, Linux kernel commit
6b43ae8a, “ntp: Fix leap-second hrtimer livelock” — 修复 2012 年闰秒 bug - Google Research Blog: “Leap Smear” — Google 闰秒涂抹方案说明
- CGPM 27th Meeting Resolution 4 (2022): “On the use and future development of UTC” — 废除闰秒的决议
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【分布式系统百科】混合逻辑时钟与 TrueTime:在物理和逻辑之间找到平衡
物理时钟对不齐,逻辑时钟丢物理信息,向量时钟太重。HLC 用物理时间 + 逻辑计数器找到了平衡。但 Google 选了另一条路:用原子钟和 GPS 把物理误差压到几毫秒。这篇文章从 HLC 的算法正确性证明、CockroachDB 源码实现、TrueTime 工程架构,一直讲到 AWS Clockbound 的开源方案——在物理和逻辑之间,每种选择都是一笔工程账。
【分布式系统百科】Paxos:从 Single-Decree 到 Multi-Paxos 的工程之路
Paxos 是分布式共识的理论基石。本文从 Single-Decree Paxos 的精确语义和安全性证明出发,逐步推导 Multi-Paxos 的工程优化,分析 Dueling Proposers、性能瓶颈和实现困难,最后给出一份可运行的 Go 实现。
【分布式系统百科】共识问题的精确定义:Agreement、Validity、Termination
共识到底在解决什么问题?Agreement、Validity、Termination 三个性质的精确含义是什么?Safety 和 Liveness 的区分为什么如此关键?FLP 不可能定理对工程实践意味着什么?本文从形式化定义出发,逐步展开共识的变体、原子广播的等价性,以及状态机复制这个最重要的应用。
【分布式系统实战】混合时钟与因果一致性:Lamport → Vector → HLC
分布式系统里最难的问题之一:如何定义事件的先后?物理时钟对不齐,逻辑时钟丢信息,向量时钟太重。HLC 用物理时间 + 逻辑计数器找到了平衡点。从 Lamport 时钟一路推导到 CockroachDB 的工程实现。