药方开三副,按幅度抓药
第 6 篇的病理报告结论:回拨幅度决定处理等级。所以治理方案从来不是三选一,而是按幅度分级的组合拳:小回拨硬扛、中回拨绕行、大回拨止损。逐副拆。
第一招:自旋等待,硬扛毫秒级
回拨几十毫秒以内,最朴素的应对是等它自己追回来:lastTimestamp 停在回拨前的位置,自旋等到系统时钟重新走过那个点,再正常发号。等待期间发出的号全部安全,因为时间戳始终不小于历史最大值:
if (now 前于 lastTimestamp) {
差值 = lastTimestamp - now;
if (差值 小于等于 阈值) {
自旋等待 差值 毫秒后继续; // 典型阈值 100ms
} else {
升级到第二招;
}
}两个细节决定这招安全不安全:一,必须设等待上限——回拨一秒还傻等一秒,吞吐归零;超限立刻升级。二,等待用 sleep 或短自旋,别把 CPU 烧成暖宝宝;等待计数打点上报,回拨频率是时钟健康度的直接指标。
第二招:拒绝发号,把风险挡在门外
秒级以上的回拨,等待失去意义——回拨多久不可知,等下去是黑洞。正确姿势是快速失败:抛出明确异常,拒绝发号。听着粗暴,其实把矛盾转给了上游容灾:
客户端拿到「发号失败」,重试路由到集群里其他时钟正常的节点——回拨通常只影响个别机器,集群里总有健康节点。业务侧的注册中心健康检查配合剔除,带病节点自动下线。这一招的前提是发号服务多节点部署,单点雪花节点无路可退(所以发号器必须集群化,第 14 篇展开)。
第三招:备用机器号,给时间线开岔
但有一种场景第二招无解:NTP 集中校时导致全集群同时回拨——健康节点一个不剩,重试也是死。这时用第三招:换机器号。
原理想通极妙:重复号的构成是「旧时间戳加同一机器号」。既然时间戳已经回去了,那就换一个没在历史里出现过的机器号——三维里有一维变了,ID 就不会和历史重合。给每台节点多分配几个备用 workerID,回拨超阈值时切到下一个备用号,时间线等于开了条岔路:
节点 7 主号发到时间 T 后回拨 2 秒
检测到 大幅回拨 -> 切备用机器号 107
备用号从回拨后的时间继续发
时间 T x 机器 7 的组合永远不再出现,唯一性保住代价要心里有数:机器号要多留余量(每节点备二三个备用号),监控上要把「切换过备用号」的节点打上带病标记,运维定位这台机器的时钟为什么回拨。Leaf 的实现选择了更保守的路线——检测到回拨直接抛异常等待人工,把第三招留给业务自己实现,两条路线按团队运维能力选。
组合成一张流程图
nextId() 检测到 now 前于 lastTimestamp
|
v 判定回拨幅度
|-- 几十 ms 内 --------> 自旋等待追平(招一)
|-- 几百 ms 到秒级 ----> 拒绝发号,路由健康节点(招二)
|-- 秒级以上 ----------> 切备用机器号继续发(招三)
| 同时告警 + 节点标记 + 人工跟进预防优于治疗
代码治标,运维治本。发号节点的时钟纪律三条:一,NTP 用 slew 模式——让时钟以每秒零点几毫秒的速率缓慢逼近,避免 step 跳变;二,校时分批——绝不搞全集群统一校时,一批几台,错峰进行;三,时钟监控——节点间两两比对偏移量,偏差超阈值提前告警,别等撞号了才知道时钟病了。
小结
回拨治理一句话:毫秒级等待、秒级绕行、大步开岔,NTP 分批限速治本。三招组合下来,时钟回拨从死穴降级为可控风险。但整套方案都建立在一个前提上:每台节点的机器号是对的、不重复的。这个「身份证」怎么发、怎么续,就是下一篇的 workerID 之争。
评论 (0)