MIT-6.824 MapReduce 学习记录 + Lab1

Part1

MapReduce论文学习

Map操作:

处理一个Key/Value对,生成许多个中间的key/value键值对结果

Reduce操作:

对map生成的所有键值对,相同的key的作合并

MapReduce是一种编程范式,能够使得大规模的并行化计算成为可能。同时,这也使得“再次执行”功能作为初级的容错机制。这篇论文主要贡献是通过简单的接口来实现自动的并行化和大规模的分布式计算。

编程模型介绍

整个模型的输入输出:
输入: 一个key/value pair的集合
输出: 一个key/value pair的集合

WordCount程序的伪代码:
MIT-6.824 MapReduce 学习记录 + Lab1_第1张图片
map函数输入输出:
map(k1,v1) - > list(k2,v2)
输入:一个key/value pair
输出:一个Key/value 集合

reduce函数输入输出
reduce(k2,list(v2)) -> list(v2)
输入:一个中间key值,和一个相关value值集合
输出:一个value值集合

MIT-6.824 MapReduce 学习记录 + Lab1_第2张图片通过把map调用的输入数据自动分割为M个数据片段的集合,(产生的中间结果存放在计算节点的本地磁盘)map调用被分布到多台机器上执行。输入的数据片段能够在不同的机器上并行处理。使用分区函数把map调用的产生的中间key值分成R个不同分区。reduce调用分布到多台机器上执行。

全部流程概述:

1.用户程序先把输入文件划分为相同大小的数据片,然后把程序复制到集群中的计算节点中去.

2.master节点负责给空闲的节点分派map或者reduce任务

3.被分配了map任务的worker进程读取输入数据片段,具体是从输入的数据片段解析出key/value pair,然后把key/value pair传递给用户自定义的map 函数,由map函数生成并输出中间key/value pair,缓存在内存中。

4.缓存中的key/value pair通过分区函数分成R个区域,周期性写入到本地磁盘上,缓存中的key/value pair在本地的磁盘位置回传给master,由master负责把这些存储位置再传送给reduce worker

5.当reduce worker程序接收到master程序发来的数据存储位置信息之后,使用RPC从map worker所在的主机磁盘上读取这些缓存数据。当reduce worker读取了所有中间数据之后,通过对key进行排序后使得具有相同key值的数据聚合在一起。

由于许多不同的key会映射到相同的reduce任务中,因此要进行排序。如果中间数据太大,无法在内存中排序,则要在外部排序。

6.worker程序遍历排序后的中间数据,对于每个惟一的中间key值,worker程序把这个key值和相关的中间value值集合传递给用户自定义的reduce函数。reduce函数的输出追加到所属分区的输出文件。

7.map与reduce操作完成后,master节点会通知用户程序,然后完成一次mapReduce操作

master数据结构

master持有一些数据结构,存储每一个Map与reduce任务的状态以及worker机器的标识。

任务状态分为:空闲,工作中或者完成。

只有当master告知了reduce worker数据路径之后,后者才会执行,否则一直处于空闲状态。

优化点:只要有一个key的数据操作完了,master就可以告知reduce节点操作数据的地址而启动任务,那样就没有必要等到所有的key操作完了之后再开始reduce任务。

master就像一个数据管道。中间文件存储区域的位置信息会通过这个管道从map传递到reduce.

对于每个完成了的map任务,master存储了map任务产生的R个中间文件存储区域的大小与位置。map任务完成后,master接收到位置和大小的更新信息,这些信息会被逐步递增地推送给正在工作的reduce任务。

总结
master节点的作用:
会记录所有map与reduce节点的状态,同时会记录已经分配了任务的机器节点以方便调配,map节点利用master节点来通知相应的reduce节点来取走相应的数据

容错机制

worker故障

master周期性ping每个worker,如果在一定约定时间范围内没有收到worker返回的信息,master将把这个worker标记为失效。所有由这个失效的worker完成的map任务将被重设为初始的空闲状态,然后这些任务可以安排给其他的worker。

同样的,worker失效的时候正在运行的map和reduce任务也将重新设置为空闲状态,等待重新调度。

当worker故障的时候,由于已经完成的map任务输出存储在这台机器上,map任务输出已经不可访问了。所以要重新执行。而已经完成的reduce任务输出存储在全局文件系统上,不需要重新执行。

当一个map任务首先被workerA执行,挂掉之后,调度到worker B执行。重新执行的动作会被通知给所有执行reduce任务的worker。任何还没有从worker A读取数据的reduce任务将从worker B读取数据。

master故障

