23.1 物理时钟与漂移
一句话
物理时钟会漂、会跳、也不可跨机比较:测耗时要用单调时钟,定顺序要用逻辑时钟(或 HLC/TrueTime)——墙钟只适合 “给人看的时间戳”。
两种时钟,两种用途
| 时钟 | 会回拨吗 | 会跳变吗 | 适用 |
|---|---|---|---|
墙钟(CLOCK_REALTIME) | 会(NTP 校正、手动改) | 会 | 展示、日志时间戳 |
单调时钟(CLOCK_MONOTONIC) | 不会 | 会随频率调整缓慢偏移 | 测量耗时、超时、租约 |
| 逻辑时钟(Lamport) | 不会 | — | 定因果顺序(见 23.3 Lamport 逻辑时钟) |
| 混合逻辑时钟(HLC) | 不会 | — | 既近物理时间又可定序 |
漂移:量级决定风险
| 振荡器 | 漂移 | 每天误差 |
|---|---|---|
| 普通晶振 | 10–100 ppm | 0.86–8.6 秒 |
| 温补晶振(TCXO) | 1–5 ppm | 0.09–0.43 秒 |
| 原子钟/GPS | < 0.001 ppm | 毫秒级 |
公网 NTP 的同步精度通常在 1–10ms(受网络抖动影响),局域网可到亚毫秒。这意味着 “跨机比较毫秒级时间戳” 天然不可靠。
| 用 time.Now() 做去重键 | 时钟回拨导致键重复/失效 |
| 跨机比较 created_at 判断先后 | 漂移让“旧写”覆盖“新写” |
三个经典事故
| 事故 | 原因 | 正确做法 |
|---|---|---|
| 耗时算出负数 | NTP 回拨墙钟,end - start < 0 | 用单调时钟测耗时 |
| 后写的数据被 “更早” 的时间戳覆盖 | 跨机时间戳比较 | 用逻辑时钟/版本号定序 |
| 租约提前失效 / 双主 | 时钟漂移超出余量 | 租约留足漂移余量(如 TTL 的 10%) |
怎么补救
| 手段 | 精度/性质 | 代价 |
|---|---|---|
| NTP/chrony | 1–10ms(公网) | 依赖网络,仍会跳 |
| PTP | 亚微秒(需硬件支持) | 部署成本高 |
| HLC | 接近物理时间 + 因果单调 | 需要携带 (物理, 逻辑) 对 |
| TrueTime(Spanner) | 给出不确定区间 [earliest, latest] | 需要原子钟 + GPS,等待区间收敛 |
TrueTime 的精髓不是 “更准”,而是 “承认不准并给出误差界”:事务提交时等待误差窗口过去,从而保证外部一致性。
代码:别把墙钟当秒表
// 错:墙钟受 NTP 校正,可能回拨 → 耗时负数
start := time.Now()
doWork()
elapsed := time.Since(start) // 内部用单调钟,这里其实是对的
// 若你手写 end.Sub(start):time.Time 的 Sub 也基于单调读数,
// 但一旦时间来自“反序列化/另一台机器”,就没有单调分量了 —— 这才是坑
elapsedBad := parsedEnd.Sub(parsedStart) // 跨机/跨进程存储的时间戳:不可信
-- 错:用数据库时间戳判断“谁先写”
UPDATE t SET v=?, updated_at=now() WHERE id=?; -- 多实例时钟不同步
-- 对:用版本号/序号做先后判断
UPDATE t SET v=?, ver=ver+1 WHERE id=? AND ver=?;
反模式
| 反模式 | 后果 |
|---|---|
| 用墙钟测耗时 | NTP 回拨导致负数/极值,监控失真 |
| 用时间戳做 “谁更新” 的判断 | 时钟漂移导致旧覆盖新 |
| 假设各机时钟一致 | 分布式锁/租约在漂移下失效 |
| 只依赖 NTP 且不留余量 | 闰秒、校时抖动引发连锁故障 |
| 用时间戳做去重键 | 时钟回拨导致键重复或失效 |
| 跨机比较 created_at 判断先后 | 漂移让“旧写”覆盖“新写” |
三条铁律
🔑 耗时用单调时钟,顺序用逻辑时钟。
🔑 跨机时间戳只能当 “估算”;不能作为正确性依据。
🔑 租约/超时必须为漂移留余量(通常 ≥ 10%)。
自检
Q1 为什么 time.Now() 算出的耗时可能为负?
▸ 答案:因为它读的是墙钟,NTP 校时或人工改时间会让它回拨;end 可能小于 start。用 time.Since()(内部基于单调时钟)或 CLOCK_MONOTONIC 才能得到单调的耗时。
Q2 两台机器的时间戳相差 5ms,能否用它判断 “哪个写更晚”?
▸ 答案:不能可靠判断。5ms 在 NTP 精度范围内,也可能被漂移吃掉;真正的先后应由逻辑时钟或版本号决定。时间戳只能用于 “展示与粗略排序”。
Q3 HLC 相比纯 Lamport 时钟好在哪?
▸ 答案:HLC 携带 物理分量 + 逻辑分量:物理部分让它接近真实时间(便于运维与范围查询),逻辑部分保证因果单调(不受漂移影响)。它在 “需要时间感又要定序” 的数据库里很常见。
Q4 TrueTime 为什么要 “等误差窗口”?
▸ 答案:因为它的 API 只给 “时间区间” 而非点值。事务提交时等到 latest 过去,才能断言 “我的提交时间一定早于下一个事务的开始”,从而实现外部一致性。代价是提交延迟(通常几毫秒)。
Q5 租约 TTL 为什么要留 10% 余量?
▸ 答案:因为持有者与授予者的时钟都会漂移,且网络有延迟:持有者认为 “还有 1 秒”,授予者可能已经认为 “过期”。留余量(如 TTL=10s,实际安全期按 9s 算)能避免 “双主同时认为自己有效”。
Q7 为什么 “把时间戳存进数据库再比较” 会出错,而 “存进内存比较” 不会?
▸ 答案:因为内存里的 time.Time 可能带单调时钟分量(同进程内比较可靠);一旦序列化到数据库或跨进程,就只剩墙钟读数,回调/漂移都会让它失去单调性。所以跨系统比较必须用逻辑序号,而不是时间戳。
关联
- 前置:23.6 happened-before 关系
- 并列:23.2 NTP 与 TrueTime、23.5 混合逻辑时钟 (HLC)
- 下游:21.11 领导者租约、23.3 Lamport 逻辑时钟