连载中 6/16

时钟回拨:雪花的第一死穴

2026-06-08 · 5373 阅读 · 0 评论 · 0 赞

一次校时引发的撞号风暴

雪花算法上线三个月风平浪静,直到运维做了一次机房时钟治理:统一 NTP 校时,20 台发号节点里有几台时钟快了几百毫秒,被同时拨了回去。几分钟后,撞号告警响成一片——两个不同的请求,拿到了同一个 ID。老王对着雪花结构看了半小时才反应过来:这不是算法 bug,这是它第一死穴「时钟回拨」的发病现场。

时钟不是恒定向前的

先破除一个错觉:服务器时钟不会自动精准。现实里时钟会飘(晶振误差每天快慢几十毫秒很常见),于是运维靠 NTP 定期校时。而 NTP 校时是双向的——慢了往前拨,快了往回拨。除此之外,回拨还有好几个来源:虚机热迁移(宿主机切换时时钟跳变)、闰秒(分钟级回拨的极端案例)、运维手动调整、双机时钟漂移后强行对齐。

雪花算法的时间戳直接取自系统时钟,而时钟可能倒着走——算法的根基出现在了不稳定的地基上

回拨为什么能造重复 ID

把唯一性的三维摆出来看:时间戳、机器号、序列号。机器号没变,重复只可能发生在时间与序列的组合上。演一遍事故现场:

回拨前:时间戳 T=1000,本毫秒序列号已发到 5
        已发出的 ID:时间 1000 x 机器 7 x 序列 0..5

时钟回拨 500 毫秒,now 变回 T=1000
序列号跟着 now 重新从 0 计数

回拨后发出的 ID:时间 1000 x 机器 7 x 序列 0..5
——与前 6 个 ID 完全重合,重复诞生

看明白了吗:回拨没有破坏算法本身,它让「新的一毫秒」和「旧的一毫秒」长得一模一样。序列号空间是每毫秒独立的计数器,时间一倒带,计数器也倒带,历史号被原样重发。机器号这时救不了场——同一台机器、同一段历史。

更麻烦的是重复号会污染下游:订单主键冲突直接报错算走运;要是存进去了,就是第 1 篇那种「退款退错人」的资损现场。所以雪花的实现必须把回拨当成一等事故对待,而不是异常路径里的 if。

检测:一行比较就能发现

好消息是检测极其简单——生成 ID 时记着 lastTimestamp,每来一个请求先比一比:

if (now 前于 lastTimestamp) {
  差值 = lastTimestamp - now;
  触发回拨处理逻辑(等级见下篇);
}

难的不是发现,是处理策略与回拨幅度强相关:回拨 5 毫秒和回拨 5 分钟,是两种完全不同的事故。

按幅度分级的病理报告

幅度典型来源直觉应对
毫秒级(几十 ms 内)NTP 微调、时钟漂移自旋等待追上 lastTimestamp等待时间不可控就变死循环
秒级NTP 大步校时拒绝发号或换备用机器号全节点同回拨时全站拒号
分钟级以上虚机迁移、闰秒、人工调整告警人工介入,切备用方案自动等待毫无意义

那张表里最阴的一行是来源:NTP 集中校时是全节点同步动作——老王事故里 20 台节点一起回拨,意味着「换一台节点顶上」的常规容灾也失效了,全集群同时带病。运维侧的预防同样重要:NTP 校时必须分批、限速(slew 模式缓慢逼近而不是 step 一步跳回),发号节点绝不参与集中校时。

小结

时钟回拨一句话:时间倒带让「新毫秒」撞脸「旧毫秒」,三维唯一瞬间塌维;检测只需一行比较,处理却要按幅度分级。它不可根除,只能被管理——具体怎么管,下一篇把等待、拒绝、扩展位三招药方一次开齐。

503

10 年全栈工程师 · 503咖啡馆主理人

#分布式ID#时钟回拨#NTP#雪花算法#撞号

评论 (0)

相关推荐

连载中 12/20

排查四件套:jstack、jmap、jstat、jcmd 的实战分工

jstack 看线程在干什么,jmap 看堆里装了什么,jstat 看运行时在变什么,jcmd 是统一入口。四把刀各管一段,配合着用没有查不动的现场。

#jstack#jmap#jstat#jcmd#排查工具
2026-09-16 · 5 阅读 · 0 评论 · 0 赞
连载中 11/20

GC 日志:把回收过程翻译成人话

一行 GC 日志里塞着七种信息:谁触发的、收了哪、停了多久、活了哪些。加上 -Xlog 配置,再加上日志分析工具,GC 不再是只能盯监控曲线的黑盒。

#GC日志#Xlog#日志分析#GC监控#Full GC排查
2026-09-16 · 13 阅读 · 0 评论 · 0 赞
连载中 10/20

ZGC:亚毫秒停顿是怎么炼成的

百 G 大堆停顿不到一毫秒,靠的是把搬家全部挪到并发阶段——着色指针让引用自带状态,读屏障让搬运中的对象依然可访问。代价是吞吐与内存,收益是停顿与堆大小解耦。

#ZGC#着色指针#读屏障#亚毫秒停顿#分代ZGC
2026-09-15 · 8 阅读 · 0 评论 · 0 赞