MIT6.824Lab1代码与思路


最近有了点空闲,想着学一下师兄推荐的MIT6.824,go和分布式都是第一次接触,慢慢学慢慢做,记录一下。

1.推荐资料

B站爱学习的阿噜Lab1实验:没接触过go和ubuntu上bash编程以及不会跑示例实验的同学可以看一下这个。
B站课程视频:这个是翻译版本,英文不好的同学可以看这个,而且下面也有分享资料什么的。
博主东东儿的代码原贴:借鉴了很多思路,做这个实验每天都要review人家代码好几遍,写得简洁又漂亮,思路清晰。

2.实验文件介绍

我也是第一次做go和在ubuntu上编程,看懂课程源文件也花了不少时间,不懂实验的同学可以看一下这部分,可以让实验简单很多。
main/mrsequential.go:这个是官方给的一个单机版map-reduce程序,不熟悉go的同学可以借鉴里面main()函数中有map和reduce的文件创建读写以及函数调用等代码。
main/mrworker.go和main/mrcoordinator.go:这个是官方给的调用程序,按照规则这里是不许更改的,不过看懂可以更清楚整个程序是如何调用的。
mrapps:这个文件夹下给出了不同测试的mapf和reducef,这里也注意,worker方法并不需要自己写mapf和reducef,人家也不允许,参照main/mrsequential.go中直接进行使用即可
mr/coordinator.go:这里是master的结构体与方法,一开始还以为是master通过coordinator与worker交流,后来才发现是课程lab版本不一样,master就是coordinator,本文使用coordinator
mr/rpc.go:定义rpc结构体,用以worker与coordinator通讯
mr/coordinator.go:定义worker的方法,这里要注意main/mrsequential.go很多方法看起来像系统方法(如ByKey)其实是要自己实现接口的
main/test-mr.sh测试文件,每个部分之间用#隔开了,如果某一部分不通过可以将其他部分注释然后单独跑,此外这个文件会删除产生的中间件,如果有debug需要,也可以注释掉相关代码(如rm -f mr-out*这些)

3.实验流程与思路

3.1实验思路

bash运行mrcoordinator.go创建coordinator,然后运行mrworker.go创建数个workerworker通过rpc通讯找coordinator要任务,coordinator返回map任务,worker报告完成,完成所有map任务后,worker要任务,coordinator返回reduce任务,worker完成后报告给coordinator再进行下一项测试
要点:

  1. coordinator在分发完所有map任务后,需要等待所有map任务都提交再分发reduce任务。因为可能会有reduce读取未完成的map文件产生冲突,而且按照官方思路,reduce应当是交错读。如果你有合适的方法对已经完成的map任务进行标记不出错,不进行等待也可以,但是等待比较简单。
  2. 输出文件和中间件最好都按照官方建议,不过对bash编程很熟悉可以按照测试文件代码要求来写。
  3. 设计数据结构记录coordinator目前状态以及map和reduce的进展。
  4. 编写代码时可以先写出大体架构,跑通后再添加细节。
  5. 通过定义const让代码更间接

3.2RPC结构体

用以与coordinator通讯,这里建议把申请与报告任务结束分开写,可以让整个思路更清晰,有些字段让我用作不同的地方了,其实这里写的有点粗糙,按理说不应该这样设计。其中map与reduce的任务申请和reduce的任务结束不需要传递信息。

package mr
// RPC definitions.
// remember to capitalize all names.
import "os"
import "strconv"
// example to show how to declare the arguments
// and reply for an RPC.
//get task rpc 
type GetTaskRequest struct {
	Index int
}
type GetTaskReply struct {
	//State 0 sleep 1 map 2 reduce 3 finish
	TaskType int
	// file name to store Map result,used both in map and reduce
	TaskName string
	// reduce worker num
	ReduceNum int 
	// num of this worker
	Index int
	InputName []string
	//OutputName []string
}
//response task rpc
type FinishTaskRequest struct {
	TaskType int
	TaskName string
	FileName  []string
}
type FinishTaskReply struct {
	Repltype int
}
// Cook up a unique-ish UNIX-domain socket name
// in /var/tmp, for the coordinator.
// Can't use the current directory since
// Athena AFS doesn't support UNIX-domain sockets.
func coordinatorSock() string {
	s := "/var/tmp/824-mr-"
	s += strconv.Itoa(os.Getuid())
	return s
}

3.3coordinator结构体

设const或者用枚举方法可以优化代码,此外Coordinator中有得参数没有用到,但我懒得改了

const (
	Sleep=iota
	Map
	Reduce
	Finish
)
const (
	Working=iota
	Timeout
)
const (
	NotStarted=iota
	Processing
	Finished
)
type Coordinator struct {
	// Your definitions here.
	// Num of Worker for reduce proess
	NumReduceWorker int
	// Num of input file part
	NumMapFile int
	//State 0 sleep, 1 map, 2 reduce, 3 finish 
	State int
	//map任务产生的中间件存储 
	ReduceRecord map[int]string
	//List for record which file is busy
	Mapfiles map[string]int
	Reducefiles map[string]int
	Mux sync.Mutex
	//记录现有map任务的数量
	MapTaskNum int
}

