一次校时引发的撞号风暴
雪花算法上线三个月风平浪静,直到运维做了一次机房时钟治理:统一 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 一步跳回),发号节点绝不参与集中校时。
小结
时钟回拨一句话:时间倒带让「新毫秒」撞脸「旧毫秒」,三维唯一瞬间塌维;检测只需一行比较,处理却要按幅度分级。它不可根除,只能被管理——具体怎么管,下一篇把等待、拒绝、扩展位三招药方一次开齐。
评论 (0)