背景
在master-worker中一般会采用全局mq进行任务分发,由worker进行依次拉取任务并执行。但是worker拉取任务是有一定耗时的(具体耗时因mq选型而异),另外在网络抖动等一些情况下,拉取耗时可能会变的很大,且不可控。
解决方案
预拉取
- worker新增预拉取策略,本地缓存下一个要执行的任务
- 等当前任务执行完之后,从buffer中取出,同时异步去拉取下一个任务
饥饿模式
上面的方案短板是很明显的,如果worker当前执行任务耗时太久,则会导致下一个任务执行的latency加大。这个是我们不希望看到的。
因此提出饥饿模式,这里我们需要一个manager,它负责维护任务分发等信息,并且我们需要给任务定义以下5种状态
- 待分发
- 已分发待执行,(已经分发给worker,等待执行)
- 执行中,(worker已经在执行)
- 执行完成
- 饥饿模式
针对每个任务从分发出去开始,启动一个定时器,如果在一定时间范围内没有被执行,则该任务状态切换为饥饿模式,同时发命令给之前的worker取消执行buffer中的任务(因为是分布式的,任务可能被多次执行,所以需要保证幂等)。
针对饥饿模式的任务,因为已经等待了一定的时间,所以如果重新走任务分发可能也好导致单个任务latency较大,所以可以预启动一些worker做buffer,处理这种饥饿模式下的任务。
具体实现
manager
- 维护一个
已分发待执行
的延迟队列,针对已经到期的任务, 执行将原worker删除任务命令 和 任务切换到饥饿模式。 - 维护一个饥饿模式任务队列,当其不为空的时候,启用buffer-worker。
- 问题:是否可以强制其他正常worker也先执行这边的任务。(这样的设计感觉有点负责,存疑)
worker
- 针对本地任务的状态上报,对manager进行上报。manager收到状态变动后,对其状态进行修改。
- 执行任务并异步拉取下一条任务。
- 如果本地没有队列,向manager同步拉取并阻塞住。
- 对worker而言,其自身不应该意识到自己是normal还是buffer。尽量让其执行任务和任务流转状态保持一致。
QA
饥饿模式名词的由来
参考自 go Mutex
的设计:https://github.com/golang/go/blob/master/src/sync/mutex.go#L42。
mutex
等待队列中第一个goroutine在长时间抢占不到锁的时候,会将mutex
切换到饥饿模式(starvation)。该模式下其他goroutine将不会再尝试抢占锁,这样就在一定程度上保证了goroutine
抢占锁的公平性。