压测暴露的 RT 毛刺
号段模式上线,日常平稳,大促前全链路压测却暴露了毛刺:99 分位 5ms 的发号接口,隔一阵冒出几个 80ms 的尖刺,间隔大致等于段长消耗完的周期。老王顺着毛刺时间点查,答案就在第 3 篇的设计里:号段用尽那一刻,业务请求线程要同步去 DB 领下一段。DB 平稳时这段路只要几毫秒;DB 一抖动(大促时恰恰最爱抖),领号请求全堵在 DB 行锁上,下游一起排队。
问题本质:取号段的动作发生在请求线程的关键路径上。要消毛刺,就得把这段路挪出去——异步预加载,也就是双 buffer。
双 buffer:发号器里的双跑道
设计思路像机场的双跑道:一个在用,一个在备:
当前段 [1 - 1000] 备用段 [1001 - 2000]
|
| 用到 900(10% 警戒线)
v
后台线程异步领取 [2001 - 3000],放入预备位
|
当前段耗尽 -> 备用段无缝顶上 -> 再异步预取下一段三个要点:一是触发时机——当前段消耗到警戒线(常见 10% 到 20%)就启动预取,别等用完,给 DB 预留反应时间;二是切换零成本——备用段在内存里原子换位,请求线程永远只从「当前段」取号,感知不到后台动作;三是持续循环——备用顶上成为当前段的同时,后台马上去领下一个备用,双跑道永远保持。
改造后压测:毛刺消失,99 分位稳定在个位数毫秒。取号段的 DB 访问彻底离开了请求关键路径。
动态 step:段长跟着流量走
段长 step 固定值有个尴尬:设小了,低峰期也会频繁回 DB;设大了,服务重启浪费的号段多,容灾窗口计算也失真。工程化的解法是动态 step(美团 Leaf 的思路):按最近一段时间的发号速率自适应——统计过去若干分钟内的号消耗量,取其数倍作为下一次领取的段长。低峰期段自动变小,大促前段自动变大,DB 访问频率始终维持在合理区间。
老王给发号器加了每小时一次的速率统计,step 在 1000 到 50 万之间浮动,大促当天自动拉满,DB 写压力肉眼可见地平。
容灾时间账:DB 挂了能撑多久
双 buffer 还有第 3 篇没讲透的隐藏价值:把 DB 故障的容灾时间变成可计算的账。算给运营看:
| 参数 | 数值 | 说明 |
|---|---|---|
| 峰值发号速率 | 1 万个每秒 | 大促下单峰值 |
| 当前段剩余 | 最多 30 万 | 警戒线 10% 时正好预取 |
| 备用段存量 | 30 万 | 双 buffer 常备 |
| 可撑时长 | 约 60 秒 | 60 万存量除以 1 万速率 |
一分钟,够 DB 主从切换跑完,也够值班同学收到告警。想要更长,把 step 上限调大即可——号段模式的容灾是用内存换时间,而代价只是重启时多废几个号。
预取失败的处理也要定好:后台线程重试加指数退避,连续失败触发告警;内存段见底前最后一搏时,请求线程才允许同步领号——这层兜底平时永不触发,触发说明 DB 真出事了。配合第 3 篇说的已发号段持久化,主从切换回跳的窗口也能被拦住。
小结
双 buffer 一句话:当前段加备用段,异步预取离开关键路径,动态 step 跟着流量走,容灾时间用内存换。到这一步,号段模式已经是生产可用的发号器。但老王的野心不止于此——每秒百万级发号、完全去中心化、连 DB 都不依赖,这就是下一篇的主角:雪花算法。
评论 (0)