3.4coordinator功能实现

coordinator所有可能访问共享代码,结构,文件,数据的内容都需要加锁,而且由于coordinator代码运行很快,一个偷懒的做法是所有的方法都加锁,除了部分代码需要等待一段时间。
获取任务代码,先加锁,然后判断目前的状态,最后根据不同状态返回任务需求,并记录以及开启超时等待

func (c *Coordinator) GetTask(args *GetTaskRequest, reply *GetTaskReply) error {
	c.Mux.Lock()
	defer c.Mux.Unlock()
	if c.State == Sleep{
		reply.TaskType = Sleep
		return nil
	}else if c.State == Finish{
		reply.TaskType = Finish
		return nil
	}else if c.State == Map{
		reply.TaskType = Map
		//给worker一个编号
		if args.Index == -1{
			reply.Index = c.MapTaskNum
			c.MapTaskNum++
		}
		// map 的逻辑,查看哪个文件是空闲的,分发给worker,
		for k,v := range c.Mapfiles{
			if v == NotStarted{
				reply.TaskName = k
				reply.ReduceNum = c.NumReduceWorker
				// 记录该文件正在工作,启动超时等待
				c.Mapfiles[k] = Processing
				go c.HandleTimeOut(Map,k)
				return nil
			}
		}
		//没有需要进行的map任务,返回sleep
		reply.TaskType = Sleep
		return nil
	}else if c.State == Reduce{
		reply.TaskType = Reduce
		for k,_ := range c.Reducefiles{
			if c.Reducefiles[k] == NotStarted{
				// reduce 的逻辑,获取需要reduce的文件名
				tempname,_ := strconv.Atoi(k)
				files := strings.Split(c.ReduceRecord[tempname], " ") 
				c.Reducefiles[k] = Processing
				reply.InputName = files
				go c.HandleTimeOut(Reduce, k)
				return nil
			}
		}
		reply.TaskType = Sleep
		return nil
		// 记录正在reduce的文件,启动超时等待
	}else{
		log.Fatal("wrong state")
	}
	return nil
}

报告任务代码,流程同申请任务,不过要加一步判断当前map/reduce过程是否已经完成,完成了就进入下一阶段

func (c *Coordinator) ResponseTask(args *FinishTaskRequest, reply *FinishTaskReply) error {
	c.Mux.Lock()
	defer c.Mux.Unlock()
	if args.TaskType == Map{
		if c.Mapfiles[args.TaskName] == Processing{
		c.Mapfiles[args.TaskName] = Finished
		// 存储中间件文件名
		for _,v := range args.FileName{
			tempstring := strings.Split(v, "-")
			index := tempstring[len(tempstring)-1]
			temp, err := strconv.Atoi(index)
			if err != nil {
				return nil
			}
			if c.ReduceRecord[temp] != ""{
				c.ReduceRecord[temp] = c.ReduceRecord[temp]+ " " + v
			}else{
				c.ReduceRecord[temp] = v
			}
		}
		//检查map过程是否已经结束
		flag := true
		for _,v:=range c.Mapfiles{
			if v == NotStarted || v == Processing{
				flag = false
			}
		}
		if flag == true{
			c.State = Reduce
		}		
		}else{
			return nil
		}
	}else if args.TaskType == Reduce{
		index := args.FileName[0][strings.LastIndex(args.FileName[0],"-")+1:]			
		if c.Reducefiles[index] == Processing{
			c.Reducefiles[index] = Finished
			//检查reduce过程是否已经结束
			flag := true
			for _,v:=range c.Reducefiles{
				if v == NotStarted || v == Processing{
					flag = false
				}
			}
			if flag == true{
				c.State = Finish
			}
		}else{
			return nil
		}	
	}
	return nil
}

超时代码,启动一个worker后启动该线程,等待10s后查看对应的worker是否成功汇报,没有则将该任务设为未进行状态,其实查看crash.go代码后感觉这里可能会出现写文件冲突的情况,不过即使被复写了应该也不会影响最终结果,只是可能会有写冲突。总之超时代码不太严谨,但是额外再加一个锁给文件太复杂了,所以就先这样。

func (c *Coordinator) HandleTimeOut(TaskType int,TaskName string) error{
	time.Sleep(time.Second*10)
	c.Mux.Lock()
	defer c.Mux.Unlock()
	if TaskType == Map{
		if c.Mapfiles[TaskName] != Finished{
			c.Mapfiles[TaskName] = NotStarted
		}
	}else if TaskType == Reduce{
		if c.Reducefiles[TaskName] != Finished{
			c.Reducefiles[TaskName] = NotStarted
		}
	}
	return nil
}

Done方法一直查询coordinator状态,结束任务后返回true

func (c *Coordinator) Done() bool {
	c.Mux.Lock()
	defer c.Mux.Unlock()
	if c.State == Finish{
		return true
	}
	return false

	// Your code here.
}

