map(String key, String value): // key: document name // value: document contents for each word w in value: EmitIntermediate(w, “1”);
reduce(String key, Iterator values): // key: a word // values: a list of counts int result = 0; for each v in values: result += ParseInt(v); Emit(AsString(result));
在 Google 内部所使用的计算环境中,机器间的网络带宽是比较稀缺的资源,需要尽量减少在机器间过多地进行不必要的数据传输;
Google MapReduce 采用 Google File System 来保存输入和结果数据,因此 Master 在分配 Map 任务时会从 Google File System 中读取各个 Block 的位置信息,并尽量将对应的 Map 任务分配到持有该 Block 的 Replica 的机器上;
如果无法将任务分配至该机器,Master 也会利用 Google File System 提供的机架拓扑信息将任务分配到较近的机器上;
// The map function is called once for each file of input. The first // argument is the name of the input file, and the second is the // file's complete contents. You should ignore the input file name, // and look only at the contents argument. The return value is a slice // of key/value pairs. funcMap(filename string, contents string) []mr.KeyValue { // function to detect word separators. ff := func(r rune)bool { return !unicode.IsLetter(r) }
// split contents into an array of words. words := strings.FieldsFunc(contents, ff)
kva := []mr.KeyValue{} for _, w := range words { kv := mr.KeyValue{w, "1"} kva = append(kva, kv) } return kva }
// The reduce function is called once for each key generated by the // map tasks, with a list of all the values created for that key by // any map task. funcReduce(key string, values []string)string { // return the number of occurrences of this word. return strconv.Itoa(len(values)) }
就是定义了 Map、Reduce 两个函数,而不必关心后面的任务是如何调度的(分布式 or 顺序执行);
随后执行:go run mrsequential.go wc.so pg*.txt 指定了 MapReduce 动态连接库,以及提前准备好的测试文件(以 pg 开头);
// read each input file, // pass it to Map, // accumulate the intermediate Map output. 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))
oname := "mr-out-0" ofile, _ := os.Create(oname)
// call Reduce on each distinct key in intermediate[], // and print the result to mr-out-0. 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 }
ofile.Close() }
// load the application Map and Reduce functions // from a plugin file, e.g. ../mrapps/wc.so funcloadPlugin(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) xreducef, err := p.Lookup("Reduce") if err != nil { log.Fatalf("cannot find Reduce in %v", filename) } reducef := xreducef.(func(string, []string)string)
// TaskInfo The MapReduce task type TaskInfo struct { Id string Type TaskTypeOpt Index int File string WorkerId string Deadline time.Time }
// AckAndQueryNewTaskRequest RPC request for Workers to query a task after finished previously task type AckAndQueryNewTaskRequest struct { // The finished previous task index (if it has finished task) PreviousTaskIndex int TaskType TaskTypeOpt WorkerId string }
// AckAndQueryNewTaskResponse RPC response for Workers to query a task type AckAndQueryNewTaskResponse struct { // The task id(filename) for Map or Reduce to yield results(if there has) Task *TaskInfo MapWorkerCnt int ReduceWorkerCnt int }
// MakeMaster create a Master. // main/mrmaster.go calls this function. // nReduce is the number of reduce tasks to use. funcMakeMaster(files []string, nReduce int) *Master {
// Step 2: Store the data for idx, file := range files { task := &TaskInfo{ Id: generateTaskId(TaskTypeMap, idx), Type: TaskTypeMap, Index: idx, File: file, } m.tasks[task.Id] = task // Store the tasks m.availableTasks <- task // Send the task to the channel }
// Step 3: Start master server m.server() infof("master server started: %v", m)
// generateTaskId Generate TaskId for the given task // The id follows the format: "taskType-taskIndex" funcgenerateTaskId(taskType TaskTypeOpt, index int)string { return fmt.Sprintf("%s-%d", taskType, index) }
随后,启动 RPC Server:
src/mr/master.go
1 2 3 4 5 6 7 8 9 10 11 12 13
// start a thread that listens for RPCs from worker.go func(m *Master) server() { _ = rpc.Register(m) rpc.HandleHTTP() //l, e := net.Listen("tcp", ":1234") sockname := masterSock() _ = os.Remove(sockname) l, e := net.Listen("unix", sockname) if e != nil { log.Fatal("listen error:", e) } go http.Serve(l, nil) }
// Step 1: Mark previous task finished if necessary if req.TaskType != "" { err := m.handlePreviousTask(req) if err != nil { errorf("handlePreviousTask err: %v", err) return err } }
// Step 2: Get the next task task, ok := <-m.availableTasks if !ok { // Channel closed: no available tasks m.lock.RLock() defer m.lock.RUnlock() retTask := &TaskInfo{} if m.status == TaskTypeFinished { retTask.Type = TaskTypeFinished } resp.Task = retTask returnnil }
// Step 3: Assign the task to the worker m.lock.Lock() defer m.lock.Unlock() infof("Assign task %v to worker %s", task, req.WorkerId) task.WorkerId = req.WorkerId task.Deadline = time.Now().Add(10 * time.Second) m.tasks[generateTaskId(task.Type, task.Index)] = task
m.lock.Lock() defer m.lock.Unlock() taskInfo, exists := m.tasks[previousTaskId] if exists { if taskInfo.WorkerId == req.WorkerId { // This task belongs to the worker infof("Mark task [%v] finished on worker %s", taskInfo, req.WorkerId)
// Step 1: Handle the previous finished task handler, handlerExists := taskFinishHandlerMap[taskInfo.Type] if !handlerExists || handler == nil { return fmt.Errorf("handler not found for task: %v", taskInfo) } err := handler(req.WorkerId, req.PreviousTaskIndex, m.reduceCnt) if err != nil { errorf("Failed to handle previous task: %v", err) return err } delete(m.tasks, previousTaskId)
// Step 2: Transit job phase if necessary iflen(m.tasks) <= 0 { m.transit() } returnnil } else { // The task is no longer belongs to this worker infof("Task %v is no longer belongs to this worker", taskInfo) returnnil } } else { // Previous task not found in task map(worker retry ack maybe)! warnf("[Warn] Previous task: %v not found in map", taskInfo) returnnil } }
// The map-type task handler for worker funchandleMapTask(resp *AckAndQueryNewTaskResponse, workerId string, mapFunc func(string, string) []KeyValue) error {
// Step 1: Read task file taskFile, err := os.Open(resp.Task.File) if err != nil { errorf("Failed to open map input file %s: %v", resp.Task.File, err) return err } content, err := ioutil.ReadAll(taskFile) if err != nil { errorf("Failed to read map input file %s: %v", resp.Task.File, err) return err }
// Step 2: Use MapFunc to yield intermediate results // Key: FileName, Value: FileContent // Then Hash the key by ihash func, and push result into different buckets mapResults := mapFunc(resp.Task.File, string(content)) hashedKva := make(map[int][]KeyValue) for _, kv := range mapResults { hashed := ihash(kv.Key) % resp.ReduceWorkerCnt hashedKva[hashed] = append(hashedKva[hashed], kv) }
// Step 3: Writes all intermediate results into intermediate files for idx := 0; idx < resp.ReduceWorkerCnt; idx++ { tmpFileName := tmpMapOutFile(workerId, resp.Task.Index, idx) intermediateFile, _ := os.Create(tmpFileName) for _, kv := range hashedKva[idx] { // The intermediate file format is: ${key}\t${value}\n _, err = fmt.Fprintf(intermediateFile, "%v\t%v\n", kv.Key, kv.Value) if err != nil { errorf("Write intermediate file: %s failed, err: %v", intermediateFile, err) intermediateFile.Close() return err } } intermediateFile.Close() }
// use ihash(key) % NReduce to choose the reducer // task number for each KeyValue emitted by Map. funcihash(key string)int { h := fnv.New32a() _, _ = h.Write([]byte(key)) returnint(h.Sum32() & 0x7fffffff) }
// The reduce-type task handler for worker funchandleReduceTask(resp *AckAndQueryNewTaskResponse, workerId string, reduceFunc func(string, []string)string) error {
// Step 1: Read the corresponding file var lines []string for mi := 0; mi < resp.MapWorkerCnt; mi++ { inputFile := finalMapOutFile(mi, resp.Task.Index) file, err := os.Open(inputFile) if err != nil { errorf("Failed to open map output file %s: %v", inputFile, err) return err } content, err := ioutil.ReadAll(file) if err != nil { errorf("Failed to read map output file %s: %v", inputFile, err) return err } lines = append(lines, strings.Split(string(content), "\n")...) }
// Step 2: Format the lines var mapResults []KeyValue for _, line := range lines { if strings.TrimSpace(line) == "" { continue } parts := strings.Split(line, "\t") mapResults = append(mapResults, KeyValue{ Key: parts[0], Value: parts[1], }) }
// Step 3: Sort the results sort.Sort(ByKey(mapResults))
// Step 4: Write the results(Just as the mrsequential.go do!) tmpFileName := tmpReduceOutFile(workerId, resp.Task.Index) tmpFile, _ := os.Create(tmpFileName) defer tmpFile.Close()
// Step 5: Call Reduce on each distinct key in mapResults[], and write the intermediate result. i := 0 for i < len(mapResults) { // Step 5.1: Find the same key in mapResults j := i + 1 for j < len(mapResults) && mapResults[j].Key == mapResults[i].Key { j++ } var values []string for k := i; k < j; k++ { values = append(values, mapResults[k].Value) }