1 实现思路
lab1基于RPC调用
由一个coordinator与一个或者多个Worker构成,下图中的Master由coordinator替代
lab1中的worker是一个个独立的进程,这些进程轮询调用coordinator从而获取任务。
由于并未区分Mapper和Reducer所以我们的策略是,先将所有的Map任务全部进行完,进行最终的归约(Reduce)
2 实现难点
- coordinator需要实现超时控制
- 保证线程安全
- 对性能的优化
3 难点攻坚
对于难点1我的实现思路是当coordinator分配任务的时候,开一个go routine负责监听,在go routine当中使用select case监听一个chan
worker任务结束后会请求coordinator的一个方法告知coordinator任务已经完成,在方法当中向chan中传一个消息(Msg) 告知go routine任务完成而go routine可以监听在固定时间内是否接受到信息如果超时(本lab为10s),可触发time.After()处理相关逻辑,比如将任务重新放回分配队列当中
难点1有个地方需要考虑,就是如果go routine认为已经超时,将任务重新放到可分配队列当中
而在这之后,worker才告知coordinator任务完成,这时候如何处理?
我的策略是,所有的task(map和reduce)都维护一个TaskId,map和reduce都维护一个HashSet(哈希表),用来标记这个TaskId是否超时,超时则不处理
难点2 加锁
难点3 见代码,使用队列,以及归并的思路统计数据
代码实现
这里给出rpc.go worker.go coordinator.go的代码
rpc.go
package mr
//
// RPC definitions.
//
// remember to capitalize all names.
//
import (
"os"
"strconv"
)
//
// example to show how to declare the arguments
// and reply for an RPC.
//
type ExampleArgs struct {
X int
}
type ExampleReply struct {
Y int
}
// Add your RPC definitions here.
type TaskArgs struct {
}
type TaskReply struct {
TaskType int // 0 是map任务 1是reduce任务
MapFilename string
ReduceTaskNum int
TaskId int
NReduce int
HasFinished bool
Rejested bool
}
type ReduceMsgArgs struct {
ReduceId int
TaskId int
}
type ReduceMsgReply struct {
Flag bool
}
type MapMsgArgs struct {
Filename string
TaskId int
}
type MapMsgReply struct {
Flag bool
}
// 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/5840-mr-"
s += strconv.Itoa(os.Getuid())
return s
}
worker.go
package mr
import (
"encoding/json"
"errors"
"fmt"
"hash/fnv"
"io/ioutil"
"log"
"net/rpc"
"os"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
// for sorting by key.
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 }
//
// 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.
// 通过该方法将一个 key val 映射到相应的reduce服务
func ihash(key string) int {
h := fnv.New32a()
h.Write([]byte(key))
return int(h.Sum32() & 0x7fffffff)
}
// 将mapper和reducer抽象成两个结构体
// type Handler interface {
// Handle()
// }
// type MapHandler struct {
// }
// func (m *MapHandler) Handle() {
// }
// type ReduceHandler struct {
// }
// func (r *ReduceHandler) Handle() {
// }
func ReduceHandle(reduceTaskNum int, taskId int, reducef func(string, []string) string) {
// Reduce任务
// 读取所有的以reduceTaskNum 结尾的中间文件
files, err := ioutil.ReadDir(".")
if err != nil {
log.Fatalf("cannot open cur file")
}
re := regexp.MustCompile(`^mr.*\d+$`)
// 用切片记录所有的kv
// kva := make([]KeyValue, 0) 不能全放到一个集合里
// 应该设计数据结构如下
// 每个文件对应一个集合
kva := make(map[string][]KeyValue)
ids := make(map[string]int) // 指针集合,用来遍历所有的kv集合
for _, f := range files {
fileName := f.Name()
if !re.MatchString(fileName) {
continue
}
// 匹配的
parts := strings.Split(fileName, "-")
reduceId, _ := strconv.Atoi(parts[len(parts)-1])
if reduceId != reduceTaskNum {
continue
}
file, err := os.Open(fileName)
if err != nil {
fmt.Println("can not open file %v here", fileName)
}
dec := json.NewDecoder(file)
for {
var kv KeyValue
if err := dec.Decode(&kv); err != nil {
break
}
// 如果这个filename对应的切片没有创建这里需要进行创建
if kva[fileName] == nil {
kva[fileName] = make([]KeyValue, 0)
}
kva[fileName] = append(kva[fileName], kv)
}
ids[fileName] = 0 // 遍历从0开始
}
// kva全部找出来了
// 第一步先归并 每个filename对应的集合,需要一个指针,来标记当前记录到哪里了
fileSize := len(kva)
successFile := 0
hasSuccess := make(map[string]bool)
kvs := make([]KeyValue, 0) // 最终大集合
// 需要归并,将key一样的放到一块
for successFile < fileSize {
// 需要变量标记是否是字典序最小的
miniStr := "zzzzzzzzzzzzzzzz"
miniFile := ""
for k, v := range kva {
if hasSuccess[k] {
continue
}
// 遍历所有的集合
index := ids[k]
// 判断这个index是否超出了范围
n := len(v)
if index == n {
// 说明这个文件的数据完成了
successFile++ // 只在第一次触发
hasSuccess[k] = true
continue
}
// v 是这个file的所有KeyValue Key是字符串 Value是1
tmpStr := v[index].Key
if tmpStr < miniStr {
// 说明更小
miniStr = tmpStr
miniFile = k
}
}
if successFile == fileSize {
break
}
// 找到了最小的键值对
kv := kva[miniFile][ids[miniFile]]
ids[miniFile]++
kvs = append(kvs, kv)
}
// 下一步进行Reduce key一样的value全部放到一个地方然后传给Reduce函数
i := 0
output := make([]KeyValue, 0)
// 维持顺序的遍历 不可以使用for range
for i < len(kvs) {
j := i + 1
for j < len(kvs) && kvs[j].Key == kvs[i].Key {
j++
}
// 上面是找到相同key的区域[i,j)
values := []string{} // 存储所有的value,应当都为1
for k := i; k < j; k++ {
values = append(values, kvs[k].Value)
}
// 返回值是string类型表示这个词出现的总次数
occurrenceCount := reducef(kvs[i].Key, values)
// kvs[i].Key output
output = append(output, KeyValue{kvs[i].Key, occurrenceCount})
// 向文件里写这个词出现的次数和key
i = j
}
// 向新文件里面写数据
fn := "mr-out-" + strconv.Itoa(reduceTaskNum) // 写入的文件名字
writeReduceOutputFromInner(fn, output, reduceTaskNum, taskId)
}
func MapHandle(mapFilename string, taskId int, nReduce int, mapf func(string, string) []KeyValue) {
// 存的时候按Json存
file, err := os.Open(mapFilename)
if err != nil {
log.Fatalf("cannot open %v", mapFilename)
}
content, err := ioutil.ReadAll(file)
if err != nil {
log.Fatalf("cannot open %v", mapFilename)
}
// map任务
innerFiles := mapf(mapFilename, string(content))
reduceKVMap := make(map[int][]KeyValue)
reduceIds := make([]int, 0)
// 最后应该是一个reduceId一个文件
for _, v := range innerFiles {
reduceId := ihash(v.Key) % nReduce
if len(reduceKVMap[reduceId]) == 0 {
// 说明没有
reduceIds = append(reduceIds, reduceId)
}
reduceKVMap[reduceId] = append(reduceKVMap[reduceId], v)
}
// 写,遍历所有的reduceId来写
for i := 0; i < len(reduceIds); i++ {
reduceId := reduceIds[i]
kvs := reduceKVMap[reduceId] // 所有某个reduceId的键值对,需要对key进行排序
// shuffle阶段排序
sort.Sort(ByKey(kvs))
// 每写一个文件,就会向协调器传消息,所以会卡住
// 写多个中间文件,但只确认一次
// 一起写
// err = WriteInnerToFile(fn, kvs, mapFilename, taskId)
if err != nil {
fmt.Println("Error creating temporary file:", err)
}
}
err = WriteInnerToFile(reduceIds, reduceKVMap, mapFilename, taskId)
if err != nil {
fmt.Println("Error creating temporary file:", err)
}
}
//
// main/mrworker.go calls this function.
//
func Worker(mapf func(string, string) []KeyValue,
reducef func(string, []string) string) {
// 这里应该是一个轮询逻辑
// Your worker implementation here.
for {
taskType, mapFilename, reduceTaskNum, taskId, nReduce, err := CallForTasks()
if err != nil {
// 说明有问题,有问题sleep一会再continue
time.Sleep(1 * time.Second)
continue
}
if taskType == 1 {
ReduceHandle(reduceTaskNum, taskId, reducef)
} else {
MapHandle(mapFilename, taskId, nReduce, mapf)
}
// 请求后sleep一段时间
time.Sleep(1 * time.Second)
}
// uncomment to send the Example RPC to the coordinator.
// CallExample()
}
// 确认的时候会出现问题,因为coordinator做验证的时候,保护了一种情况,就是当一个task已经超时了,超时以后请求confirm的时候会通过HashSet进行验证是否超时
// 使用的是reduceId进行验证,所以会存在超时一次就一直超时的问题
// 解决方案 继续使用维护的taskId来维护超时的问题
func writeReduceOutputFromInner(filename string, kvs []KeyValue, reduceId int, taskId int) error {
// 向这个文件里面写入相应的数据
tempFile, err := ioutil.TempFile("", "reduce-out-*") // 建立临时文件
tempName := tempFile.Name()
defer os.Remove(tempFile.Name()) // 在程序结束前删除临时文件
if err != nil {
fmt.Println("Error creating temporary file:", err)
return err
}
i := 0
for i < len(kvs) {
// 向临时文件里面写内容
fmt.Fprintf(tempFile, "%v %v\n", kvs[i].Key, kvs[i].Value)
i++
}
tempFile.Close()
// 然后是改名阶段,改之前需要先请求一手确认有木有毛病,需要
flag := CallForReduceConfirm(reduceId, taskId) // true说明超时或者报错
if flag {
fmt.Println("请求超时")
// 让他超时
err := errors.New("写入文件失败")
return err
}
// 改名
if err := os.Rename(tempName, filename); err != nil {
// 这里进行回滚
return err
}
return nil
}
// 将map的产物写进文件
func WriteInnerToFile(reduceIds []int, reduceKVMap map[int][]KeyValue, mapFilename string, taskId int) error {
tmpNameMap := make(map[string]string) // 需要一个临时文件名字和真实文件名之间的一个映射关系表
for i := 0; i < len(reduceIds); i++ {
reduceId := reduceIds[i]
kvs := reduceKVMap[reduceId] // 所有某个reduceId的键值对,需要对key进行排序
fn := "mr-" + strconv.Itoa(taskId) + "-" + strconv.Itoa(reduceId)
tempFile, err := ioutil.TempFile("", "map-inner-*") // 建立临时文件
tmpNameMap[tempFile.Name()] = fn
if err != nil {
fmt.Println("Error creating temporary file:", err)
return err
}
defer os.Remove(tempFile.Name()) // 在程序结束前删除临时文件
enc := json.NewEncoder(tempFile)
// 写的时候注意,使用JSON的数据格式
for i := 0; i < len(kvs); i++ {
// 这里有问题写文件不是按顺序写的
enc.Encode(&reduceKVMap[reduceId][i])
}
// 关闭临时文件
err = tempFile.Close()
if err != nil {
fmt.Println("Error closing temporary file:", err)
return err
}
}
// 上面写所有的临时文件
// fn := "mr-" + strconv.Itoa(taskId) + "-" + strconv.Itoa(reduceId)
// 然后我们请求协调器,看看有没有问题
flag := CallForMapConfirm(mapFilename, taskId) // 确认的时候有问题,应该一个文件请求一次,请求完没问题,全部改名字
if !flag {
// 可以改名
for k, v := range tmpNameMap {
if err := os.Rename(k, v); err != nil {
// 这里进行回滚
CallBack(tmpNameMap)
return err
}
}
}
return nil
}
func fileExists(filename string) bool {
_, err := os.Stat(filename)
return !os.IsNotExist(err)
}
func CallBack(tmpNameMap map[string]string) error {
// 把所有Value的路径的全部删了
for _, v := range tmpNameMap {
if fileExists(v) {
// 存在
err := os.Remove(v)
return err
}
}
return nil
}
func CallForTasks() (taskType int, mapFilename string, reduceTaskNum int, taskId int, nReduce int, err error) {
args := &TaskArgs{}
replys := &TaskReply{}
ok := call("Coordinator.AssignTask", args, replys)
if !ok {
err = errors.New("调用失败")
return
}
if replys.Rejested {
err = errors.New("请求被拒绝")
return
}
if replys.HasFinished {
err = errors.New("任务已经完成")
return
}
mapFilename = replys.MapFilename
reduceTaskNum = replys.ReduceTaskNum
taskId = replys.TaskId
taskType = replys.TaskType
nReduce = replys.NReduce
// taskType, mapFilename, _, taskId, nReduce
return taskType, mapFilename, reduceTaskNum, taskId, nReduce, nil
}
// 调用Reduce任务的确认回执
func CallForReduceConfirm(reduceId int, taskId int) bool {
args := &ReduceMsgArgs{}
args.ReduceId = reduceId
args.TaskId = taskId
replys := &ReduceMsgReply{}
ok := call("Coordinator.ReceiveReduceMsg", args, replys)
if !ok {
fmt.Printf("call failed!\n")
return true // true表示报错
}
return replys.Flag
}
// 调用Map任务的确认回执
func CallForMapConfirm(mapFilename string, taskId int) bool {
args := &MapMsgArgs{}
args.TaskId = taskId
args.Filename = mapFilename
replys := &MapMsgReply{}
ok := call("Coordinator.ReceiveMapMsg", args, replys)
if !ok {
fmt.Printf("call failed!\n")
return true
}
return replys.Flag
}
//
// send an RPC request to the coordinator, wait for the response.
// usually returns true.
// returns false if something goes wrong.
//
func call(rpcname string, args interface{}, reply interface{}) bool {
// c, err := rpc.DialHTTP("tcp", "127.0.0.1"+":1234")
sockname := coordinatorSock()
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
}
coordinator.go
package mr
import (
"log"
"net"
"net/http"
"net/rpc"
"os"
"strconv"
"sync"
"time"
)
// 需要实现 1 判断是否包含 2 remove
type HashSet struct {
tm map[int]bool
lock sync.Mutex
}
func (o *HashSet) put(taskId int) {
o.lock.Lock()
defer o.lock.Unlock()
o.tm[taskId] = true
}
func (o *HashSet) contains(taskId int) bool {
o.lock.Lock()
defer o.lock.Unlock()
_, exists := o.tm[taskId]
return exists
}
func (o *HashSet) remove(taskId int) {
o.lock.Lock()
defer o.lock.Unlock()
delete(o.tm, taskId)
}
type Counter struct {
num int
lock sync.Mutex
}
func (t *Counter) getCounter() int {
t.lock.Lock()
defer t.lock.Unlock()
t.num++
return t.num
}
type SafeQueue struct {
elements []string
lock sync.Mutex
}
type SafeStringChanMap struct {
m map[string]chan bool
lock sync.Mutex
}
func (ssc *SafeStringChanMap) set(s string, b bool) {
ssc.lock.Lock() // 上锁
defer ssc.lock.Unlock() // 关锁
// 往里面塞东西捏
ssc.m[s] <- b
}
func (ssc *SafeStringChanMap) get(s string) chan bool {
ssc.lock.Lock() // 上锁
if ssc.m[s] == nil {
// 需要创建chan
ssc.m[s] = make(chan bool)
}
a := ssc.m[s]
defer ssc.lock.Unlock() // 关锁
return a
}
type SafeIntChanMap struct {
m map[int]chan bool
lock sync.Mutex
}
func (ssc *SafeIntChanMap) set(s int, b bool) {
ssc.lock.Lock() // 上锁
defer ssc.lock.Unlock() // 关锁
// 往里面塞东西捏
ssc.m[s] <- b
}
func (ssc *SafeIntChanMap) get(s int) chan bool {
ssc.lock.Lock() // 上锁
if ssc.m[s] == nil {
// 需要创建chan
ssc.m[s] = make(chan bool)
}
a := ssc.m[s]
defer ssc.lock.Unlock() // 关锁
return a
}
type Coordinator struct {
// Reduce的问题是nReduce 需要n个机器处理Reduce任务
taskCounter Counter
isExit bool
mapSuccessCount int
reduceSuccessCount int
fileCount int
reduceCount int
// Your definitions here.
// 需要有参数记录当前有多少个map任务有多少reduce任务
mapTaskQueue SafeQueue
reduceTaskQueue SafeQueue
// 每个string对应一个chan 每个string不会重复访问
mapTaskMsg SafeStringChanMap
reduceTaskMsg SafeIntChanMap
// 超时集合,负责控制超时的任务
mapOutTimeSet HashSet
reduceOutTimeSet HashSet
}
// Enqueue 在队列末尾添加元素
func (q *SafeQueue) Enqueue(element string) {
q.lock.Lock()
defer q.lock.Unlock()
q.elements = append(q.elements, element)
}
// Dequeue 从队列头部删除元素并返回
func (q *SafeQueue) Dequeue() (string, bool) {
q.lock.Lock()
defer q.lock.Unlock()
if len(q.elements) == 0 {
return "", false
}
element := q.elements[0]
q.elements = q.elements[1:]
return element, true
}
// IsEmpty 检查队列是否为空
func (q *SafeQueue) IsEmpty() bool {
q.lock.Lock()
defer q.lock.Unlock()
return len(q.elements) == 0
}
// Your code here -- RPC handlers for the worker to call.
//
// an example RPC handler.
//
// the RPC argument and reply types are defined in rpc.go.
//
func (c *Coordinator) Example(args *ExampleArgs, reply *ExampleReply) error {
reply.Y = args.X + 1
return nil
}
// 返回是否超时
func (c *Coordinator) ReceiveReduceMsg(args *ReduceMsgArgs, reply *ReduceMsgReply) error {
// 问题是比如这个文件12s以后回调这个方法,那就应该抛弃这个消息
// 首先需要确认这个taskId是否存在超时的集合当中
reduceId := args.ReduceId
taskId := args.TaskId
// 判断是否超时
flag := c.reduceOutTimeSet.contains(taskId)
if flag {
// 说明超时了
// fmt.Println("超时了")
reply.Flag = true
return nil
}
// 说明执行成功
c.reduceTaskMsg.set(reduceId, true)
// fmt.Println(reduceId)
reply.Flag = false // 成功
return nil
}
// 返回是否超时
func (c *Coordinator) ReceiveMapMsg(args *MapMsgArgs, reply *MapMsgReply) error {
// 问题是比如这个文件12s以后回调这个方法,那就应该抛弃这个消息
// 首先需要确认这个taskId是否存在超时的集合当中
taskId := args.TaskId
filename := args.Filename
// 判断是否超时
flag := c.mapOutTimeSet.contains(taskId)
if flag {
// 说明超时了
reply.Flag = true
return nil
}
// 说明执行成功
c.mapTaskMsg.set(filename, true)
reply.Flag = false // 成功
return nil
}
// 分配任务以后使用管道,等待信息,再另外一个方法当中
func (c *Coordinator) AssignTask(args *TaskArgs, reply *TaskReply) error {
// 分配任务的时候生成一个taskId
taskId := c.taskCounter.getCounter()
if c.mapSuccessCount == c.fileCount {
// 说明map完成,分配reduce任务
// 判断一下如果reduce任务也全部Success,就可以结束这个任务
if c.reduceSuccessCount == c.reduceCount {
reply.HasFinished = true
c.isExit = true // 结束任务 这时候得告诉他得返回true
// 然后要通知一下回去,告诉worker任务结束
return nil // 返回
}
// 分配Reduce任务
if !c.reduceTaskQueue.IsEmpty() {
// 分配任务
reduceId, _ := c.reduceTaskQueue.Dequeue()
rid, _ := strconv.Atoi(reduceId)
reply.ReduceTaskNum = rid
reply.TaskType = 1
reply.TaskId = taskId
// 要做的是,读取中间文件生成out文件,只读取reduceId的文件,需要超时控制
go func() {
// 这个goroutine 不处理完,别的线程不可能可以处理这个filename
// fmt.Println(rid)
select {
case value := <-c.reduceTaskMsg.get(rid):
// fmt.Println(value)
// 两种可能 成功或者失败
if value {
// 成功
c.reduceSuccessCount++
} else {
c.reduceTaskQueue.Enqueue(reduceId) // 放回去
}
case <-time.After(10 * time.Second):
// fmt.Printf("超时了 " + strconv.Itoa(rid))
//重新放回去
c.reduceTaskQueue.Enqueue(reduceId)
// 将超时的ID存到集合里
c.reduceOutTimeSet.put(taskId)
}
}()
} else {
// 没任务的时候要告诉回去
reply.Rejested = true
}
} else {
// 分配map任务
// 优先分配map任务
if !c.mapTaskQueue.IsEmpty() {
// 分配mapFiles 任务 如果有空
fileName, _ := c.mapTaskQueue.Dequeue()
reply.MapFilename = fileName
reply.TaskType = 0
reply.TaskId = taskId
reply.NReduce = c.reduceCount
go func() {
// 这个goroutine 不处理完,别的线程不可能可以处理这个filename
select {
case value := <-c.mapTaskMsg.get(fileName):
// 两种可能 成功或者失败
if value {
// 成功
c.mapSuccessCount++
} else {
c.mapTaskQueue.Enqueue(fileName) // 放回去
}
case <-time.After(10 * time.Second):
// fmt.Println("超时了")
//重新放回去
c.mapTaskQueue.Enqueue(fileName)
// 将超时的ID存到集合里
c.mapOutTimeSet.put(taskId)
}
}()
} else {
// 没任务的时候要告诉回去
reply.Rejested = true
}
}
return nil
}
//
// start a thread that listens for RPCs from worker.go
//
func (c *Coordinator) server() {
rpc.Register(c)
rpc.HandleHTTP()
//l, e := net.Listen("tcp", ":1234")
sockname := coordinatorSock()
// fmt.Println(sockname)
os.Remove(sockname)
l, e := net.Listen("unix", sockname)
if e != nil {
log.Fatal("listen error:", e)
}
go http.Serve(l, nil)
}
//
// main/mrcoordinator.go calls Done() periodically to find out
// if the entire job has finished.
//
func (c *Coordinator) Done() bool {
// Your code here.
return c.isExit
}
//
// 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 {
c := &Coordinator{
taskCounter: Counter{},
isExit: false,
mapOutTimeSet: HashSet{tm: make(map[int]bool)},
reduceOutTimeSet: HashSet{tm: make(map[int]bool)},
mapSuccessCount: 0,
reduceSuccessCount: 0,
fileCount: 0,
reduceCount: 0,
mapTaskQueue: SafeQueue{elements: make([]string, 0)},
reduceTaskQueue: SafeQueue{elements: make([]string, 0)},
mapTaskMsg: SafeStringChanMap{m: make(map[string]chan bool)},
reduceTaskMsg: SafeIntChanMap{m: make(map[int]chan bool)},
}
// Your code here.
// 通过files赋值给全局的变量,由变量作为分配任务的依据
for _, v := range files {
c.mapTaskQueue.Enqueue(v)
c.fileCount++
}
c.reduceCount = nReduce
for i := 0; i < nReduce; i++ {
c.reduceTaskQueue.Enqueue(strconv.Itoa(i))
}
c.server()
// 决定分配worker,然后安排n个Reduce处理情况
return c
}