前言
作为学习分布式系统的一门课程,MIT6.824一直都是国内外质量上乘的课程之一,知乎上很多经验丰富的软件开发人员也都是力推这门课程,而它的好处就是在于教授的授课是结合学生课前阅读论文、上课讲述思路以及课后的lab共同组成,会很大程度上的让学生独立地从0到1去实现一个编程模型,而不是仅仅使用各个厂家的框架。
那么我跟随的课程是2020春季学期的这门课程,课程地址为:pdos.csail.mit.edu/6.824/sched…
- 注:其实Go很好上手,我实习的时候学了4天,然后下一周就进了项目,不会的童鞋可以参考Go语言之旅这个简短的教程来熟悉Go,地址是tour.go-zh.org/list
MapReduce原理
如果你还没有读过MapReduce
的论文,那么最好还是看一下,这样能够很快速的理解它的思想,我在这里就先总结一下。
首先,MapReduce
是一个能够处理和生成大量数据集的分布式的编程模型,它的结论就是通过给程序开发人员提供map
和reduce
两个函数来分别实现产生(k,v)
中间结果和将相同key
的数据进行汇总归并的操作,这样map
和reduce
两个函数其实就可以在不同的机器上进行运行,从而提高我们计算的效率。
在论文中,最重要的部分也就是下面这张图:
这张图里面有几个比较重要的部分:
-
Master进程
它是用来分配任务给worker进程的角色,他来决定worker执行的是
map
函数还是reduce
函数 -
worker进程
如果worker进程执行的是
map
函数,那么它就要读取已经分片了的输入文件,处理文件内的每一行,然后将文件转换成(k,v)
这样形式的中间文件;而如果它被指定为要执行reduce
函数,那么worker就需要去读取中间文件(这个过程是通过RPC
实现的),然后对中间文件的key
先做排序,然后根据排序的结果做归并的处理。
除此之外,就是还要留意一下中间文件的个数,这个个数是由用户程序指定的,然后怎么选择用哪个reduce
程序执行是通过公式hash(key) mod R
进行选择。
实验
课程给出了相应的例子,具体的代码路径是src\main\mrsequential.go
,这个例子给出的是非分布式版本的,而我们需要实现的是分布式版本的,具体来说我们需要完成src\mr
下面的master.go
, worker.go
和rpc.go
三个程序,
首先,我从worker
来开始着手写,每个worker
都需要通过rpc
和master
进行通信来申请一个任务(具体是map还是reduce任务是由master来决定的)
//
// main/mrworker.go calls this function.
//
func Worker(mapf func(string, string) []KeyValue,
reducef func(string, []string) string) {
// Your worker implementation here.
// uncomment to send the Example RPC to the master.
// CallExample()
}
这是作业中的Worker函数,我们也是需要在这个函数里实现我们的worker
函数,那么思路就是:
- 向
master
请求任务 - 针对返回来的任务类型,做不同的处理
- 返回任务类型是
map
:调用Map
函数 - 返回任务类型是
reduce
:调用Reduce
函数 - 还有两种特殊情况,就是任务等待和任务终止,分别延时一段时间和直接返回
- 默认情况抛异常
- 返回任务类型是
那么代码写出来就是这样的
//
// main/mrworker.go calls this function.
//
func Worker(mapf func(string, string) []KeyValue,
reducef func(string, []string) string) {
// Your worker implementation here.
// uncomment to send the Example RPC to the master.
// CallExample()
for {
//请求任务(rpc调用)
taskInfo := CallTask()
switch taskInfo.State {
case 0:
Map(mapf, )
break;
case 1:
Reduce(reducef, )
break;
case 2:
time.sleep(time.Duration(time.Second * 10))
break;
case 3:
fmt.Println(" all of tasks have completed, nothing to do...")
return
default:
panic("Invaild State of Worker, Please try again....")
}
}
}
注意到这里面我们用到了CallTask()
这个函数用来向master请求任务,在这个函数中我们需要调用提供给我们的call()
函数,最后返回了一个taskInfo
对象,根据对象中的State
决定怎么样处理,那么我们在worker.go
中先要实现CallTask()
方法:
func CallTask() *TaskInfo {
args := ExampleArgs{}
reply := TaskInfo{}
//通过rpc调用Master的RequireTask方法
call("Master.RequireTask", &args, &reply)
return &reply
}
func call(rpcname string, args interface{}, reply interface{}) bool {
// c, err := rpc.DialHTTP("tcp", "127.0.0.1"+":1234")
//masterSock()方法是在rpc.go中,返回的是UNIX域下socket的名字
sockname := masterSock()
c, err := rpc.DialHTTP("unix", sockname)
if err != nil {
log.Fatal("dialing:", err)
}
defer c.Close()
err = c.Call(rpcname, args, reply)
if err == nil {
return true
}
fmt.Println(err)
return false
}
除了完成这两个函数,我们还需要定义一下taskInfo
的内容,即TaskInfo
类,写在rpc.go
中:
type TaskInfo struct {
/*** state value
* 0 --> map
1 --> reduce
2 --> wait
3 --> nothing to do
*/
State int
//要读取的文件名
FileName string
//经过map后输出到哪个file中-->针对map
FileIdx int
//要写到哪个文件-->针对reduce
OutFileIdx int
//分成几个reduce
ReduceNum int
FileNum int
}
完成了上面步骤之后,现在就需要回到Worker
方法中,然后我们需要进入到switch
中,看到我们还有两个函数没有实现,即Map
和Reduce
:对于前者而言,输入的参数除了一个mapf
函数之外,还需要把任务信息传递进来,因为任务信息包含了你要读入文件的名称和index
。
func Map(mapf func(string, string) []KeyValue, taskInfo *TaskInfo) {
//根据文件名读文件,并将调用mapf之后输出的kv保存在中间数组中
interFile := []KeyValue
file, err := os.Open(taskInfo.FileName)
if err != nil {
panic(err)
}
content, err := ioutil.ReadAll(file)
if err != nil {
panic(err)
}
file.Close()
kvs := mapf(taskInfo.FileName, string(content))
interFile = append(interFile, kvs...)
//准备输出文件
outPrefix = "mr-tmp/mr-"
outPrefix += strconv.Itoa(taskInfo.FileIdx)
outFiles := make([]*os.File, taskInfo.ReduceNum)
outFilesEncode := make([]*json.Encoder, taskInfo.ReduceNum)
for idx := 0; idx < taskInfo.ReduceNum; idx++ {
//在mr-tmp文件夹下生成空的临时文件并设置编码
outFiles[idx], _ = ioutil.TempFile("mr-tmp", "mr-tmp-*")
outFilesEncode[idx] = json.NewEncoder(outFiles[idx])
}
//依据不同的key路由存储到到不同的文件中去
for _, kv := range interFile {
outputIdx := ihash(kv.Key) % taskInfo.ReduceNum
file = outFiles[outputIdx]
}
//保存文件
for idx, file := range outFiles {
outName := outPrefix + strconv.Itoa(idx)
oldPath := filepath.Join(file.Name())
os.Rename(oldPath, outname)
file.Close()
}
//通知master我map做完了
TaskDone(taskInfo)
}
func Reduce(reducef func(string, []string) string, taskInfo *TaskInfo) {
outName := "mr-out-" + strconv.Itoa(taskInfo.OutFileIdx)
interPrefix := "mr-tmp/mr-"
interSuffix := "-" + strconv.Itoa(taskInfo.OutFileIdx)
interFile := []KeyValue{}
for idx := 0; idx < taskInfo.FileNum; idx++ {
inname := interPrefix + strconv.Itoa(idx) + interSuffix
file, err := os.Open(inname)
if err != nil {
fmt.Printf("Open intermediate file %v failed: %v\n", inname, err)
panic("Open file error")
}
dec := json.NewDecoder(file)
for {
var kv KeyValue
if err := dec.Decode(&kv); err != nil {
break
}
interFile = append(interFile, kv)
}
file.Close()
}
sort.Sort(ByKey(interFile))
ofile, err := ioutil.TempFile("mr-tmp", "mr-*")
if err != nil {
fmt.Printf("Create output file %v failed: %v\n", outname, err)
panic("Create file error")
}
i := 0
for i < len(intermediate) {
j := i + 1
for j < len(interFile) && interFile[j].Key == interFile[i].Key {
j++
}
values := []string{}
for k := i; k < j; k++ {
values = append(values, interFile[k].Value)
}
output := reducef(interFile[i].Key, values)
fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)
i = j
}
os.Rename(filepath.Join(ofile.Name()), outName)
ofile.Close()
// acknowledge master
TaskDone(taskInfo)
}
这样,我们就完成了worker.go
的内容,现在我们需要考虑master.go
的结构。
首先我们都知道每个任务都包含的信息都已经写在了rpc.go/TaskInfo
中,但是在master中,我们得管理每一个任务,因此我们就需要有一个类来保存任务的状态:
type TaskStat struct{
fileName string
startTime time.Time
fileIndex int
outFileIndex int
reduceNum int
fileNum int
}
然后还要定义两个类继承这个任务信息类
type MapTask struct {
TaskStat
}
type ReduceTask struct {
TaskStat
}
有了对应的实现类以后我们还需要定义对外的接口,以提供获取任务信息的功能
type TaskInfoInterface interface {
GenerateTask() TaskInfo
OutOfTime() bool
GetFileIndex() int
GetOutFileIndex() int
SetTime()
}
所有的任务类都要实现上面的方法
func (this *MapTask) GenerateTaskInfo() TaskInfo {
return TaskInfo {
State: 0
FileName: this.fileName
FileIdx: this.fileIndex
OutFileIdx: this.outFileIndex
ReduceNum: this.reduceNum
FileNum: this.fileNum
}
}
func (this *ReduceTask) GenerateTaskInfo() TaskInfo {
return TaskInfo {
State: 1
FileName: this.fileName
FileIdx: this.fileIndex
OutFileIdx: this.outFileIndex
ReduceNum: this.reduceNum
FileNum: this.fileNum
}
}
func (this *TaskStat) OutOfTime() bool {
return time.Now().Sub(this.startTime) > time.Duration(time, Second * 60)
}
func (this *TaskStat) GetFileIndex() int {
return this.fileIndex
}
func (this *TaskStat) GetOutFileIndex int {
return this.outFileIndex
}
func (this *TaskStat) SetTime() {
this.startTime = time.Now()
}
除此之外我们定义一下master
类和两个由worker
请求的方法:
type Master struct {
fileNames []string
mapRunningTaskChannel chan TaskStat
mapWaitingTaskChannel chan TaskStat
reduceRunningTaskChannel chan TaskStat
reduceWaitingTaskChannel chan TaskStat
isDone bool
reduceNum int
}
func (this *Master) RequireTask(args *ExampleArgs, reply *TaskInfo) error {
if this.isDone {
reply.State = 3
return nil
}
mapTask := <- mapWaitingTaskChannel
if mapTask != nil {
mapTask.setTime()
mapRunningTaskChannel <- mapTask
*reply = mapTask.GenerateTaskInfo()
return nil
}
reduceTask := <- reduceWaitingTaskChannel
if reduceTask != nil {
reduceTask.setTime()
reduceRunningTaskChannel <- reduceTask
*reply = reduceTask.GenerateTaskInfo()
return nil
}
if len(this.mapRunningTaskChannel) > 0 || len(this.reduceRunningTaskChannel) > 0 {
reply.State = 2
return nil
}
replr.State = 3
this.isDone = true
return nil
}
func (this *Master) TaskDone(args *TaskInfo, reple *ExampleReply) error {
switch args.State {
case 0:
mapTask := <- mapRunningTaskChannel
if len(this.mapRunningTaskChannel) == 0 && len(this.mapWaitingTaskChannel) == 0 {
this.distributeReduce()
}
break
case 1:
reduceTask := <- reduceRunningTaskChannel
break
default:
pannic("task error")
}
return nil
}
func (this *Master) distributeReduce() {
reduceTask := ReduceTaskStat{
TaskStat{
fileIndex: 0,
outFileIndex: 0,
ReduceNum: this.ReduceNum,
fileNum: len(this.filenames),
},
}
for reduceIndex := 0; reduceIndex < this.nReduce; reduceIndex++ {
task := reduceTask
task.partIndex = reduceIndex
reduceWaitingTaskChannel <- task
}
}
那么到此为止呢,实验的大部分内容我也都写完了,主要思路也是借鉴了zhuanlan.zhihu.com/p/260752052…
参考连接: