在这个实验中,你将构建一个 MapReduce 系统。 你将实现一个调用应用程序 Map 和 Reduce 函数并处理文件读写的 worker 进程, 以及一个将任务分配给 worker 并处理 worker 故障的 coordinator 进程。 你将构建类似于 MapReduce 论文 中的系统。 (注意:本实验使用 "coordinator" 代替论文中的 "master"。)
你需要 配置 Go 来完成实验。
使用 git(一个版本控制系统)获取初始实验软件。 要了解更多关于 git 的知识,请查看 Pro Git 书 或 git 用户手册。
$ git clone git://g.csail.mit.edu/6.5840-golabs-2026 6.5840 $ cd 6.5840 $ ls Makefile src $
我们在 src/main/mrsequential.go 中提供了一个简单的串行 mapreduce 实现。它在单个进程中依次运行 map 和 reduce。我们还提供了几个 MapReduce 应用程序:mrapps/wc.go 中的词频统计,以及 mrapps/indexer.go 中的文本索引器。你可以按如下方式串行运行词频统计:
$ cd ~/6.5840 $ cd src/main $ go build -buildmode=plugin ../mrapps/wc.go $ rm mr-out* $ go run mrsequential.go wc.so pg*.txt $ sort mr-out-0 A 509 ABOUT 2 ACT 8 ACTRESS 1 ...(你可能需要设置 LC_COLLATE=C 环境变量以使 sort 产生上述输出:LC_COLLATE=C sort mr-out-0)
mrsequential.go 将其输出留在文件 mr-out-0 中。输入来自名为 pg-xxx.txt 的文本文件。
你可以随意从 mrsequential.go 中借用代码。你还应该查看 mrapps/wc.go 以了解 MapReduce 应用程序代码的样子。
在本实验及所有其他实验中,我们可能会对我们提供的代码进行更新。为了确保你能获取这些更新并使用 git pull 轻松合并,最好将我们提供的代码保留在原始文件中。你可以按照实验说明在我们提供的代码基础上添加内容;只是不要移动它。你可以将自己的新函数放在新文件中。
我们提供了一些初始代码。coordinator 和 worker 的 "main" 例程在 main/mrcoordinator.go 和 main/mrworker.go 中;不要修改这些文件。你应该将实现放在 mr/coordinator.go、mr/worker.go 和 mr/rpc.go 中。
以下是在词频统计 MapReduce 应用程序上运行你的代码的方法。首先,构建词频统计插件:
$ cd main $ go build -buildmode=plugin ../mrapps/wc.go在一个窗口中,运行 coordinator:
$ rm mr-out* $ go run mrcoordinator.go sock123 pg-*.txtsock123 参数指定了 coordinator 接收 worker RPC 的 socket。mrcoordinator.go 的 pg-*.txt 参数是输入文件;每个文件对应一个 "split",是一个 Map 任务的输入。
在一个或多个其他窗口中,运行一些 worker:
$ go run mrworker.go wc.so sock123当 worker 和 coordinator 完成后,查看 mr-out-* 中的输出。当你完成实验后,输出的排序并集应该与串行输出匹配,如下所示:
$ cat mr-out-* | sort | more A 509 ABOUT 2 ACT 8 ACTRESS 1 ...
我们提供了所有用于评分的测试。测试的源代码在 mr/mr_test.go 中。你可以在 src 目录中运行测试:
$ cd src $ make mr ...这些测试检查 wc 和 indexer MapReduce 应用程序在给定 pg-xxx.txt 文件作为输入时是否产生正确的输出。测试还检查你的实现是否并行运行 Map 和 Reduce 任务,以及你的实现是否能从运行任务时崩溃的 worker 中恢复。
如果你现在运行测试,它们会在第一个测试中挂起:
$ cd ~/6.5840/src $ make mr ... cd mr; go test -v -race === RUN TestWc ...
你可以在 mr/coordinator.go 的 Done 函数中将 ret := false 改为 true,使 coordinator 立即退出。然后:
$ make mr ... === RUN TestWc 2026/01/22 14:56:24 reduce created no mr-out-X output files! exit status 1 FAIL 6.5840/mr 4.516s make: *** [Makefile:44: mr] Error 1 $
测试期望在名为 mr-out-X 的文件中看到输出,每个 reduce 任务一个。mr/coordinator.go 和 mr/worker.go 的空实现不会生成这些文件(也基本不做其他任何事情),所以测试失败。
当你完成后,测试输出应该如下所示:
$ make mr ... === RUN TestWc --- PASS: TestWc (8.64s) === RUN TestIndexer --- PASS: TestIndexer (5.90s) === RUN TestMapParallel --- PASS: TestMapParallel (7.05s) === RUN TestReduceParallel --- PASS: TestReduceParallel (8.05s) === RUN TestJobCount --- PASS: TestJobCount (10.04s) === RUN TestEarlyExit --- PASS: TestEarlyExit (6.05s) === RUN TestCrashWorker 2026/01/22 14:58:14 *re*-starting map ../../main/pg-tom_sawyer.txt 0 2026/01/22 14:58:14 *re*-starting map ../../main/pg-metamorphosis.txt 2 2026/01/22 14:58:39 *re*-starting map ../../main/pg-metamorphosis.txt 2 2026/01/22 14:58:40 map 2 already done 2026/01/22 14:58:45 *re*-starting reduce 0 --- PASS: TestCrashWorker (40.18s) PASS ok 6.5840/mr 86.932s $
根据你终止 worker 进程的策略,你可能会看到如下错误:
2026/02/11 16:21:32 dialing:dial unix /var/tmp/5840-mr-501: connect: connection refused每个测试看到少量这些消息是正常的;它们出现在 coordinator 退出后 worker 无法联系 coordinator RPC 服务器时。
enc := json.NewEncoder(file)
for _, kv := ... {
err := enc.Encode(&kv)
读回这样的文件:
dec := json.NewDecoder(file)
for {
var kv KeyValue
if err := dec.Decode(&kv); err != nil {
break
}
kva = append(kva, kv)
}
reply := SomeType{}
call(..., &reply)
不要在调用前设置 reply 的任何字段。如果你传递了具有非默认字段值的 reply 结构体,RPC 系统可能会默默返回不正确的值。
实现你自己的 MapReduce 应用程序(参见 mrapps/* 中的示例),例如分布式 Grep(MapReduce 论文的第 2.3 节)。
让你的 MapReduce coordinator 和 worker 在单独的机器上运行,就像实际部署中那样。你需要设置 RPC 通过 TCP/IP 而不是 Unix socket 进行通信(参见 Coordinator.server() 中被注释掉的行),并使用共享文件系统读写文件。例如,你可以在 MIT 的多个 Athena 集群机器上使用 ssh,它们使用 AFS 共享文件;或者你可以租用几个 AWS 实例并使用 S3 进行存储。