工头的giveTask方法可以由开发者实现,工头先获取线上工人数量,然后调用各个工人的doTask方法,让工人们并行完成任务,工人的doTask方法也可以由开发者实现
“职介所”(ParkServer)可以部署在一台独立计算机,它在每次分布式计算时给“包工头”介绍“工人”,当“工人”加入集群时首先在“职介所”进行登记,然后“包工头”去“职介所”获取可用于计算的“工人”,然后“职介所”会继续和“工人”保持松散的联系,以便在有新的工程时继续介绍该“工人”。
“工人”(MigrantWorker)为一个计算节点,可以部署在多个机器,它的业务逻辑由开发者自由实现,计算时,“工人”到“输入仓库”获取“包工头”分配的输入资源,再将计算结果放回“输出仓库”返回给“包工头”。
“包工头”(Contractor)负责承包一个复杂项目的一部分,可以理解为一个分配任务和调度程序,它的业务逻辑由开发者自己实现,开发者可以自由控制调度过程,比如按照“工人”的数量将源数据切分成多少份,然后远程分配给“工人”节点进行计算处理,它处理完的中间结果数据不限制保存在HDFS里,而可以自由控制保存在分布式缓存、数据库、分布式文件里。但是通常情况下,输入数据多半分布在“工人”机器上,这样有利于“工人”本地进行计算处理和生成结果,避免通过“包工头”进行网络传送耗用。“包工头”实际上是一个任务的调度者,它通过getWaitingWorkers方法获取线上工人并行调度任务执行,“包工头”是一个并行计算应用的程序入口,它不是一个服务程序,运行完成就退出。
“手工仓库”(WareHouse)为输入输出设计(参见后面的图2-4),让计算的资源独立于计算的角色(包工头,工人),包工头和工人的数据交互都是通过手工仓库,它可以当做远程请求的参数,同时也是返回结果的对象。“手工仓库”可以存放任意类型的对象,它本身就是一个map的实现。
我们注意WareHouse doTask(WareHouse inhouse)这样的接口设计,它的输入输出都是WareHouse,意味着它可以接受任意数量、任意类型的输入,也可以返回任意数量、任意类型的输出,这样能做到最大灵活程度,因为作为一个框架,我们无法假定开发用户设计出什么样的输入输出参数,以及什么样的类型。
有人容易认为,包工头是将数据发到职介所,由工人去职介所拿数据(当然这种基于消息中枢的计算方式也是可以支持,下个章节会介绍),但是图2-2展示的默认最常用的计算模型,是包工头直接跟工人交互的。也就是职介所只起介绍作用,介绍给工头后就不干涉他们之间分配任务干活了(因此计算过程中,职介所挂掉也不会影响工头/工人完成计算,这对计算结构的健壮性来说很重要),但是介绍后职介所还会跟工人保持一种松散的联系,询问工人工作的怎么样,联系方式是否变了,等等。联系是松散的意味着不影响工人工作,可以半个月1个月跟职介所联系一次,时间可以自由设置,但是不能中断联系。保持松散联系的目的是为了在下一次有工头来职介所找工人时,是否继续介绍该工人,如果该工人死机,跟职介所已经失去了联系,就不能再介绍给工头。“包工头-职介所-工人”整个作业过程跟我们现实社会中的场景完全吻合,这样设计有利于更直观形象地理解这背后的并行计算思想。
最后我们再归纳一下各个角色职责:
❏ 包工头负责分配任务,开发者实现分配任务接口。
❏ 农民工负责执行任务,开发者实现任务执行接口。
❏ 职介所负责协同一致性等处理(登记,介绍,保持联系)。
❏ 手工仓库负责输入输出数据交换。