直接用time.Ticker轮询会失效,因其固定间隔无法响应实时权重变化;正确做法是用time.AfterFunc配合最小堆优先队列动态计算下次执行时间,按权重反比确定等待时长,并加锁保护并发更新。

为什么直接用 time.Ticker 做轮询会失效
因为固定间隔的 time.Ticker 无法响应权重变化——比如某个后端节点负载升高,你希望立刻拉长它的调度间隔,但 Ticker 只能停掉重建,中间有 gap,且重建开销不可控。真实场景中,轮询不是“每隔 N 秒调一次”,而是“按当前权重决定下次调用时机”。
用 time.AfterFunc + 优先队列维护动态下次执行时间
核心思路:不依赖周期性触发,而是每次执行完,根据当前各节点的权重算出下一次该选谁、等多久,再用 time.AfterFunc 注册回调。优先队列(如最小堆)存 {nextTime, nodeID},按 nextTime 排序,每次只取最早那个。
- 权重影响的是“下次被选中的概率”和“等待时长”,推荐用反比关系:权重越高,
baseInterval / weight越小,越早被调度 - 避免浮点运算误差累积,建议用整数权重,并统一换算成纳秒级
time.Duration - 必须加锁保护权重更新和队列操作,尤其在并发修改权重 + 调度器运行同时发生时,否则
heap.Fix可能 panic - 示例关键片段:
heap.Push(&q, &task{next: time.Now().Add(duration), node: "api-1"})
权重热更新时如何不中断调度、不丢任务
常见错误是直接改权重变量后重排整个队列——这会导致正在 pending 的任务被丢弃或重复触发。正确做法是:保留旧任务项,仅对新生成的任务使用新权重;已入队但未触发的旧任务保持原 nextTime 不变,让它自然过期执行即可。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- 每次权重变更后,只影响后续新调度周期的计算,不 retroactively 修改已有队列项
- 可加一个版本号字段(
weightVersion int64),每个任务携带创建时的版本,执行前校验是否过期,过期则按新权重重新计算下一次时间 - 注意
time.AfterFunc的函数一旦注册就不能取消,所以不要试图“取消旧任务”,而是让旧任务执行时发现权重已变,自动跳过或降级处理
为什么不用 select + 多个 time.Timer
看似直观,但 N 个节点就要维护 N 个 Timer,每个都需单独 Stop() 和 Reset(),极易漏掉 Stop 导致 timer 泄漏(Go runtime 不回收已触发但未 Stop 的 timer)。而且权重调整时要遍历所有 timer,性能随节点数线性下降,50+ 节点就明显卡顿。
立即学习“go语言免费学习笔记(深入)”;
-
time.Timer的Reset在 timer 已触发状态下返回 false,必须配合select { case 清空 channel,否则下次 <code>Reset失败 - 优先队列方案把所有调度逻辑收束到单个 goroutine + 单个 heap,状态可控,调试也方便
- 实测:100 节点权重每秒更新一次,
heap方案 CPU 占用稳定在 0.3%,多 timer 方案峰值冲到 12% 且毛刺明显

















