在分布式系统的实现中,shuffle是一个非常关键的操作,直接决定算法在分布式环境下执行的可行性。shuffle被称为数据混洗,它在某些并行系统中被称作exchange。shuffle最简单的实现方式就是对数据做某种策略上的切分(比如说哈希切分,比如说范围切分),然后可以选择切分之后的数据直接传输或者将数据持久化供容错,我们比较一下这两种方式。1,直接传输:当数据切分之后马上传输,会不经过磁盘,直接从内存中向另一个节点的内存中传,当然这可以使网络达到最大速度,但是没法容错,除非你设计的系统就天天跑个位数的机器(个人观点),否则你作业稍微一错,重跑。分布式下的作业一般涉及较多数据和长时间执行,我们可以计算一下trade-off,显然不划算。2,持久化之后传输虽然慢,但你的容错有保障,因为数据在磁盘上呢,不过你只能得到最多1/2的网络带宽极限(个人实现证明)。
空谈都是虚的,直接看shuffle的简单实现,我们就以压磁盘的情况来讨论,在经典的mapreduce理论中,可以分为两个节点,一个map阶段,一个reduce阶段,map节点端会容错,也就是压磁盘。我们在此分为上端和下端,因为数据位拉取策略,从执行拓扑的根节点开始执行,reduce端为上端,map端为下端:
说先上端会将其所在位置的执行子树发送到下端(我们首先考虑上端的情况),注意这一步我们需要做什么?因为要传输到远程节点执行。我们从最本源的地方说起,传输就涉及到网络,网络在C++/C中你可以选择库,但是为了挑战一下你自己,你可以直接用syscall函数socket()。socket在上端先建立,返回的是一个文件描述符,再将这个文件描述符绑定到地址+端口上,然后监听(listen)这个文件描述符(所谓网络IO和磁盘IO,都可以用文件描述符表示)。下一步序列化执行子树,这点相当重要,试想你能将一段代码发过去吗?不能(你发代码还要编译啊,再者你发一段远端没有的代码,还需要把动态链接库也发过去),只能将一个序列化后的对象发过去。所以序列化对象之后发送,在下端收到上端发送的序列化的对象之后,马上解序列化,调用执行,在此可以用actor并发库(Theron)。由于序列化之后调用执行了代码,下端会开启socket和上端通信(因为上端已经建立好socket等待连接),下端马上connect上端,上端用accept函数接受,返回fd映射,并且因为下端有较多的节点,返回的是一个fd的数组,这个数组可以被用来监听,从下端获得发送过来的数据(我们可以想象server-client端程序中,server有每个client端的fd映射,这样才能通信),现在可以传输数据了。下端开始“制造”数据,这些数据可以被压到磁盘上,记录一下(这个记录一下你可以将它记录到master节点,或者一个专门的mapouttracker),然后传输,传输的时候按照分区号传输到具体本地对应的文件描述符。这个文件描述符在本地connect的时候就存在了,然后本地的数据就往这个文件描述符写。通过网络会到达上端。
下面的图片是一个二对三的连接建立示意图:
下端的数据到持久化到磁盘,这是一个阶段,在此pipeline会断开,然后从磁盘上将数据发送出去,可以有一个缓冲区(这个缓冲区如果存在,是一个partition的缓冲区,就是存储的是一个block的矩阵,是一个个block,并且分区了),也可以没有缓冲区,如果有缓冲区,就是一个生产消费者问题,如果没有缓存区,直接传输。上端呢?也可以有缓冲区,但是这个缓存区并不是分区的,因为到这个节点上的所有的block都是属于同一个分区的。如果没有呢?就顺序的传输。
但是如果不是持久化到磁盘呢?直接传输,将上端和下端完全pipeline起来呢?试想,一条数据,直接传输了,不需要缓存层,但是现在按块传输。一旦按块传输,肯定需要用缓存,因为你不知道哪个块会满。那上端需要缓冲区吗?试想不需要的情形,那就是收一块发一块,但是收一块发一块一看就是同步的,同步会很慢的,也会引起下端卡住,所以异步接收放到缓冲区中(syscall函数select()),再次利用生产消费者模式pthread_create出另一个线程,将数据从缓冲区取走,因此如果是全pipeline的,两个缓冲区再说难免,上下端都有。
(全是字啊,未完,还会画图和贴代码的:)