3.4worker功能实现

功能代码,这部分mrsequential.go也用到了,不是系统自带的需要自己写,相当于ByKey实现了一个sort的排序接口。

// Map functions return a slice of KeyValue.
type KeyValue struct {
	Key   string
	Value string
}
type ByKey []KeyValue
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 }
// 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)
}

worker功能代码,不确定worker是否复用,所以让他一直循环,用call方法申请任务,根据任务信息调用相应的方法,执行结束后进行汇报。

func Worker(mapf func(string, string) []KeyValue,
	reducef func(string, []string) string) {
	for {
		args := GetTaskRequest{}
		args.Index = -1
		reply := GetTaskReply{}
		call("Coordinator.GetTask",&args,&reply)
		if reply.TaskType == Sleep{
			time.Sleep(time.Millisecond*10)
		}else if reply.TaskType == Finish{
			return
		}else if reply.TaskType == Map{
			// map function
			filenames := make([]string,reply.ReduceNum)
			filenames = HandleMap(mapf,reply.TaskName,reply.ReduceNum,reply.Index)
			// report to coordinator
			report := FinishTaskRequest{}
			reportReply := FinishTaskReply{}
			report.FileName = filenames
			report.TaskType = reply.TaskType
			report.TaskName = reply.TaskName
			call("Coordinator.ResponseTask",&report,&reportReply)

		}else if reply.TaskType == Reduce{
			// reduce function
			oname := HandleReduce(reducef,reply.InputName)

			// report to coordinator
			report := FinishTaskRequest{}
			reportReply := FinishTaskReply{}
			report.FileName = append(report.FileName,oname)
			report.TaskType = reply.TaskType
			call("Coordinator.ResponseTask",&report,&reportReply)
		}else{
			log.Fatal("error : unknow TaskType")
		}
	}
}

map部分代码,基本就是加工了一下官方代码,这里要注意中间件的存储名称与顺序,和后续reduce对得上。

func HandleMap(mapf func(string,string)[]KeyValue,filename string,reducenum int,mapnum int)[]string{

	//read input file and do map process
	intermediate := []KeyValue{}

	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()
	kav := mapf(filename,string(content))
	intermediate = append(intermediate,kav...)

	//create each json file if not exist
	filenames := make([]string,reducenum)
	basicname := "mr-" + strconv.Itoa(mapnum) + "-"
	files := make([]*os.File,reducenum)
	for i := 0 ; i < reducenum ; i++{
		filenames[i] = basicname + strconv.Itoa(i)
		files[i],_ = os.Create(filenames[i])
	}
	//write map result,**using lock here
	for _, kv := range intermediate{
		index := ihash(kv.Key)%reducenum
		enc := json.NewEncoder(files[index])
		enc.Encode(&kv)
	}
	return filenames
}

reduce部分代码,也是加工了一下官方代码,这里抄官方代码的时候要注意多了一步文件读写,可以通过rpc传递中间件名称或者自己按照某种约定写死,总之不要出错就可。

unc HandleReduce(reducef func(string, []string) string,filenames []string)string{
	//read input file and sort all of them
	files:= make([]*os.File,len(filenames))
	intermediate := []KeyValue{}
	for i:=0;i<len(files);i++{
		file,err := os.Open(filenames[i])
		if err != nil {
			log.Fatalf("cannot open %v", filenames[i])
		}
		files[i] = file
		kv := KeyValue{}
		dec := json.NewDecoder(files[i])
		for{
			err:=dec.Decode(&kv)
			if err!=nil{
				break
			}
			intermediate = append(intermediate,kv)
		}
	}
	sort.Sort(ByKey(intermediate))
	//create output file
	oname := "mr-out-"
	index:=filenames[0][strings.LastIndex(filenames[0],"-")+1:]
	oname=oname+index
	if oname == "mr-out-0"{
	}
	ofile, _ := os.Create(oname)
	//write map result,**using lock here
	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
	}
	return oname
}

4.总结

  1. 做完之后回顾感觉其实挺简单的但是如果不熟悉go和bash以及ubuntu的同学可能上手真的难受,这个只能自己花时间熟悉了。
  2. 我用的test-mr.sh这个测试文件进行代码调试,主要通过在关键节点打印字符确定自己错在哪了,主要是自己还不太会go的调试,有能力的同学可以自己想办法调试。没有办法的同学可以试试打印字符的方法进行debug,其实也挺好用的。
  3. 有可能会有如下的错误,是go版本的原因无视就好。在这里插入图片描述
  4. 官方说不要贴源码,我就不贴了,主要代码都在这而且自己写的也雀食不好看,有需要的同学可以看博主东东儿的代码,其实挺多地方看不到全部源码还是挺难理解的,只能说这个东西就是会的不难难的不会。
  5. 最后也是庆祝一下自己终于跑通,从装环境开始弄了有一个星期,一开始什么都一筹莫展最后还是跑通了。在这里插入图片描述
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值