使用Go语言实现简单MapReduce框架

2,546 阅读5分钟

年后回来第一篇!老规矩,先上Github

SimpleGoMapReduce

学习Go语言也很有一段时间了。这个东西从年前就开始构思,这两天终于研究着搞出来了。算是对于goroutine相关的一个练习吧。

###框架概述

框架的入口为MapReduce容器 MRContainer。使用流程如下:

  • 初始化一个MRContainer,指定map与reduce线程数参数。
  • 指定map与reduce执行方法。
  • 输入数据。
  • 调用Start函数启动服务。
  • 通过GetResult函数获取结果集。

框架会根据配置开启指定数目的map与reduce协程,然后轮询这些协程以分配任务。最终将结果放在一个list存储的结果集中。

###实现细节

####容器内容 既然要做MapReduce,首先要分析程序中包含哪些东西。

首先map与reduce方法自然是必不可少的。由于协程有框架来进行调度,两个方法声明如下:

Mapper func(data interface{})MapperedDataEntry
Reducer func(in MapperedDataSet) interface{}

如此声明在实现上限定了mapreduce的操作方式。若是一个map方法需要输出多个映射结果,可以选择将输出channel传入Mapper方法以供使用。

容器中包括输入,中转和输出三个数据缓存区。用来保存输入,map结果与reduce结果。

用于goroutine之间通讯的channel也需要保存起来,由于一个goroutine有多个channel需要管理,声明了一个结构体用来持有这些channel。保存于一个list用于轮询。

####工作流程

map与reduce的协程启动起来基本都是一样的。所以就直接放在一起说了。

根据go的推荐用法,在goroutine之间使用channel进行通信。又由于channel操作是阻塞的,若是某个任务执行时间很长,会影响轮询,或者说轮询也就基本失去了意义。(必须等待任务执行完成才能继续)

为了解决这个问题,每个工作协程附带着启动了一个心跳协程,在轮询时通过select同时监听输出和心跳协程。心跳协程会每隔一段时间返回当前工作协程的状态。若收到输出或者工作协程状态为空闲,则会给工作协程一个新的任务。否则跳过继续轮询。

启动时,首先启动指定数目的工作协程,并将持有的channel保存到容器中,下面以map协程为例:

//map协程方法
func (container *MRContainer) mapWrapper (in <-chan interface{}, out chan<- MapperedDataEntry, system chan int,cb chan int){
	mapper := container.Mapper
	workState := SIGN_BEEP_FREE // 当前工作状态,若为0,则可以输入,否则正忙
	beepChan := make(chan int)
	go beep(&workState,cb,beepChan) // 启动心跳
	for {
		shutdownFlag := false
		select {
		case src,ok := <-in:
			if !ok {
				fmt.Print("not ok !")
			}
			workState = SIGN_BEEP_WORKING
			entry := mapper(src)
			workState = SIGN_BEEP_FREE
			out<-entry

		case command,ok := <-system:
			if !ok {
				fmt.Print("not ok !")
			}
			fmt.Println("recive shutdown sign :", strconv.Itoa(command))
			if(command == SIGN_CTRL_SHUTDOWN){
				shutdownFlag = true
			}
		}

		if(shutdownFlag){
			break;
		}
	}
	fmt.Println("goroutines has been finish")
}

此时工作协程处于空闲状态,等待接收数据。然后就要在容器协程启动轮询开始分发数据,同样以map操作为例:

func (container *MRContainer) startMap (){
	fmt.Println("container doing map work")
	holder := container.holdMapChans.Front();
	inbuffer := container.inBuffer
	for  {
		fail2Stop := false // 判断是否可以停止map工作,当缓存清空且所有工作均已经完成即可

		if chans,ok := holder.Value.(mapChanSet);ok{
			select {
			case entry,ok :=<-chans.out:
				if !ok {
					fmt.Print("not ok !")
				}
				container.midBuffer.put(entry.key,entry.value)
				e := inbuffer.pull()
				if e!=nil {
					chans.in <- e
					fail2Stop = true //还有人没完成工作呢,哼哼
				}

			case state,ok := <-chans.beep:
				if !ok {
					fmt.Print("not ok !")
				}
				if state==0 {
					e := inbuffer.pull()
					if  e!=nil {
						chans.in <- e
						fail2Stop = true //还有人没完成工作呢,哼哼
					}else{
						chans.system <- SIGN_CTRL_SHUTDOWN // 当前已经没有更多数据,且此协程已经完成工作,发出关闭信号
					}
				}else{
					fail2Stop = true // 尝试停止服务失败,还有协程没有完成任务
				}
			}
		}
		if(!fail2Stop){
			break;
		}
		//维持轮询
		ele := holder.Next()
		if ele==nil{
			holder = container.holdMapChans.Front()
		}

	}

}

###问题总结

在开发过程当中当然会遇到很多问题,从中也吸取了很多教训。

#####Go中的类型校验:

与java不同,在Java中,不明真相的类型之间可以互相进行转换,转换失败才会报错。在Go中未经过类型校验直接转换是会报错的。应当也可以算是某种强制的编程规范吧。做法如下:

    if ele,ok:=e.Value.(MapperedDataSet);ok{
						chans.in <- ele
						fail2Stop = true //还有人没完成工作呢,哼哼
					}

#####channel的阻塞

在编码中最初遇到的问题之一就是由于channel阻塞导致各种死锁崩溃。go能够自动判断死锁并结束程序还是非常好的。但是出问题就很纠结了。

很多种情况会导致崩溃,不过我遇到的问题基本就是由于一个channel进行了多次读取。在多线程的程序中这种问题真的很隐蔽,找了很久才找明白原因。换用单项channel可以很有效的避免这种问题。

#####指针和值的使用

一开始的时候在容器中直接使用变量来保存数据,后来发现有些数据的修改一直都不管用,直到最后换用指针方式才成功执行。

这是一个很大的话题,还需要仔细研究,估计之后还可以再单发一篇简书出来。

###总结

不得不说用Go来编写多线程的程序真的是太方便了,比起java来说不知道高明到哪里去了。线程之间的通信等也十分简洁。不需要过多啰嗦的代码就可以很流畅的实现通信。

通过这个框架的实现,算是比较全面的练习了一下go语言的核心功能:goroutine,以及相关的技术。同时也算装了一下MapReduce的B。原本神秘的技术其实最简单的实现起来原理并不困难。

新的一年,从此开始!