原文链接:https://pdos.csail.mit.edu/6.824/labs/lab-mr.html
Introduction
你将在这个实验构建MapReduce系统。你将实现一个worker进程,它能够调用Map和Reduce函数,处理对文件的读写;同时你也要实现一个coordinator,它能够给各个worker分配任务,并且处理出错了的worker进程。在这个实验中你构建的系统类似于MapReduce论文中所实现的。(注:这个实验中使用了概念“coordinator”而不是论文中的“master”)
注:论文链接需翻墙,需要者可自行查询MapReduce论文。
Getting Started
为了这个实验,你需要安装Go语言:setup Go。(可自行查找安装)
你可以使用git获取这个实验的初始版本的代码。如下git clone命令获取代码:
$ git clone git://g.csail.mit.edu/6.824-golabs-2021 6.824
$ cd 6.824
$ ls
Makefile src
$
我们给你提供了mapreduce的顺序(非并发)实现,是文件src/main/mrsequential.go。它在单一进程运行了maps和reduces(注:也就是只运行一个进程,把map和reduce的工作按顺序完成了)。我们还给你提供了MapReduce的应用实现:文件mrapps/wc.go实现了 word-count;文件mrapps/indexer.go实现了a text indexer。想看一下我们最终实现的实验结果,你可以运行一下如下命令:
$ cd ~/6.824
$ cd src/main
$ go build -race -buildmode=plugin ../mrapps/wc.go
$ rm mr-out*
$ go run -race mrsequential.go wc.so pg*.txt
$ more mr-out-0
A 509
ABOUT 2
ACT 8
...
(注:如果你编译时没有使用 -race ,在运行时也不要使用 -race。)
对于mrsequential.go,它的输出文件是mr-out-0,它的输入文件是pg-xxx.txt。(xxx为任意单词)
你可以放心地从文件mrsequential.go中copy你需要的代码(这个文件本来就是给你做参考的)。你想知道的更多,也可以看一下mrapps/wc.go中的代码,来看看map和reduce函数相关的代码。
Your Job(时长:六小时及以上)
你要实现分布式MapReduce,具体实现分为两部分程序:coordinator程序和worker程序。运行时,有一个coordinator进程,一个或多个worker进程同时运行。在真正的系统中,你的worker进程应该运行在不同的机器上,不过这里你只是运行在同一个机器上。worker进程们通过RPC和coordinator通信。每一个worker进程向coordinator要任务,然后根据这个任务读入一个或多个文件,再执行这个任务,执行后输出一个或多个文件。coordinator应该关注这个worker是否在规定的合理时间内完成了这个任务(这个实验中规定的合理时间是10秒),如果超过这个时间没完成,就把这个worker要做的任务分配给其他worker。
我们已经给了你一些代码作为框架来开始你的工作。在main文件夹下,coordinator有关的代码文件为main/mrcoordinator.go,worker有关代码为main/mrworker.go,不要改变这两个文件(包括里面的内容)。你的代码实现应该在mr/coordinator.go,mr/worker.go和mr/rpc.go中。
下面讲一下如何运行这个Map Reduce系统。首先,你要保证相关plugin是最新编译的,运行如下命令:
$ go build -race -buildmode=plugin ../mrapps/wc.go
之后再main目录下,运行coordinator。
$ rm mr-out*
$ go run -race mrcoordinator.go pg-*.txt
其中pg-*.txt作为参数,是coordinator.go的输入文件;这些文件就是作为Map任务的输入文件。而 -race 是用来检测代码中的race的。
另外,在一个或多个窗口,运行worker:
$ go run -race mrworker.go wc.so
当worker和coordinator都结束后,查看一下输出文件mr-out-*。当年完成实验,你的输出文件的所有内容应该和之前运行main/mrsequential.go的输出文件的内容相匹配。像下面这样:
$ cat mr-out-* | sort | more
A 509
ABOUT 2
ACT 8
...
我们给你提供了测试脚本,是文件main/test-mr.sh。该测试脚本测试对于输入文件pg-xxx.txt,测试wc和indexer在Map Reduce应用程序中是否产生正确输出。同时该脚本能够测试你的实现是否正确,测试你的实现是否是并行运行,测试你的实现是否克服了worker进程的意外崩溃。
如果你现在运行这个测试脚本,它会停住,因为coordinator永不结束:
$ cd ~/6.824/src/main
$ bash test-mr.sh
*** Starting wc test.
你可以修改mr/coordinator.go中的Done函数,修改其中的ret := false改为ret := true,这样coordinator就能马上退出。接着运行测试脚本会看到如下:
$ bash 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任务的输出文件mr-out-X(X为数字)。目前你还没实现mr/coordinator.go和mr/worker.go,所以还没产生这些输出文件,因此所有测试都会像上面一样失败。
当你完成你的工作,运行测试脚本后它的输出应该包含如下内容:
$ bash 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
忽略这个提示,因为注册的coordinator作为RPC服务端会被检查其methods是否应该有RPC固定格式(3个输入,一个error输出),但是我们这里Done不是用于RPC通信,所以无关紧要。
A few rules:
- 在map阶段,所有map函数处理后产生的中间结果,要根据其key分到nReduce个不同的reduce任务中,其中nReduce在main/mrcoordinator.go中作为参数传入MakeCoordinator()函数。
- 实现worker时,它在产生reduce任务的输出文件时文件名应该统一为mr-out-X。(X从0到nReduce-1)
- mr-out-X文件中每行包含一个Reduce函数产生的结果,应该使用Go中的“%v %v”格式,来写入reduce后的key和value。你可以参考main/mrsequential.go,里面就有正确的格式。如果格式相差太多,运行test-mr.sh脚本时可能会出错。
- 你可以改动mr/worker.go,mr/coordinator.go和mr/rpc.go。你也可以为了测试暂时改动其他文件,但是请确保最后运行测试文件时其他文件没有改变。
- 你的worker进程应该把Map的输出存到当前目录的文件,这样之后该文件作为reduce工作的输入能够被读到。
- main/mrcordinator.go要求实现mr/coordinator.go中的Done()方法,当Done返回true时,代表整个mapreduce工作的结束,mrcoordinator.go也会退出。
- 当所有工作结束,worker进程也应该退出。一个简单的办法是,每次调用call()函数时查看其返回值,如果返回值显示不能正常与coordinator通信,那么就假定工作已经结束且coordinator已经退出,因此worker也可以结束。这取决于你的设计,也可以设计“please exit”作为coordinator给worker的伪任务,来提提示worker的退出。
Hints
- 开始的第一个方法是修改mr/worker.go中的Worker()函数,该函数负责发送RPC向coordinator请求任务。如何修改coordinator,让其给worker分发还未开始进行的map任务的相关文件。之后去修改worker去读取这些文件内容,并调用Map函数,就像mrsequential.go做的那样。
- map和reduce函数在运行时通过Go plugin加载进来,就是.so文件。
- 你每次对mr/文件夹下的内容进行改变,都需要重新编译mapreduce的plugin,你都要重新输入命令:go build -race -buildmode=plugin ../mrapps/wc.go。
- 这个实验的worker要共享同一个文件系统。当worker运行在同一机器上时这一点非常显然,但是当worker运行在不同机器上,是需要一个像GFS一样的全局文件系统。
- 对于map产生的中间结果,存于文件当中,其作为reduce的输入,比较合理的文件名是mr-X-Y,其中X是map任务的序号,Y是reduce任务的序号。
- worker的map任务结果要以key/value的键值对形式存于文件,同时reduce读取文件中内容也要以这种形式读取。在GO的encoding/json包中提供了函数,来写key/value键值对:
enc := json.NewEncoder(file)
for _, kv := ... {
err := enc.Encode(&kv)
同时提供了如下函数,来读取key/value键值对:
dec := json.NewDecoder(file)
for {
var kv KeyValue
if err := dec.Decode(&kv); err != nil {
break
}
kva = append(kva, kv)
}
- 在map部分,worker可以通过ihash(key)函数(在worker.go中)来决定该key/value存于那个reduce任务文件中。
- 你可以从mrsequential.go中借鉴文件的读写,以及排序中间key/value键值对的程序。
- 对于coordinator,作为RPC服务端,是并发接受连接请求的,所以不用忘记对共享数据加锁。
- 使用Go的race探测器,也就是在编译和运行时使用-race,像go guild -race和go run -race。test-mr.sh脚本运行时已经加入了-race参数。
- worker有时候需要等待,比如说直到map的工作都结束才能开始reduce相关工作。一种可能的实现是worker每隔一段时间向coordinator请求工作,并在期间使用time.Sleep()。另一种可能的实现是在coordinator中的相关RPC处理函数使用time.Sleep()或sync.Cond等方式循环或等待。Go对每一个RPC都有自己的线程,所以这样的忙等待不会阻碍coordinator对其他RPC的处理。
- coordinator是不能完全区分worker到底是崩溃了,还是因为某些原因长时间停滞,比如执行过慢等。对worker长时间无反应的最好处理方法是,一旦你等待一段时间后,该worker依然未有反应,你可以放弃它,并把该worker正处理的任务交给其他worker。而在这个实验,coordinator将等待10秒,在worker10秒都未有反应后,coordinator假定这个worker已经died(当然,也可能这个worker还在工作着)。
- 如果你选择实现备份(论文section3.6),你的备份任务对应的时间间隔应该安排得更久(比如10秒以上),以免对我们的测试脚本的测试产生影响。(注:备份任务不会被测试)
- 为了测试是否克服崩溃,你可以使用mrapps/crash.go应用plugin,来随机在map或者reduce函数中退出。(注:测试脚本里已经提供相关测试)
- 为了保证在对文件进行写操作当发生崩溃时,人们不会看到多余的文件,MapReduce论文提到了相关技巧——使用临时文件,并且当文件写完后自动将该暂存文件重命名。你可以使用ioutil.TempFile来创建临时文件,使用os.Rename来对其进行重命名。
- test-mr.sh在mt-tmp子目录下运行各进程,如果测试脚本提示错误,而你想查看中间文件和最终结果文件来了解发生了什么,你可以查看那个子目录。你可以调整test-mr.sh,使其在提示失败时退出(否则会继续测试,并且在子目录重新中间文件和最终文件)。
- test-mr-many.sh提供了一个带有超时的test-mr.sh的基本脚本。它将测试次数作为参数。你不能同时运行多个test-mr.sh,因为coordinator将会占用同一个socket,这会造成矛盾。