Lab1 MapReduce
读完谷歌的MapReduce的论文,基本上就会对这个分布式模型设计有了初步的认识,这里就不过多介绍。
来具体看看lab1的 任务要求
task: 在现有的顺序MapReduce程序中,设计一个分布式的实现。包括coordinator以及worker。worker通过RPC与coordinator通信,每一个woker都会询问一个任务,从文件中读取输入,执行任务,并且将输出写入到files中。coordinator负责检测一段时间(10 s)后worker如果没有完成这个任务,那么会将这个任务分发给其他的worker。
Sequential MapReduce 的实现
在实现分布式的mapreduce之前,可以看看给出的代码中顺序版本是如何实现的。mapreudce是可以处理一系列功能的分布式模型,然而单一代码如何实现不同的功能呢,总不能直接修改源码吧。 这里用到了 go 中的加载插件的方法,具体的 go 语言我也不是很懂,我只是讲讲这里的使用。 将不同的map 和 reduce 方法的实现写入到不同的文件中(文件夹 mrapps下,如wc.go), 在使用的时候通过go build -race -buildmode=plugin
来生成.so文件(动态链接库),在运行worker的时候动态链接上就好,通过 loabPlugin()
方法来读取这些方法文件,加载到程序中就可以正常使用了
//mrsequential.go
func loadPlugin(filename string) (func(string, string) []mr.KeyValue, func(string, []string) string) {
p, err := plugin.Open(filename)
if err != nil {
log.Fatalf("cannot load plugin %v", filename)
}
xmapf, err := p.Lookup("Map")
if err != nil {
log.Fatalf("cannot find Map in %v", filename)
}
mapf := xmapf.(func(string, string) []mr.KeyValue) //类型转换,转换成对应的方法
// 这种断言的类型转换的好处是能够返回err,
// 看是否有错误 当然这里只用一个返回值
xreducef, err := p.Lookup("Reduce")
if err != nil {
log.Fatalf("cannot find Reduce in %v", filename)
}
reducef := xreducef.(func(string, []string) string)
return mapf, reducef
}
来看看sequential mapredeuce 如何实现:
// for sorting by key. 这里 []KeyValue 的形式是一种切片slice,切片的内容是KeyValue 也就是 By是一个指针
// ByKey是一个interface 实现了sort的几个比较的方法 长度 交换 大小
type ByKey []mr.KeyValue
// for sorting by key.
// 满足 sort 包中 sort.Interface 的要求,以便对切片进行排序
func (a ByKey) Len() int { return len(a) }
func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByKey) Less(i, j int) bool { return a[i].Key < a[j].Key } //比较key的大小
func main() {
if len(os.Args) < 3 {
fmt.Fprintf(os.Stderr, "Usage: mrsequential xxx.so inputfiles...\n")
os.Exit(1)
}
mapf, reducef := loadPlugin(os.Args[1])
//
// read each input file,
// pass it to Map,
// accumulate the intermediate Map output.
//遍历 输入的需要进行 map 的文件,执行map 并且保存keyvalue值
intermediate := []mr.KeyValue{}
for _, filename := range os.Args[2:] {
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()
kva := mapf(filename, string(content))
intermediate = append(intermediate, kva...)
}
//
// a big difference from real MapReduce is that all the
// intermediate data is in one place, intermediate[],
// rather than being partitioned into NxM buckets.
//
sort.Sort(ByKey(intermediate)) //按照key进行排序
oname := "mr-out-0"
ofile, _ := os.Create(oname)
//
// call Reduce on each distinct key in intermediate[],
// and print the result to mr-out-0.
//
//执行reduce 将 相同的key值的value进行整合相加
i := 0
for i < len(intermediate) {
j := i + 1
//计算有多少个该key值
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)
}
output := reducef(intermediate[i].Key, values)
// this is the correct format for each line of Reduce output.
fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)
i = j
}
ofile.Close()
}
总的来说 sequential的 mapreduce 还是很简单的。
分布式MapReduce实现
Coordinator 端实现
main 文件夹中直接运行mrcoordinator 文件
func main() {
if len(os.Args) < 2 { //读取参数 go run mrcoordinator. pg.txt
fmt.Fprintf(os.Stderr, "Usage: mrcoordinator inputfiles...\n")
os.Exit(1)
}
//创建一个coordinator 传入的是 files 和 指定的reduce
m := mr.MakeCoordinator(os.Args[1:], 10)
for m.Done() == false { //循环检测是否有结束
time.Sleep(time.Second)
}
time.Sleep(time.Second)
}
创建makecoordinator后直接 循环判断reduce是否全部完成。
//这个是比较重要的结构体,里面的锁可能有优化的空间
type Coordinator struct {
// Your definitions here.
MapTasks map[int]*MapTask
// MapTask map[string]bool //待处理的map任务 map[文件名字]是否完成
ReduceTasks map[int]*ReduceTask
MapMux sync.Mutex
// ReduceTask map[string]bool //待处理的reduce任务
ReduceMux sync.Mutex
NReduce int //设置有几个reduce任务,也就是map worker要输出的文件的哈希取模值
MapNum int //当前还剩几个mapNum没有被finished
ReduceNum int //当前还剩几个reduceNum没有有finished
}
// create a Coordinator.
// main/mrcoordinator.go calls this function.
// nReduce is the number of reduce tasks to use.
func MakeCoordinator(files []string, nReduce int) *Coordinator {
// println("一共由", len(files), "个map任务")
c := Coordinator{
MapTasks: make(map[int]*MapTask),
// ReduceTask: make(map[string]bool),
ReduceTasks: make(map[int]*ReduceTask),
NReduce: nReduce,
ReduceNum: nReduce,
}
//创建 nreduce个任务
for i := 0; i < nReduce; i++ {
thiReduceTask := new(ReduceTask)
thiReduceTask.Send = false
thiReduceTask.Finish = false
c.ReduceTasks[i] = thiReduceTask
}
//遍历文件 添加进map任务中
n := 0
for _, str := range files {
thisMapTask := new(MapTask)
// thisMapTask := MapTask{} //这是在栈上创建
thisMapTask.FileName = str
thisMapTask.Finish = false
thisMapTask.Send = false
c.MapTasks[n] = thisMapTask
n = n + 1
}
c.MapNum = n //一共有几个map任务,即有几个文件
// Your code here.
//执行rpc服务器
c.server()
return &c
}
// start a thread that listens for RPCs from worker.go
func (c *Coordinator) server() {
rpc.Register(c) //将coordinator 注册到rpc上
rpc.HandleHTTP() //通过http进行rpc通信
//l, e := net.Listen("tcp", ":1234") 这是真正的分布式 应该用tcp通信
sockname := coordinatorSock() //本地通信的地址,是通过一个本地文件来实现通信
os.Remove(sockname)
l, e := net.Listen("unix", sockname)
if e != nil {
log.Fatal("listen error:", e)
}
go http.Serve(l, nil) //开启服务器协程
}
开启server 服务器后 就是等待rpc 远程调用 coordinator的各种方法,方法如下:
type MapTask struct {
FileName string //文件名
Send bool //是否被发布
Finish bool //是否有被完成
}
type ReduceTask struct {
Send bool //是否被发布
Finish bool //是否有被完成
}
//分发任务后 需要计时,防止worker 失效
func (c *Coordinator) startCounting(Tasktype int, key int) {
if Tasktype == 0 {
//如果是map任务计时
time.Sleep(time.Second * 10) //睡眠10秒等待 任务完成
c.MapMux.Lock()
defer c.MapMux.Unlock()
if c.MapTasks[key].Finish {
//如果完成了 结束该任务
return
} else {
//没有完成 直接将该任务设置为没有发送状态
c.MapTasks[key].Send = false //要重新发布了
return
}
} else {
//如果是reduce任务计时
time.Sleep(time.Second * 10) //睡眠10秒等待 任务完成
c.ReduceMux.Lock()
defer c.ReduceMux.Unlock()
if c.ReduceTasks[key].Finish {
//如果完成了 结束该任务
return
} else {
//没有完成 直接将该任务设置为没有发送状态
c.ReduceTasks[key].Send = false //要重新发布了
return
}
}
}
//worker 来调用 ,获取map/reduce 的任务
func (c *Coordinator) TaskAsk(args *TaskAskRequest, reply *TaskAskResponse) error {
//遍历所有的map任务,找到 一个send 为false的的任务
//遍历的过程 需要锁上
c.MapMux.Lock()
defer c.MapMux.Unlock()
for key, value := range c.MapTasks {
if value.Send { //如果是 true 那么就是被分发了 就不管了
continue
} else {
reply.Filename = value.FileName
reply.TaskType = 0
reply.NReduce = c.NReduce
reply.MapNum = key //第几个map任务
c.MapTasks[key].Send = true //表示当前任务已经发派
// println("cor中 map任务发派出去了", reply.MapNum)
// println("cor中 map任务的名字", reply.Filename)
//分配任务后要开始计时等待
go c.startCounting(0, key)
return nil
}
}
if c.MapNum > 0 {
//如果所有任务都是send 但是没有全部是finished的 那么就发送wait的信息
reply.Wait = true
return nil
}
// println("开始分发reduce任务了")
//如果所有的map任务全部完成了 就可以分发reduce任务
c.ReduceMux.Lock()
defer c.ReduceMux.Unlock()
for key, value := range c.ReduceTasks {
if value.Send {
continue
} else {
// println("cor中 reduce任务", key)
reply.TaskType = 1
reply.ReduceNum = key
c.ReduceTasks[key].Send = true
// println("cor中 reduece任务发派出去了", key)
go c.startCounting(1, key)
return nil
}
}
if c.ReduceNum > 0 {
//如果所有任务都是send 但是没有全部是finished的 那么就发送wait的信息
reply.Wait = true
return nil
}
//没任务了就直接返回没任务
reply.AllDone = true
return nil
}
//map完成后worker调用
func (c *Coordinator) OneMapFinish(args *MapFinishRequest, reply *MapFinishResponse) error {
c.MapMux.Lock()
defer c.MapMux.Unlock()
c.MapTasks[args.MapNum].Finish = true
// println(args.MapNum, "该map任务完成")
c.MapNum = c.MapNum - 1
// println("还剩下map任务", c.MapNum)
return nil
}
//reduce完成后worker调用
func (c *Coordinator) OneReduceFinish(args *ReduceFinishRequest, reply *ReduceFinishResponse) error {
c.ReduceMux.Lock()
defer c.ReduceMux.Unlock()
c.ReduceTasks[args.ReducepNum].Finish = true
// println(args.ReducepNum, "该reduce任务完成")
c.ReduceNum = c.ReduceNum - 1
// println("还剩下reduce任务", c.ReduceNum)
return nil
}
//循环判断是否 能够循环结束 推出 coordinator
func (c *Coordinator) Done() bool {
ret := false
c.ReduceMux.Lock()
defer c.ReduceMux.Unlock()
if c.ReduceNum == 0 { //这个地方是否需要锁? 这个地方只读应该不需要锁吧,但好像还是会有data race 还是锁上吧
ret = true
}
// Your code here.
return ret
}
Worker
type ByKey []KeyValue
// for sorting by key.
func (a ByKey) Len() int { return len(a) }
func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByKey) Less(i, j int) bool { return a[i].Key < a[j].Key } //比较key的大小
// Map functions return a slice of KeyValue.
type KeyValue struct {
Key string
Value string
}
// use ihash(key) % NReduce to choose the reduce
// task number for each KeyValue emitted by Map.
func ihash(key string) int {
h := fnv.New32a()
h.Write([]byte(key))
return int(h.Sum32() & 0x7fffffff)
}
// main/mrworker.go calls this function.
//
// 通过RPC判断要分配的任务是什么
func Worker(mapf func(string, string) []KeyValue,
reducef func(string, []string) string) {
// Your worker implementation here.
//获得需要操作的文件 以及任务的类型 Map or reduce
for { //循环获取任务
//远程调用TaskAsk,获取任务
//每次rpc之前都要创建全新的这两个
args := TaskAskRequest{}
reply := TaskAskResponse{}
args.WorkID = 1 //暂定为1
// println("正在寻求任务")
call("Coordinator.TaskAsk", &args, &reply)
if reply.AllDone {
//所有任务都完成了 可以退休了 *****
return
}
//如果是wait 那么 就等待1s再去询问任务
if reply.Wait {
time.Sleep(time.Second * 1)
continue
}
taskType := reply.TaskType
if taskType == 0 {
//执行map任务
filename := reply.Filename
nReduce := reply.NReduce
mapNum := reply.MapNum
// println("已经接收到map任务 ", reply.MapNum)
// println("已经接收到任务map名字 ", filename)
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()
kva := mapf(filename, string(content))
sort.Sort(ByKey(kva)) //直接就对kva 排序,而不是对文件排序
// intermediate = append(intermediate, kva...)
// sort.Sort(ByKey(intermediate)) //按照key进行排序
//得到所有的的kv对 都在 kva数组里面,现在要将他们分别写入不同文件中
//创建10个临时文件
fileMap := make(map[int]*os.File)
for i := 0; i < nReduce; i++ {
file, _ := ioutil.TempFile("", "Temp")
fileMap[i] = file
}
for _, kv := range kva {
reduceNum := ihash(kv.Key) % nReduce
// interFileName := fmt.Sprintf("mr-%v-%v", mapNum, reduceNum)
// interFile, err := os.OpenFile(interFileName, os.O_WRONLY|os.O_CREATE, 0666)
// interFile, err := os.Create(interFileName)
// if err != nil {
// log.Fatalf("cannot open %v", interFileName)
// }
enc := json.NewEncoder(fileMap[reduceNum])
err1 := enc.Encode(&kv)
if err1 != nil {
log.Fatal(err1)
}
}
//当写入的临时文件全部写完后 将10个文件夹全部转化为该有的名字,原子命名
for num, file := range fileMap {
interFileName := fmt.Sprintf("mr-%v-%v", mapNum, num)
os.Rename(file.Name(), interFileName)
}
//rpc告诉coordinator map任务完成
req := MapFinishRequest{}
// req.mapFileName = filename
req.MapNum = mapNum
reply := MapFinishResponse{}
call("Coordinator.OneMapFinish", &req, &reply)
// println("wor中 map任务", req.MapNum, "已经完成")
// oname := fmt.Sprint("mr-%v-0", filename) //map输出的中间文件
// ofile, _ := os.Create(oname)
} else if taskType == 1 {
//执行reduce任务
var wg sync.WaitGroup
var mu sync.Mutex
kva := []KeyValue{} //所有的key和vaule值
reduceNum := reply.ReduceNum
// println("已经接收到reduce任务 ", reply.ReduceNum)
//这时目标要写入的数据
oname := fmt.Sprintf("mr-out-%v", reduceNum)
ofile, _ := os.Create(oname)
//打开所有的名字中包含 mr-*-reduceNum的文件
filenames, err := filepath.Glob(fmt.Sprintf("mr-*-%v", reduceNum))
if err != nil {
fmt.Printf("Error matching pattern: %v\n", err)
return
}
/** 串行处理
for _, filename := range filenames {
// 打开文件
file, err := os.Open(filename)
if err != nil {
fmt.Printf("Error opening file %s: %v\n", filename, err)
continue
}
defer file.Close()
dec := json.NewDecoder(file)
for {
var kv KeyValue
if err := dec.Decode(&kv); err != nil {
break
}
kva = append(kva, kv)
}
// 处理打开的文件
// ...
}**/
//多协程进行解码
for _, filename := range filenames {
wg.Add(1)
go func(filename string) {
defer wg.Done()
file, err := os.Open(filename)
if err != nil {
fmt.Printf("Error opening file %s: %v\n", filename, err)
return
}
defer file.Close()
dec := json.NewDecoder(file)
for {
var kv KeyValue
if err := dec.Decode(&kv); err != nil {
break
}
mu.Lock()
kva = append(kva, kv)
mu.Unlock()
}
}(filename)
}
wg.Wait()
sort.Sort(ByKey(kva)) //按照key再次进行排序
intermediate := kva
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)
}
output := reducef(intermediate[i].Key, values)
// this is the correct format for each line of Reduce output.
fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)
i = j
}
//告诉coordinator 我reduce完成了
req := ReduceFinishRequest{}
// req.mapFileName = filename
req.ReducepNum = reduceNum
reply := ReduceFinishResponse{}
call("Coordinator.OneReduceFinish", &req, &reply)
} else {
fmt.Println("task 类型没见过")
}
}
}
实现流程
- coordinator 开启监听,开启rpc 服务器等待远程调用
- worker开启开始循环调用
TaskAsk
来获取任务 - coordinator中TaskAsk 遍历 maptasks,找到没有发送的
send
是false的。 发送该任务后,开启计时 - 计时10s后判断这个任务的 finish 是否为 true, 如果不是,那么将send 改为重新改为false重新加入分发组中
- worker 执行 map 任务完成后,调用
onemapfinish
后 将该map任务的finish 改成true 任务调用成功 - 只有所有的map任务完成后 才能够开始分发map任务,所以coordinator 中当所有的 map任务发派出去后,但是并没有全部完成,这时候worker 来寻找任务,coordinator 发送wait信号,worker 等待一段时间后重新去找任务
- 继续分发reduce任务
- 当所有reduce任务finish 后, 就会发送
ALLDone
给worker 双方都可以退出。
感想与踩坑
了解具体要干什么后 ,整体的过程还是比较顺利,最后改bug 也就只有几个小bug,当然也得打半天日志才找到bug的位置所在。
踩坑:
- 第一个bug 是 不熟悉go语言,rpc 中struct 变量首字母大小写随意设置,导致很奇怪的问题。比如 worker收到的 maprespoense 中 filename 是正确的,但是mapnum是错误的,找了半天,最后才试出来,很恶心。
- 第二个bug 。worker中我最开始的实现是在 for{}前面 创建了
TaskAskRequest{} TaskAskResponse{}
这两个变量,这时候会发现map任务发送是正常的,而worker接收也是大部分正常,只有mapnum == 0的任务会重复 上一个的编号,尽管filename还是对的,又是一个奇奇怪怪的bug,当然后来发现把两个rpc 的结构体放在for后面创建就对了。可能每次重新for循环的时候,使用之前创建的结构体会有问题,当然我还没具体找出为啥。 - 第三个bug是测试的时候所有的test都是对的,只有reduce parallelism 测试任务 显示 too few parallelism。也就是太少并发了,查看了相应的 riming.go 和测试脚本后,发现脚本是开启两个woker,当只有一个worker执行了reduce任务会出现这个错误。后来发现设置的 worker 的wiat时间是10s,哈哈哈,这样一个woker wait 10s后,那估计所有的reduce任务都被另外一个worker接走了,自然reduce就没有并发了,将wait 时间改成1s bug解除。所有任务完成。