使用Go語言實現簡單MapReduce框架

晦若晨曦發表於2017-12-14

年後回來第一篇!老規矩,先上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。原本神祕的技術其實最簡單的實現起來原理並不困難。

新的一年,從此開始!

相關文章