首發於公眾號:努力學習的阿新
大家好,這裡是阿新。
MIT 6.824 是麻省理工大學開設的一門關於分散式系統的明星課程,共包含四個配套實驗,實驗的含金量很高,十分適合作為校招生的專案經歷,在文章《2022 雙非應屆 CS 碩士校招上岸位元組跳動(校招總結)》中,我也將其推薦給了各位讀者。但由於課程是全英的,實驗說明也是全英的,實驗過程中還需要閱讀相關的英文文獻,很多同學(包括曾經的筆者)受困於英語,對其望而卻步。因此,筆者決定開啟系列文章:MIT 6.824(Spring 2020)實驗檔案翻譯。來嘗試為大家翻譯實驗的說明檔案和相關論文的關鍵部分,同步更新的還有系列帶做文章,以期能夠幫助各位讀者順利完成該課程實驗。
本文對 MIT 6.824 Lab 1:MapReduce 的說明檔案進行了全文翻譯。需要注意的是文中的 job 和 task,其中,job 是指整個 MapReduce 計算,表示的是任務整體,而 task 則是指一次 Map/Reduce 呼叫,表示的是任務區域性,一個完整的 MapReduce job 由一些 Map task 和 Reduce task 組成,筆者無法找到兩個中文詞彙來很好的描述兩者之間的關係,因此並未翻譯。同時,由於筆者水平有限,如有翻譯不恰當的地方還請各位批評指正。
以下為原文翻譯。
作者:MIT 6.824 Staff | 譯者:阿新
依據 CC BY 3.0 US 許可證進行授權
許可證連結:https://creativecommons.org/licenses/by/3.0/us/deed.zh
本實驗的目標是引導您構建一個 MapReduce 系統,您需要實現兩個程式:worker 和 master。其中 worker 程序負責處理檔案讀寫操作以及呼叫 Map/Reduce 函數處理 Task。而 master 程序則負責為 worker 程序分配 Task 並處理崩潰的 worker 程序。您構建的系統與MapReduce 論文中所描述的系統類似。
除了我們提供給您的程式碼外,您必須獨立完成實驗要求的程式碼。實現過程中杜絕參考其他同學和前幾年課程實驗的解決方案。允許您與同學討論,但不允許您抄襲他們的程式碼。之所以設立這條規則,是因為我們認為只有獨立完成實驗,您的能力才會得到最大的提升。
請不要公佈您的程式碼,也不要讓它被選修 6.824 的學生通過某種方式獲取到。github.com
中的倉庫許可權預設是公開的,除非您將倉庫許可權設為私有,否則請勿用其儲存實驗程式碼。儲存程式碼可以使用MIT 的 Github,但請確保您建立的是私有倉庫。
本實驗(以及 6.824 的其他實驗)使用的程式語言是Go語言,如果您不熟悉 Go 語言,其官方網站上有很多教學供您學習。我們將使用 1.13 版本的 Go 語言來評判您的程式碼,因此您也應該使用這一版本(譯者注:筆者所用的 Go 語言版本為 1.17.7,編輯此文章時已經完成了 Lab 2A,無異常)。另外,如果您要檢視計算機中已有的 Go 語言版本,可以執行go version
命令。
我們推薦您在自己的機器上完成實驗,這樣您就可以使用您熟悉的環境,比如工具,文字編輯器等。另外,您也可以在 Athena 上完成實驗。
您可以使用Homebrew來安裝 Go 語言。安裝好 Homebrew 後,執行brew install go
命令即可。
根據您使用的 Linux 發行版,您可以從相應的軟體包庫中下載最新版的 Go 語言,比如在 Ubuntu 中可以執行apt install golang
命令來安裝 Go 語言。此外,您還可以從 Go 語言的官方網站中手動下載二進位制包。首先,確保您使用的是 64 位的 Linux 核心(uname -a
會提示"x86_64 GNU/Linux"),然後執行如下命令即可:
$ wget -qO- https://dl.google.com/go/go1.13.6.linux-amd64.tar.gz | sudo tar xz -C /usr/local
如果是國內的同學,可以執行如下命令:
$ wget -qO- https://studygolang.com/dl/golang/go1.13.6.linux-amd64.tar.gz | sudo tar xz -C /usr/local
需要確保/usr/local/bin
在您的環境變數PATH
中。
實驗可能無法直接執行於 Windows 上。如果您敢於嘗試,可以試試Windows Subsystem for Linux,並執行上述 Linux 命令(譯者注:筆者做實驗用的是 VSCode+WSL2+Golang 1.17.7,需要注意的是一定要用 WSL2,否則無法使用 VSCode 偵錯 Go 程式)。否則還是乖乖使用 Athena 吧。
您可以通過ssh {your kerberos}@athena.dialup.mit.edu
命令登入公共 Athena 主機。一旦登入成功,便可通過如下命令獲取 1.13 版本的 Go 語言:
$ setup ggo
您將使用git(版本控制系統)拉取實驗初始版本程式碼。如果您不熟悉 git,可以通過查閱Git-Book和Git User Manual自行學習。執行以下命令,即可從遠端拉取 6.824 的初始實驗程式碼:
$ git clone git://g.csail.mit.edu/6.824-golabs-2020 6.824
$ cd 6.824
$ ls
Makefile src
$
在src/main/mrsequential.go
中,我們為您實現了一個簡單的順序 MapReduce。由於使用的是單程序,因此該程式同一時刻僅能執行一個 Map/Reduce task。此外,我們還為您提供了多組處理不同 task 的 Map/Reduce 應用程式程式碼:如mrapps/wc.go
中包含單詞計數所需的 Map/Reduce 方法,mrapps/indexr.go
中包含計算檔案索引的 Map/Reduce 方法。您可以執行以下命令來執行單詞計數的順序版本 MapReduce:
$ cd ~/6.824
$ cd src/main
$ go build -buildmode=plugin ../mrapps/wc.go
$ rm mr-out*
$ go run mrsequential.go wc.so pg*.txt
$ more mr-out-0
A 509
ABOUT 2
ACT 8
...
mrsequential.go
所需的輸入資料來自名為pg-xxx.txt
的文字檔案,輸出則儲存在檔案mr-out-0
中。
您可以從mrsequential.go
中隨意借鑑您需要的程式碼,同時也可以閱讀mrapps/wc.go
來了解 MapReduce 的應用程式程式碼長啥樣。
您的任務是實現一個分散式的 MapReduce 系統,該系統由兩個程式組成:master 和 worker,在系統執行期間共包含一個 master 程序和多個並行執行的 worker 程序。在實際應用的 MapReduce 系統中,worker 程序執行在很多不同的機器上,而在本實驗中,您只需要在一臺機器上執行它們即可。worker 程序和 master 程序之間使用 RPC 通訊。每個 worker 程序需完成以下工作:向 master 程序請求 task,從若干檔案中讀取輸入資料,執行 task,並將 task 的輸出寫入到若干檔案中。master 程序除了為 worker 程序分配 task 外,還需要檢查在一定時間內(本實驗中為 10 秒)每個 worker 程序是否完成了相應的 task,如果未完成的話則將該 task 轉交給其他 worker 程序。
我們為您提供了一些上下文程式碼。master 程式和 worker 程式的 main 函數入口分別位於main/mrmaster.go
和main/mrworker.go
中,不要修改這些檔案,您應該將您需要實現的程式碼寫在mr/master.go
,mr/worker.go
和mr/rpc.go
中。
下面說明如何執行您寫的程式碼,以單詞計數應用程式為例。首先,執行以下命令以確保被構建的單詞計數外掛是最新版:
$ go build -buildmode=plugin ../mrapps/wc.go
cd 到src/main/
中,執行 master 程式:
$ rm mr-out*
$ go run mrmaster.go pg-*.txt
其中pg-*.txt
引數是檔案路徑,這些檔案中儲存著 master 程式的輸入資料;每個檔案都可以作為單個 Map task 的輸入。
在其他若干視窗中,您可以執行如下命令來執行一個或多個 worker 程序:
$ go run mrworker.go wc.so
當 worker 程序和 master 程序執行完畢後,可以顯示 mr-out-*中的內容以檢視程式輸出。如果您的實驗程式碼沒有錯誤的話,經過排序後的輸出結果應與mrsequential.go
的輸出相同,如下所示:
$ cat mr-out-* | sort | more
A 509
ABOUT 2
ACT 8
...
main/test-mr.sh
是我們為您提供的測試指令碼。當給定pg-xxx.txt
作為輸入時,該指令碼會檢查您實現的 MapReduce 系統對兩個不同型別的 Job(單詞計數和計算檔案索引)是否產生了正確的輸出。該指令碼也會檢查您實現的 MapReduce 系統的 worker 程序在處理 Map task 和 Reduce task 時是否是並行執行的,以及您的系統是否能正確處理發生崩潰的 worker 程序。
如果您現在執行測試指令碼,它將會掛起,因為 master 並沒有被實現:
$ cd ~/6.824/src/main
$ sh test-mr.sh
*** Starting wc test.
您可以將mr/master.go
中Done
函數內的ret := false
語句修改為 true 來使主程式立刻退出,您將會得到如下輸出:
$ sh ./test-mr.sh
*** Starting wc test.
sort: No such file or directory
cmp: EOF on mr-wc-all
--- wc output is not the same as mr-correct-wc.txt
--- wc test: FAIL
$
測試指令碼期望對於每個 reduce task,都會生成一個被命名為mr-out-X
的輸出檔案。空的mr/master.go
和mr/worker.go
不會生成這些檔案(也少做了很多其他事情),進而導致測試失敗。
當將全部程式碼實現完畢後,執行測試指令碼後的輸出應該看起來像這樣:
$ sh ./test-mr.sh
*** Starting wc test.
--- wc test: PASS
*** Starting indexer test.
--- indexer test: PASS
*** Starting map parallelism test.
--- map parallelism test: PASS
*** Starting reduce parallelism test.
--- reduce parallelism test: PASS
*** Starting crash test.
--- crash test: PASS
*** PASSED ALL TESTS
$
您也會看到一些來自 Go RPC 包輸出的錯誤,看起來像這樣:
2019/12/16 13:27:09 rpc.Register: method "Done" has 1 input parameters; needs exactly three
忽略這些資訊即可。
一些規則:
nReduce
個 reduce task 讀取,而nReduce
則是main/mrmaster.go
傳遞給MakeMaster
的引數;mr-out-X
中;mr-out-X
中每行都應該是呼叫一次 Reduce 函數的輸出,應該按照 Go 語言的"%v %v"
的格式生成,也即 key 和 value,如在main/mrsequential.go
中註釋"this is the correct format"的位置所示。如果您的實現和這一格式相差太多,測試指令碼將會執行失敗;mr/worker.go
,mr/master.go
和mr/rpc.go
。儘管您可以暫時地修改其他檔案來輔助您測試,但請確保其他檔案被還原為初始狀態(原始版本)後您的程式仍能正常工作,我們將會使用原始版本的程式碼進行評判;main/mrmaster.go
希望您實現的mr/master.go
中的Done()
方法會返回 true。這樣mrmaster.go
就能知道 Job 已經順利完成,程序即可退出;call()
函數的返回值:如果 woker 程序無法和 master 程序通訊,那麼 worker 程序就可以認為整個 Job 已經全被做完了,自己也就可以退出了。當然是否這麼做還是要取決於您的設計,您也可以設計一個」please exit「的偽任務,當 worker 程序收到這一任務,就自動退出(譯者注:筆者就是這麼做的,看起來更優雅一些)。如果您覺得無從下手,可以從修改mr/worker.go
中的Worker()
函數開始,在函數中首先實現以下邏輯:向 master 傳送 RPC 來請求 task。然後修改 master:將檔名作為尚未開始的 map task 響應給 worker。然後修改 worker:讀取檔案並像mrsequential.go
程式一樣,呼叫 Map 方法來處理讀取的數;
MapReduce 應用程式的 Map/Reduce 函數被儲存在以.so
結尾的檔案中,在執行時使用 Go plugin 包讀取;
如果您改變了mr/
資料夾中的檔案,並且該檔案也被用到,那您需要將該檔案使用類似於go build -buildmode=plugin ../mrapps/wc.go
的命令重新編譯成 MapReduce 外掛;
本實驗要求 worker 程序使用同一個檔案系統,因此所有的 worker 程序必須執行於一臺機器上。如果想要讓 worker 程式執行在不同的機器上,那您需要為他們提供一個全域性的檔案系統,比如 GFS;
一個命名中間檔案的合理形式是mr-X-Y
,其中 X 是 Map task 的編號,Y 是 Reduce task 編號;
worker 程式處理 Map task 的程式碼中需要一種將中間鍵值對儲存為檔案的方式,也需要一種在處理 Reduce task 時能從檔案中正確讀回鍵值對的方式。一種可能的實現是使用 Go 語言的encoding/json
包。將鍵值對寫入 JSON 檔案的程式碼如下:
enc := json.NewEncoder(file)
for _, kv := ... {
err := enc.Encode(&kv)
從 JSON 檔案中讀回鍵值對的程式碼如下:
dec := json.NewDecoder(file)
for {
var kv KeyValue
if err := dec.Decode(&kv); err != nil {
break
}
kva = append(kva, kv)
}
在 worker 中處理 map Task 的部分,對於一個給定鍵,您可以使用ihash(key)
函數(在worker.go
中)來選擇它屬於哪一個 reduce task;
您可以將mrsequential.go
中一些功能的程式碼拿來直接用,比如:讀取 map task 的輸入檔案,在呼叫 Map 方法和呼叫 Reduce 方法之間對中間鍵值對進行排序,以及儲存 Reduce 函數的輸出到檔案。
作為一個 RPC 伺服器,master 程序將是並行的,因此不要忘記給共用資源加鎖。
可以執行命令go build -race
和go run -race
來使用 Go 語言的 race detector,在test.sh
中有一條註釋為您展示如何為測試開啟 race detector;
worker 程序有時需要等待,比如在最後一個 map task 處理完之前,worker 不能開始對 reduce task 的處理。實現這一功能的一種可能方案是 worker 程序週期性的向 master 請求 task,在每次請求間使用time.Sleep()
阻塞一段時間。另一種可能的方案是 master 在收到 rpc 請求後額外開啟一個 RPC 處理執行緒,在這個執行緒中執行迴圈等待(也可以使用time.Sleep()
和sync.Cond
),這樣使得阻塞的 RPC 不會影響 master 響應其他 RPC;
master 程序無法可靠的區分崩潰的 worker 程序、活著但因某些原因停止執行的 worker 程序和正在執行但太慢導致無法使用的 worker 程序。這一問題最好的解決方案是令 master 等待一段時間,如果某個 worker 程序在這段時間(在本實驗中,這段時間被設定為 10 秒)內沒有完成相應的 task,就放棄繼續等待並將該 task 重新分配給其他 worker。之後,master 應該假設這個 worker 程序已經死亡了(當然,它可能還活著);
為了測試容錯,您可以使用mrapps/crash.go
外掛,它在 Map 和 Reduce 函數中增加了隨機退出功能;
為了確保沒人能看到被崩潰程序寫了一半的檔案,MapReduce 論文提到了一個小技巧,那就是使用臨時檔案,一旦該檔案完全寫完,就自動重新命名。您可以使用ioutil.TempFile
建立臨時檔案,並使用os.Rename
去自動重新命名它。
test-mr.sh
將會在子目錄mr-tmp
中執行所有的程序,因此如果有錯誤發生並且您想檢視中間輸出檔案的話,可以檢視這一目錄中的檔案。
注意:在正式提交前,請執行
test-mr.sh
。
使用命令make lab1
命令打包您的實驗程式碼並將其上傳到班級的提交網站,其網址為:https://6824.scripts.mit.edu/2020/handin.py/。
第一次提交時您可能需要通過以下方式之一進行登入:1. 使用您的 MIT 證書;2. 通過 email 申請一個 API key。一旦登入成功,您的 API key(XXX
)將會被顯示,在控制檯中輸入以下命令上傳 lab1 時會用到這一 API key:
$ cd ~/6.824
$ echo XXX > api.key
$ make lab1
注意:檢查提交網站以確保您的實驗程式碼已經成功提交!
提示:您可能提交了多次。我們將會使用時間戳來檢查您的提交是否是最新的。