1.概述
1.1 schedule()
在这部分实验要将之前串行版本的MapReduce tasks改成并发模式,只需要实现 mapreduce/schedule.go中的 schedule()函数,其他文件不做更改。 主机在MapReduce作业期间调用schedule()两次,一次用于Map阶段,一次用于Reduce阶段。schedule()的工作是将tasks分发给可用的 workers。通常tasks会比 workers多,因此schedule()必须为每个 worker提供一系列task,但每个worker一次只能执行一个task。 schedule()应该等到所有tasks都完成后再返回。
func schedule(jobName string, mapFiles []string, nReduce int, phase jobPhase, registerChan chan string) {
var ntasks int
var n_other int // number of inputs (for reduce) or outputs (for map)
switch phase {
case mapPhase:
ntasks = len(mapFiles)
n_other = nReduce
case reducePhase:
ntasks = nReduce
n_other = len(mapFiles)
}
fmt.Printf("Schedule: %v %v tasks (%d I/Os)\n", ntasks, phase, n_other)
// All ntasks tasks have to be scheduled on workers. Once all tasks
// have completed successfully, schedule() should return.
//
// Your code here (Part III, Part IV).
//
fmt.Printf("Schedule: %v done\n", phase)
}
schedule()通过读取registerChan参数来获取 workers 信息。该通道为每个worker生成一个字符串,包含worker的RPC address。有些workers可能在调度schedule()之前已经存在,而某些workers可能在schedule()运行时启动,但所有workers都会出现在 registerChan上。schedule()应该使用所有worker,包括启动后出现的worker。
1.2 DoTaskArgs
schedule()通过向worker 发送Worker.DoTask RPC来告诉worker执行task,每次只能向一个给定的worker发送一个RPC。该RPC的参数被定义在MapReduce / common_rpc.go中的DoTaskArgs。其中File参数只在Map tasks中使用,表示要读取的输入文件名称。 schedule()可以在mapFiles中找到这些文件名。
type DoTaskArgs struct {
JobName string
File string // 只在Map tasks中使用,表示要读取的输入文件名称
Phase jobPhase // 标志当前是map还是reduce阶段
TaskNumber int // 此task在当前阶段的索引
// NumOtherPhase是其他阶段的task总数
// mappers需要这个来计算output bins, reducers 需要这个来知道要收集多少输入文件
NumOtherPhase int
}
1.3 call()
使用mapreduce / common_rpc.go中的call()函数 将RPC发送给worker。第一个参数是worker的 address,从registerChan读取。第二个参数应该是“Worker.DoTask”,表示通过rpc调用worker的DoTask方法,第三个参数应该是DoTaskArgs结构,最后一个参数应该是nil。
func call(srv string, rpcname string,
args interface{}, reply interface{}) bool {
c, errx := rpc.Dial("unix", srv)
if errx != nil {
return false
}
defer c.Close()
err := c.Call(rpcname, args, reply)
if err == nil {
return true
}
fmt.Println(err)
return false
}
1.4 Worker、 Worker.DoTask
再来看下Worker的结构和方法,定义在worker.go中
type Worker struct {
sync.Mutex
name string
Map func(string, string) []KeyValue
Reduce func(string, []string) string
nRPC int // quit after this many RPCs; protected by mutex
nTasks int // total tasks executed; protected by mutex
concurrent int // number of parallel DoTasks in this worker; mutex
l net.Listener
parallelism *Parallelism
}
他有下列几个方法:
- register:worker的register方法中使用RPC远程调用了Master.Register,将该worker注册到master中。master维护了一个workers []string,记录worker的address,master还负责把worker address发送到通道中(上文提到的registerChan),供schedule()读取。
- RunWorker:和master建立连接,注册它的address到master中(调用上方的register函数),等待schedul()安排tasks。
- DoTask:在schedul()函数中会调用此方法给worker安排task。根据传入的phase参数,他会去执行doMap或doReduce方法。在worker中还维护了一个Parallelism结构,跟踪worker是否并发执行。
type Parallelism struct {
mu sync.Mutex
now int32
max int32
}
其中max字段记录该worker运行的最大task数量,通过锁机制保证了并发。由于在各个函数间传递的是&Parallelism(地址),所以大家修改的是同一个Parallelism。
- Shutdown:当所有tasks都完成后,master会调用worker的Shutdown,worker应该返回所处理过的tasks数量。
2.调用流程
这个实验将执行两个测试,TestParallelBasic(验证运行是否正确)和TestParallelCheck(验证是否并发执行) 。在TestParallelBasic中首先会生成20个824-mrinput-xx.txt的输入文件,接着调用Distributed()函数,启动schedule(),运行map tasks和reduce tasks,然后新开两个协程去启动两个workers。最后检查生成的输出文件是否正确,每个worker至少执行了一个task。因此map tasks数量为20,reduce tasks数量为10,workers数量为2。
func TestParallelBasic(t *testing.T) {
mr := setup()//该函数中会调用Distributed()
for i := 0; i < 2; i++ {
go RunWorker(mr.address, port("worker"+strconv.Itoa(i)),
MapFunc, ReduceFunc, -1, nil)
}
mr.Wait()
check(t, mr.files)
checkWorker(t, mr.stats)
cleanup(mr)
}
//先运行 map tasks,然后运行reduce tasks
func Distributed(jobName string, files []string, nreduce int, master string) (mr *Master) {
mr = newMaster(master)
mr.startRPCServer()
//会先运行 map tasks(mapPhase),然后运行reduce tasks(reducePhase),具体逻辑在Master.run函数中
go mr.run(jobName, files, nreduce,
func(phase jobPhase) {
//创建一个无缓冲的通道
ch := make(chan string)
//将所有现有的和新注册的workers信息发送到ch通道中,schedule通过通道ch获取workers信息
go mr.forwardRegistrations(ch)
//本次要实现内容,将tasks分发给可用的 workers
schedule(mr.jobName, mr.files, mr.nReduce, phase, ch)
},
func() {
mr.stats = mr.killWorkers()
mr.stopRPCServer()
})
return
}
TestParallelCheck 则是验证所编写的调度程序是否并行地执行task,同样开启了两个worker,检验worker中的parallelism.max(运行的worker数量最大值)是否小于2,若小于则失败。
func TestParallelCheck(t *testing.T) {
mr := setup()
parallelism := &Parallelism{}
for i := 0; i < 2; i++ {
go RunWorker(mr.address, port("worker"+strconv.Itoa(i)),
MapFunc, ReduceFunc, -1, parallelism)
}
mr.Wait()
check(t, mr.files)
checkWorker(t, mr.stats)
parallelism.mu.Lock()
if parallelism.max < 2 {
t.Fatalf("workers did not execute in parallel")
}
parallelism.mu.Unlock()
cleanup(mr)
}
3.schedule()
schedule()的参数中mapFiles是输入文件名称列表,每个maptask处理一个。nReduce是reduce tasks的数量,registerChan通道传递所有worker的RPC address。局部变量ntasks表示当前阶段的tasks数量,若是map阶段则为输入文件数量,若是reduce阶段则为nReduce参数。
了解了大概的运行流程,schedule()中要做的就是开启多个线程,读取通道registerChan获取worker的address。构造DoTaskArgs参数。调用call()方法向worker 发送Worker.DoTask RPC,告诉worker执行task。在测试程序中开启了两个worker:worker0和worker1,而registerChan是一个无缓冲的通道,每次通道上只有一个worker address,因此可以将处理完task的worker的 address再放回registerChan,供下一个线程读取,以此实现两个worker并发运行。
一旦所有该阶段的tasks成功完成,schedule()就返回,这个功能可以使用sync.WaitGroup来实现。
我的实现如下:
func schedule(jobName string, mapFiles []string, nReduce int, phase jobPhase, registerChan chan string) {
var ntasks int
var n_other int // number of inputs (for reduce) or outputs (for map)
switch phase {
case mapPhase:
ntasks = len(mapFiles)
n_other = nReduce
case reducePhase:
ntasks = nReduce
n_other = len(mapFiles)
}
fmt.Printf("Schedule: %v %v tasks (%d I/Os)\n", ntasks, phase, n_other)
var wg sync.WaitGroup
wg.Add(ntasks)
for i:=0;i<ntasks;i++{
//开启线程并发调用
go func(taskNum int) {
//从chan获取可用的worker
for w := range registerChan {
//构造DoTaskArgs参数
var arg DoTaskArgs
switch phase {
case mapPhase:
arg = DoTaskArgs{JobName:jobName,File:mapFiles[taskNum],Phase:mapPhase,TaskNumber:taskNum,NumOtherPhase:n_other}
case reducePhase:
arg = DoTaskArgs{JobName:jobName,File:"",Phase:reducePhase,TaskNumber:taskNum,NumOtherPhase:n_other}
}
call(w,"Worker.DoTask",arg,nil)
wg.Done()
registerChan <- w//将worker address放回registerChan
break
}
}(i)
}
wg.Wait()
return
}
4.测试运行
运行下面命令来测试所编写的实验代码。这将依次执行两个测试,TestParallelBasic和TestParallelCheck 。
go test -run TestParallel
得到类似下面结果程序运行成功