简单的解决方案是,让master周期性地将上面描述的数据结构写入磁盘。即检查点。如果这个master任务失效了,可以从最后一个检查点开始启动另外一个master进程。然而由于只有一个master进程,失效之后恢复是很麻烦的。所以我们现在的实现是如果master失效,就中止mapReduce运算。客户可以检查到这个状态,并且根据需要重新执行mapReduce。

两个map节点领取到同一个map任务

master节点负责保证只有一台map节点的数据是可用的.

两个reduce节点领取到同一个任务

reduce节点输出直接写到GFS,GFS上的文件命名与key相关而且唯一,所以如果有两台reduce节点领取到同一个任务,处理的key值会相同.GFS的文件命名唯一性保证了只有一个reduce节点结果写到GFS上.

在失效方面的处理机制

当用户提供的map和reduce操作是输入确定性函数(即相同的输入产生相同的输出的时候,我们的分布式实现在任何情况下的输出都合所有程序没有出现任何错误,顺序的执行产生的输出是一样的)

我们依赖对map和reduce任务的输出是原子提交的来完成这个特性:每个工作的任务都把其输出写到私有的临时文件中。

每个reduce任务生成一个这样的文件,而每个map任务生成R个这样的文件。

当一个map任务完成的时候,worker会发送一个包含R个临时文件名的完成消息给master

如果master从一个已经完成的map任务再次接收到一个完成消息,master将会忽略这个消息。否则,master将把这R个文件的名字记录在数据结构里。

当reduce任务完成时候,reduce worker进程以原子方式把临时文件重命名为最终的输出文件。如果同一个reduce任务在多台机器上执行,针对同一个最终的输出文件,将有多个重命名操作执行。我们依赖底层文件系统提供的重命名操作的原子性来保证最终文件系统仅仅包含一个reduce任务产生的数据。

backup plan

有的机器运行久了,或者是其他任务占用同一台机器,使得一个mapreduce任务执行到后期时,运行速度会变慢.当mapreduce任务执行到后期的时候,master会备份还处于执行状态的所有任务,这时只需要原来的执行程序和备份的执行程序任意一个完成任务,就宣布本次mapreduce任务完成.

存储位置

网络带宽是一个相当匮乏的资源。我们通过尽量把输入数据存储在集群中机器的本地磁盘上来节省网络带宽。

mapReduce 的master在调度map任务的时候,会考虑输入文件的位置信息,尽量把一个map任务调度在包含相关输入数据拷贝的机器上执行:如果上述努力失败了,master将尝试在保存有输入数据拷贝的机器附近的机器上执行map任务。

当在一个足够大的cluster集群上运行大型mapReduce操作的时候,大部分输入数据都能从本地获取,因此网络带宽消耗比较少。

任务粒度

我们把map拆分为M个片段,把reduce拆分为R个任务执行。

理想情况下,M和R应该比worker的机器数量要多得多,在每台机器上执行大量不同任务能够提高集群动态负载均衡能力,并且能够加快故障恢复的速度。

失效机器上执行的大量map任务都可以分布到所有其他的worker机器上执行。

备用任务

影响一个mapReduce的总执行时间通常是“落伍者”
运算过程中,如果有一台机器花了很长时间才完成几个map或reduce任务,导致mapReduce操作总的执行时间超过预期。

出现落伍者原因:

例如机器硬盘出现问题,读取的时候要进行读取纠错操作。
如果机器调度了其他任务,其他资源会造成竞争。

通用的减少落伍者的机制:

当mapReduce操作接近完成的时候,master调度备用任务进程来执行剩下的,处于处理中状态的任务。无论是最初的执行进程,还是备用任务进程完成了任务,我们都把这个任务标记为已完成。

扩展功能介绍

分区函数

mapReduce使用者通常会指定reduce任务和reduce任务输出文件的数量®.
在中间key上使用分区函数来对数据进行分区,之后再输入到后续任务执行进程。

默认使用hash方法,而多数情况下需要提供专门的分区函数。

主要应用场景: 每个主机所有的条目都输出到同一个输出文件中.

顺序保证

中间key/value pair数据处理顺序是按照key增量顺序处理的。这样输出文件就能保证是有序的.

Combiner函数

map函数产生的中间key的重复数据可能会占很大的比重。例如词频统计程序,词频率倾向服从zipf分布,每个map任务将产生许多,其中很多key可能是相同的。

因此我们可以显指定一个combiner函数,先在本地把这些记录合并,再把合并结果通过网络发送出去。

combiner 函数在每一台执行map任务的机器上都会被执行一次,一般情况下,combiner和reduce函数是一致的。只不过reduce函数的输出保存在最终的输出文件,combiner函数写在中间文件里。

