前言
MIT 6.824 是麻省理工大学的一门研究生课程——Distributed Systems,学习这门课程对于了解分布式系统的构建原理、理解分布式程序的运行、优化分布式程序的运行环境会有很大的帮助。课程内容涵盖:分布式、容错、多副本、一致性等议题,附带了 4 个大的实验 Lab 并配套了相关的测试用例,需要基于 Go 语言完成。Lab 会将课程所讲的知识进行实践、贯通,有助于加深我们的理解和记忆。
Lab-1 主要是实现MapReduce,编写 mr 文件夹下的 master.go、rpc.go、worker.go 三个文件,完成后在main文件加下执行如下命令,来测试你的程序是否能输出正确结果。
bash ./test-mr.sh
一、MapReduce原理
通过阅读论文,我们可以知道 MapReduce 的结构:
MapReduce 执行过程:
- 用户程序中的 MapReduce 库首先将输入文件分割为 M 块;然后在集群上启动许多该程序的副本;
- 然后在集群上启动许多该程序的副本,其中一个副本是 master,其余的是 worker。总共有 M 个 map 任务和 R 个 reduce 任务,需要 master 挑选一个空闲的 worker 进行分配;
- 分配了 map 任务的 worker 读取相应输入 split 的内容。它从输入数据中解析 key-value 对,并将每对传递给用户定义的 map 函数。 map 函数生成的中间 key-value 对缓冲在内存中;
- 定期将key-value 对写入本地磁盘,并通过分区函数将其分为 R 个区域。这些key-value 对在本地磁盘上的位置被传回 master,主节点负责将这些位置转发给 reduce 工作节点;
- 当一个 reduce worker 被通知位置后,通过 RPC 去读取本地磁盘上的 key-value 对,(并排序);
- reduce worker 遍历所有的已排序key-value 对,将唯一的 key 和 value 对集合传递给用户的 reduce 函数,函数输出到最终文件(最多 R 个);
- 当所有的 reduce 完成任务后,master 唤醒(通知)用户程序。
二、实验过程
1.编写 worker
每个 worker 只管接受任务、执行计算、返回结果,不需要管别的,所以可以先从 Worker 写起。
每当 Worker 通过 RPC 向 Master 发送心跳请求的时候,会收到 3 种可能的回应:
- heat:Master 的回应心跳,代表现在 Master 没有任务可分配,Worker 暂时空闲;
- maptask:Master 向自己分配了一个 Map 任务,附带的信息有任务编号 X、输入文件路径、分区数 R;
- reducetask:Master 向自己分配了一个 Reduce 任务,附带的信息有任务编号 Y、所有的中间文件路径;
① 处理map任务
// 获取任务指定的文件名
filename := reply.Task
file, err := os.Open(filename)
if err != nil {
log.Fatalf("cannot open %v", filename)
}
// 读取文件全部内容
content, err := ioutil.ReadAll(file)
if err != nil {
log.Fatalf("cannot read %v", filename)
}
file.Close()
// 使用map函数处理文件内容,生成 key-value 对切片
kva := mapf(filename, string(content))
// 初始化buckets,用于存放分组后的键值对,每个bucket对应一个reduce任务
buckets := make([][]KeyValue, nReduce)
for i := 0; i < nReduce; i++ {
buckets[i] = []KeyValue{}
}
// 根据 key-value 对的Key进行哈希并分配到对应的bucket中
for _, kv := range kva {
buckets[ihash(kv.Key)%nReduce] = append(buckets[ihash(kv.Key)%nReduce], kv)
}
// 对每个bucket,创建并写入到中间文件
for i, bucket := range buckets {
oname := "mr-" + strings.Replace(reply.Task[3:(len(reply.Task)-4)], "-", "_", -1) + "-" + strconv.Itoa(i)
// 创建临时文件以写入数据
ofile, _ := os.CreateTemp("", oname)
// 使用JSON编码写入键值对
enc := json.NewEncoder(ofile)
for _, kv := range bucket {
if err := enc.Encode(&kv); err != nil {
log.Fatalf("cannot write %v", oname)
}
}
// 将临时文件重命名为正式的中间文件名
os.Rename(ofile.Name(), oname)
}
② 处理 reduce 任务
// 若没有可分配的Reduce任务,则等待一秒后重新尝试执行Worker
if reply2.ReduceNum < 0 {
time.Sleep(1 * time.Second)
Worker(mapf, reducef)
return
}
// 减一操作,因为reduceNum是从0开始的,但分配时减1以匹配任务编号
reply2.ReduceNum -= 1
reduceNum := reply2.ReduceNum
// 初始化变量和目录路径,准备读取中间文件
dir := "./"
suffix := strconv.Itoa(reduceNum)
kva := []KeyValue{} // 用于存储读取的所有键值对
intermediate := []KeyValue{} // 中间结果存储
var countA = 0
// 遍历指定目录下以reduceNum为后缀的文件
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// 只处理文件且文件名以reduceNum为后缀
if !info.IsDir() && strings.HasSuffix(path, suffix) {
file, err := os.Open(path)
defer file.Close() // 确保文件最终关闭
if err != nil {
return err
}
dec := json.NewDecoder(file) // 创建解码器读取JSON文件
// 读取并解析文件中的每一个Key-Value
for {
var kv KeyValue
if err := dec.Decode(&kv); err != nil {
if err == io.EOF { // 文件结束,正常退出循环
break
}
return err // 其他错误则直接返回
}
if kv.Key == "A" { // 特殊处理键为"A"的情况(示例特定逻辑)
countA++
}
kva = append(kva, kv) // 添加到键值对列表
}
}
return nil // 继续遍历
})
// 将收集到的键值对添加到中间结果列表
intermediate = append(intermediate, kva...)
if err != nil {
}
// 排序中间结果,便于后续reduce操作
sort.Sort(ByKey(intermediate))
// 准备输出文件
oname := "mr-out-" + strconv.Itoa(reduceNum)
ofile, _ := os.CreateTemp("", oname)
// 遍历排序后的键值对,执行reduce操作并写入输出文件
i := 0
for i < len(intermediate) {
j := i + 1
// 查找相同键的连续区间
for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {
j++
}
values := []string{} // 存储相同键的值
for k := i; k < j; k++ {
values = append(values, intermediate[k].Value)
}
// 应用reduce函数处理相同键的值,得到输出
output := reducef(intermediate[i].Key, values)
// 将处理结果写入文件,遵循Reduce输出的格式
fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)
// 移动到下一个不同的键
i = j
}
os.Rename(ofile.Name(), oname)
2.编写 msater
Msater的数据结构,Msater主要的功能是分配map任务和reduce任务
// 协调者,就是 Msater
type Coordinator struct {
mutex sync.Mutex
mapIndex int
reduceIndex int
files []string
map1 map[int]bool
reducePhase bool
nReduce int
}
总结
测试全部通过