小王做的煎饼很好吃,于是他决定开一家卖煎饼的店。店铺开张时只有他一个人,他既要做煎饼又要在柜台打包煎饼和记账,随着名气越来越大,有越来越多的人慕名而来。但是也有很多人看到排队时间很长而放弃购买。小王决定招聘一个服务员负责打包煎饼和记账,这样小王就能专注于做煎饼,而顾客也不用等那么长时间。
几年之后,随着旅游业的发展,有很多外地人也慕名而来尝试小王的煎饼,这时候小王和他的服务员已经忙不过来了,小王决定开几家分店。在开分店时,小王为了避免口碑的下滑,要求每个分店都在早上去小王那里拿走新鲜的原材料。这样,小王既保证了煎饼的质量,又提升了制作煎饼的速度,还能让顾客排队时间变短,小王也能赚到更多的钱。
计算机在处理一些运算量/吞吐量大的任务时,也会遇到性能瓶颈问题。这时候可以选择更换更强劲的CPU,更大的内存来解决这个问题(就像刚开始小王招聘一个服务员来减少顾客的排队时间),但是如果任务量过大的话还是会遇到性能瓶颈问题。这时候可以选择加机器,把一个大任务拆分为几个小任务让不同的机器来做这些小任务(就像小王开分店一样)。
在写本文时,参考了Google的MapReduce论文,把其中任务分发这部分内容单独拿了出来,而没有考虑具体的Map和Reduce操作。
架构图
在进行任务分发时,Client节点通过与Master节点之间的通信来完成任务的获取,任务的处理操作,其架构图如下所示。
Master负责存储任务的状态
Client节点负责获取任务和执行任务
Client与Master节点之间的通信
下图中->表示网络请求方向
- 请求任务阶段,Client1和Client2向Master节点发出任务请求
- 分配任务阶段,Master节点向Client1和Client2发送分配给各个节点的任务
- 汇报任务处理状态阶段,Client1和Client2向Master节点汇报其所做任务是否完成
该图的绘制格式来源于PBFT一文,个人感觉通过这种方式绘制通信方向会更加直观一些。
任务的状态
其中每个任务都有三种状态,分别是未分配,正在处理和已完成。
在处理任务时,如果Master长时间没有收到Client节点关于任务状态的汇报,就认为该任务处理失败,标记为未分配,重新执行该任务。
任务是如何分配的?
在Master节点,存储一个关于任务状态的数组,这样每当一个Client请求任务时,就可以遍历该数组找到一个还未分配的任务,将其分配给Client。
const (
unassign = 0
inprocess = 1
completed = 2
) //任务的三种状态
func main() {
m := Master{
}
m.task = make([]int, 10) //存储任务状态的数组
}
在此之前,Master节点需要先检查所有的任务是否已经完成,思路很简单,就是遍历整个数组,看其所有的任务是否已经完成,已经完成的话就不再分配任务。遍历时可以采用&&
操作,只要其中一个任务还没有完成,那么最终结果就是false
。
taskDone := true
for