总结

  • MapReduce是一种用于封装并行处理,容错处理,数据本地化优化,负载均衡等技术的编程模型.
  • 编程模式的统一使得并行和分布式计算变得容易
  • 网络带宽是稀缺资源,大量系统优化针对减少网络传输量为目的:具体做法是大量的数据可以从本地磁盘读取,中间文件写入本地磁盘,并且只写一份中间文件
  • 多次执行相同的任务可以减少性能缓慢的机器带来的负面影响.

lab1

Part1 实现map与reduce的基本逻辑

map
  • 读取文件
  • 把读取的文件内容用map函数处理
  • 把处理结果划分到r个文件中,方便r个reduce节点去读取(使用hash函数确保不同map节点的相同数据能够映射到相同的节点)
package mapreduce

import (
	"encoding/json"
	"hash/fnv"
	"io/ioutil"
	"log"
	"os"
)

func doMap(
	jobName string, // the name of the MapReduce job
	mapTask int, // which map task this is
	inFile string,
	nReduce int, // the number of reduce task that will be run ("R" in the paper)
	mapF func(filename string, contents string) []KeyValue,
) {
	//
	// doMap manages one map task: it should read one of the input files
	// (inFile), call the user-defined map function (mapF) for that file's
	// contents, and partition mapF's output into nReduce intermediate files.
	//
	// There is one intermediate file per reduce task. The file name
	// includes both the map task number and the reduce task number. Use
	// the filename generated by reduceName(jobName, mapTask, r)
	// as the intermediate file for reduce task r. Call ihash() (see
	// below) on each key, mod nReduce, to pick r for a key/value pair.
	//
	// mapF() is the map function provided by the application. The first
	// argument should be the input file name, though the map function
	// typically ignores it. The second argument should be the entire
	// input file contents. mapF() returns a slice containing the
	// key/value pairs for reduce; see common.go for the definition of
	// KeyValue.
	//
	// Look at Go's ioutil and os packages for functions to read
	// and write files.
	//
	// Coming up with a scheme for how to format the key/value pairs on
	// disk can be tricky, especially when taking into account that both
	// keys and values could contain newlines, quotes, and any other
	// character you can think of.
	//
	// One format often used for serializing data to a byte stream that the
	// other end can correctly reconstruct is JSON. You are not required to
	// use JSON, but as the output of the reduce tasks *must* be JSON,
	// familiarizing yourself with it here may prove useful. You can write
	// out a data structure as a JSON string to a file using the commented
	// code below. The corresponding decoding functions can be found in
	// common_reduce.go.
	//
	//   enc := json.NewEncoder(file)
	//   for _, kv := ... {
	//     err := enc.Encode(&kv)
	//
	// Remember to close the file after you have written all the values!
	//
	// Your code here (Part I).
	//
	//第一件事 读取文件
	fileContent, err := ioutil.ReadFile(inFile)
	if err != nil {
		log.Fatal("can't read file")
	}
	KeyValuePairs := mapF(inFile, string(fileContent))

	//保存到本地,首先要知道文件的名称
	// 用一个map[String] key:string value:一个Encoder
	mapFiles := make(map[string]*json.Encoder)

	for i:=0; i
reduce

1.从所有的map节点读取属于自己的中间文件
2.对reduce节点读取到的所有key进行排序
3.对每一个key的输出内容,调用定义好的reduce函数
4.输出文件

func doReduce(
	jobName string, // the name of the whole MapReduce job
	reduceTask int, // which reduce task this is
	outFile string, // write the output here
	nMap int, // the number of map tasks that were run ("M" in the paper)
	reduceF func(key string, values []string) string,
) {
	//
	// doReduce manages one reduce task: it should read the intermediate
	// files for the task, sort the intermediate key/value pairs by key,
	// call the user-defined reduce function (reduceF) for each key, and
	// write reduceF's output to disk.
	//
	// You'll need to read one intermediate file from each map task;
	// reduceName(jobName, m, reduceTask) yields the file
	// name from map task m.
	//
	// Your doMap() encoded the key/value pairs in the intermediate
	// files, so you will need to decode them. If you used JSON, you can
	// read and decode by creating a decoder and repeatedly calling
	// .Decode(&kv) on it until it returns an error.
	//
	// You may find the first example in the golang sort package
	// documentation useful.
	//
	// reduceF() is the application's reduce function. You should
	// call it once per distinct key, with a slice of all the values
	// for that key. reduceF() returns the reduced value for that key.
	//
	// You should write the reduce output as JSON encoded KeyValue
	// objects to the file named outFile. We require you to use JSON
	// because that is what the merger than combines the output
	// from all the reduce tasks expects. There is nothing special about
	// JSON -- it is just the marshalling format we chose to use. Your
	// output code will look something like this:
	//
	// enc := json.NewEncoder(file)
	// for key := ... {
	// 	enc.Encode(KeyValue{key, reduceF(...)})
	// }
	// file.Close()
	//
	// Your code here (Part I).
	//
	//1. reduce是第r个reduce节点读取map节点中的第r个中间节点
	//   并且对数据进行定义好的reduce操作
	// key: key值 value: 一个字符串数组

	keyValueArray := make(map[string][]string)

	//用来记录所有的Key
	keyArray := make([]string,0)

	for i:=0;i
坑记录

在master_rpc.go中,48行改成

debug(“RegistrationServer: accept error, %v”, err)

就不报错了

part2 WordCount
package main

import (
	"fmt"
	"../mapreduce"
	"os"
	"strconv"
	"strings"
	"unicode"
)

//
// 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.
//
func mapF(filename string, contents string) []mapreduce.KeyValue {
	// Your code here (Part II).
	kvPairs := make([]mapreduce.KeyValue, 0)
	//过滤
	f:=func(r rune) bool {
		return !unicode.IsLetter(r) && !unicode.IsNumber(r)
	}
	words:=strings.FieldsFunc(contents, f)
	for _,word:= range words{
		kvPairs = append(kvPairs, mapreduce.KeyValue{word, strconv.Itoa(1)})
	}
	return kvPairs
}

//
// 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.
//
func reduceF(key string, values []string) string {
	// Your code here (Part II).

	res:=0
	for _,val :=range values{
		valInt,_ := strconv.Atoi(val)
		res = res + valInt
	}
	return strconv.Itoa(res)

}

// Can be run in 3 ways:
// 1) Sequential (e.g., go run wc.go master sequential x1.txt .. xN.txt)
// 2) Master (e.g., go run wc.go master localhost:7777 x1.txt .. xN.txt)
// 3) Worker (e.g., go run wc.go worker localhost:7777 localhost:7778 &)
func main() {
	if len(os.Args) < 4 {
		fmt.Printf("%s: see usage comments in file\n", os.Args[0])
	} else if os.Args[1] == "master" {
		var mr *mapreduce.Master
		if os.Args[2] == "sequential" {
			mr = mapreduce.Sequential("wcseq", os.Args[3:], 3, mapF, reduceF)
		} else {
			mr = mapreduce.Distributed("wcseq", os.Args[3:], 3, os.Args[2])
		}
		mr.Wait()
	} else {
		mapreduce.RunWorker(os.Args[2], os.Args[3], mapF, reduceF, 100, nil)
	}
}

Part3&&Part4

这部分利用go func和管道这两大特性来进行对mapreduce任务的并行化分发与容错处理

  • master阶段会调用两次schedule函数,分别在map与reduce阶段调用.
  • 只要go涉及到并行处理,肯定要用go routine
  • 怎么知道哪些节点是可用?
    通过RegisterChan 管道,专门通知可用的节点
  • 怎么知道之前工作的worker已经完成工作?
    本轮任务完成之后,把worker节点放入registerChan,实现节点的重复利用
  • 节点失效怎么办?
    开一个taskChan,如果失败了,把失败的任务重新放回管道,等待下一次调用
实现思路:

1.先把所有任务放入taskChan
2.通过registerChan获得可用节点
3.调用RPC给可用节点分发任务
4.任务处理失败,放回taskChan
5.sync.WaitGroup等待所有goroutine完成

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).
   //
   // 要等待所有的任务完成后,才能结束这个函数,所以,添加 wg
   var wg sync.WaitGroup
   taskchan:=make(chan int, 0)
   go func(){
   	for i:=0; i< ntasks;i++{
   		taskchan <- i
   	}
   }()
   //要等所有任务都完成之后才能结束这个函数
   wg.Add(ntasks)
   go func(){
   	for{
   		service := <-registerChan
   		i := <-taskchan
   		args := DoTaskArgs{
   			JobName:jobName,
   			Phase:phase,
   			TaskNumber:i,
   			NumOtherPhase:n_other,
   		}
   		if phase == mapPhase{
   			args.File = mapFiles[i]
   		}
   		go func(){
   			if call(service, "Worker.DoTask",args,nil){
   				wg.Done()
   				//任务结束后,这项服务传回给"可注册的任务",等待下一次任务的分配
   				registerChan <- service
   			}else{
   				//把没有完成的任务传回给taskchan,如果任务失败了,就传回给taskChannel
   				taskchan <- args.TaskNumber
   			}
   		}()
   	}
   }()
   //阻塞
   wg.Wait()
   fmt.Printf("Schedule: %v done\n", phase)
}

你可能感兴趣的:(分布式系统)