调度中心选主需用etcd lease+put原子操作实现:所有实例竞争写入/scheduler/leader键,仅成功者执行调度,其余监听变更并参与下一轮竞选;lease ttl设5–15秒,客户端启用withrequireleader()避免连接非leader节点。

调度中心怎么选主,避免脑裂?
单实例调度中心在故障时直接不可用,多实例又容易同时发任务——这就是典型的脑裂问题。Go 里没有现成的“选主”魔法,得靠外部协调服务或自己实现租约机制。
推荐用 etcd 的 Lease + Put 原子操作实现:所有调度实例竞争写入一个 key(比如 /scheduler/leader),带 10 秒 lease;只有写成功的那个才真正进入调度循环。其他实例轮询监听该 key 的变更,一旦发现 leader 失效,立刻参与下一轮竞选。
- 别用文件锁或本地时间判断,跨节点不一致
- lease 时间不能太短(否则网络抖动就频繁切换),也不宜太长(故障恢复慢),5–15 秒是常见折中
-
etcd客户端必须启用WithRequireLeader(),避免连接到非 leader 节点导致操作失败
任务分发怎么保证不丢、不重?
单纯用 channel 或内存队列,在进程重启或节点宕机时任务就丢了;而全靠数据库轮询又容易查出重复任务。
稳妥做法是“消息中间件 + 状态机 + 幂等键”。例如用 NATS JetStream 持久化任务消息,消费端处理完再提交 Ack;同时在任务结构体里嵌入唯一 task_id,执行前先查数据库确认是否已成功,避免重复执行。
- 别把任务参数直接序列化进消息体再反解——字段增减容易出错,建议用
Protobuf或固定结构的JSON - 数据库状态更新必须和消息
Ack在同一个事务里完成,否则可能漏 Ack 或多执行 - 任务表一定要有
updated_at和retry_count字段,失败时靠它控制重试边界
执行节点上线后如何快速同步待执行任务?
新节点启动后如果只等下一次调度触发,会错过已在队列里但尚未分发的任务——这在灰度发布或弹性扩缩容时特别明显。
标准解法是在节点注册成功后,主动向调度中心发起一次 /v1/tasks/pending 请求,拉取当前所有 status = 'pending' 的任务,并设置本地超时兜底(比如 3 秒没响应就继续走常规心跳流程)。
- 这个接口返回的任务必须带
created_at时间戳,客户端按时间排序执行,避免乱序 - 调度中心返回前要对这批任务做一次
UPDATE ... SET status = 'assigned' WHERE id IN (...) AND status = 'pending',防止被其他节点抢走 - 别让执行节点自己去数据库查——权限、连接池、一致性都难控,必须走调度中心统一出口
goroutine 泄漏怎么提前发现?
调度系统里满屏 go executeTask(...) 很危险:任务 panic、context 超时未取消、channel 阻塞,都会让 goroutine 卡住不退出,内存缓慢上涨直到 OOM。
最有效的防线是三层控制:启动时用 runtime.NumGoroutine() 打点 baseline;每个任务执行都套 context.WithTimeout;所有 channel 操作加 select + default 或超时分支。
- 别依赖
defer wg.Done()就以为万事大吉——万一executeTask里死循环了,Done()根本不会执行 - 生产环境务必开启
pprof,定期抓取/debug/pprof/goroutine?debug=2查看阻塞栈 - 任务函数入口第一行加
if ctx.Err() != nil { return },别等后面逻辑跑一半才发现上下文已取消
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











