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 可能带单调时钟分量(同进程内比较可靠);一旦序列化到数据库或跨进程,就只剩墙钟读数,回调/漂移都会让它失去单调性。所以跨系统比较必须用逻辑序号,而不是时间戳。

关联