Lab1 MapReduce

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 类型没见过")
		}

	}
}

实现流程

  1. coordinator 开启监听,开启rpc 服务器等待远程调用
  2. worker开启开始循环调用 TaskAsk 来获取任务
  3. coordinator中TaskAsk 遍历 maptasks,找到没有发送的 send是false的。 发送该任务后,开启计时
  4. 计时10s后判断这个任务的 finish 是否为 true, 如果不是,那么将send 改为重新改为false重新加入分发组中
  5. worker 执行 map 任务完成后,调用onemapfinish后 将该map任务的finish 改成true 任务调用成功
  6. 只有所有的map任务完成后 才能够开始分发map任务,所以coordinator 中当所有的 map任务发派出去后,但是并没有全部完成,这时候worker 来寻找任务,coordinator 发送wait信号,worker 等待一段时间后重新去找任务
  7. 继续分发reduce任务
  8. 当所有reduce任务finish 后, 就会发送ALLDone给worker 双方都可以退出。

感想与踩坑

了解具体要干什么后 ,整体的过程还是比较顺利,最后改bug 也就只有几个小bug,当然也得打半天日志才找到bug的位置所在。

踩坑:

  1. 第一个bug 是 不熟悉go语言,rpc 中struct 变量首字母大小写随意设置,导致很奇怪的问题。比如 worker收到的 maprespoense 中 filename 是正确的,但是mapnum是错误的,找了半天,最后才试出来,很恶心。
  2. 第二个bug 。worker中我最开始的实现是在 for{}前面 创建了 TaskAskRequest{} TaskAskResponse{} 这两个变量,这时候会发现map任务发送是正常的,而worker接收也是大部分正常,只有mapnum == 0的任务会重复 上一个的编号,尽管filename还是对的,又是一个奇奇怪怪的bug,当然后来发现把两个rpc 的结构体放在for后面创建就对了。可能每次重新for循环的时候,使用之前创建的结构体会有问题,当然我还没具体找出为啥。
  3. 第三个bug是测试的时候所有的test都是对的,只有reduce parallelism 测试任务 显示 too few parallelism。也就是太少并发了,查看了相应的 riming.go 和测试脚本后,发现脚本是开启两个woker,当只有一个worker执行了reduce任务会出现这个错误。后来发现设置的 worker 的wiat时间是10s,哈哈哈,这样一个woker wait 10s后,那估计所有的reduce任务都被另外一个worker接走了,自然reduce就没有并发了,将wait 时间改成1s bug解除。所有任务完成。

在这里插入图片描述

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MIT 6.824 课程的 Lab1 是关于 Map 的实现,这里单介绍一下实现过程。 MapReduce 是一种布式计算模型,它可以用来处理大规模数据集。MapReduce 的核心想是将数据划分为多个块,每个块都可以在不同的节点上并行处理,然后将结果合并在一起。 在 Lab1 中,我们需要实现 MapReduce 的基本功能,包括 Map 函数、Reduce 函数、分区函数、排序函数以及对作业的整体控制等。 首先,我们需要实现 Map 函数。Map 函数会读取输入文件,并将其解析成一系列键值对。对于每个键值对,Map 函数会将其传递给用户定义的 Map 函数,生成一些新的键值对。这些新的键值对会被分派到不同的 Reduce 任务中,进行进一步的处理。 接着,我们需要实现 Reduce 函数。Reduce 函数接收到所有具有相同键的键值对,并将它们合并成一个结果。Reduce 函数将结果写入输出文件。 然后,我们需要实现分区函数和排序函数。分区函数将 Map 函数生成的键值对映射到不同的 Reduce 任务中。排序函数将键值对按键进行排序,确保同一键的所有值都被传递给同一个 Reduce 任务。 最后,我们需要实现整个作业的控制逻辑。这包括读取输入文件、调用 Map 函数、分区、排序、调用 Reduce 函数以及写入输出文件。 Lab1 的实现可以使用 Go 语言、Python 或者其他编程语言。我们可以使用本地文件系统或者分布式文件系统(比如 HDFS)来存储输入和输出文件。 总体来说,Lab1 是一个比较简单的 MapReduce 实现,但它奠定了 MapReduce 的基础,为后续的 Lab 提供了良好的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值