分布式系统---MapReduce实现(Go语言)

一、说明

  • 本次实验是基于MIT-6.824的课程,详情请参见官网主页
  • 下载源代码

二、MapReduce原理

2.1 经典的分布式模型

MapReduce是经典的分布式模型。通过Map函数和Reduce函数实现。

分布式计算,就是利用多台机器,完成一个任务。关于分布式计算,几个经典的例子就是单词词频统计。假设现在有1000MB的文本文件需要进行词频统计,如果只有1台机器,处理此大文件可能需要10s。

如果现在有10台机器,每台机器负责处理100MB的数据,处理完之后(并行),再进行汇总,那效率将会极大提升。

2.2 MapReduce的过程

  • MapReduce模型中,有一个master,负责分配任务,有多个worker,负责map任务和reduce任务。当map处理好任务后,输出中间结果{key, value}。一般来说,每个reduce会负责固定key的任务。reduce拿到中间结果继续处理,最后再整合输出。
    在这里插入图片描述

三. Go语言实现

3.1 Worker进行和Coordinate进行RPC通信

  • RPC的参数
type ExampleArgs struct {
	X int
}

type ExampleReply struct {
	Y int
}

需要注意,遍历要大写,因为在Go语言中大写首字母代表public。

  • RPC服务器的接口
func (c *Coordinator) Example(args *ExampleArgs, reply *ExampleReply) error {
	reply.Y = args.X + 1
	return nil
}

客户端能够远程调用该函数,并得到结果

  • 客户端远程调用
func CallExample() {
	args := ExampleArgs{}
	args.X = 99
	reply := ExampleReply{}

	// send the RPC request, wait for the reply.
	call("Coordinator.Example", &args, &reply)

	// reply.Y should be 100.
	fmt.Printf("reply.Y %v\n", reply.Y)
}

3.2 一些变量的定义

  • 我们这里使用了状态机的方式,设置了TaskMap,TaskReduce,TaskWait,TaskEnd几种状态,分别表示Map任务,Reduce任务,目前还有任务正在执行需要等待,任务全部完成
const (
	TaskMap      = 1  // Map任务
	TaskReduce   = 2  // Reduece任务
	TaskWait     = 3  // 暂时无任务
	TaskEnd      = 4  // 所有任务已完成
	FixedTimeOut = 15 // 等待15s,Worker没回复,就说明出错了
)

var Debug bool = false

// Map任务
type MapTask struct {
	FileName string // 需要处理的文件
	MapID    int    // 当前map的编号
	NReduce  int    // 需要分成几块
}

type ReduceTask struct {
	FileName string // 该任务属于哪一个文件
	MapID    int    // 哪个map的输出结果
	ReduceID int    // 当前reduce的编号
}

// Debug模式下才需要打印
func Dprintf(format string, data ...interface{}) {
	if Debug {
		fmt.Printf(format, data...)
	}
}

3.2 Coordinator

  • Coorinator负责整个MapReduce过程的管理,因此需要记录任务相关的信息
type Coordinator struct {
	// Your definitions here.
	mutex       sync.Mutex
	mapTaskQ    []MapTask
	redTaskQ    []ReduceTask // [{0-0}, {0-1}, {0-2}, ..., {0-9}, {1-0},...{1-9}]
	mapTaskingQ []MapTask    // 正在执行map任务
	redTaskingQ []ReduceTask // 正在执行reduce任务
	nReduce     int
	isDone      bool
}

mutex:因为程序涉及到多线程,因此需要加锁
mapTaskQ:记录目前的Map任务,实际上就是文件名
redTaskQ:记录当前的Reduce任务,因为每个map会输出对应nReduce的中间文件,实验给的提示是通过ihash(key)函数,将不同的key映射到不同的reduce上,因此每个reduce应该会涉及到处理多个中间文件
mapTaskingQ:正在执行的map任务,当目前还有正在执行的map任务时,但是又有闲置的map机器在请求任务,此时coordinator应该让其等待,因为可能执行任务的map机器出现故障,此时就需要找到新的map继续执行该任务。此外,如果当目前还有正在执行的map任务,也不应该进入到reduce阶段
redTaskingQ:正在执行的reduce任务,理由和上面类似,都是为了防止发生故障

  • Coordinator初始化
func MakeCoordinator(files []string, nReduce int) *Coordinator {
	c := Coordinator{}

	// Your code here.
	c.mapTaskingQ = make([]MapTask, 0)
	c.nReduce = nReduce

	// 每个文件对应一个任务
	for i, file := range files {
		task := MapTask{
			FileName: file,
			MapID:    i,
			NReduce:  nReduce,
		}

		c.mapTaskQ = append(c.mapTaskQ, task)
	}

	c.isDone = false

	Dprintf("Master working...\n")

	go c.tasking2task()

	c.server()
	return &c
}

实际上就是将文件转换为map任务

  • AskTask Worker请求任务的RPC调用
func (c *Coordinator) AskTask(args *AskTaskArgs, reply *AskTaskReply) error {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	// 还有map任务
	if len(c.mapTaskQ) > 0 {
		reply.TaskType = TaskMap
		reply.MapTask = c.assignMapTask()

		return nil
	}

	// 有正在执行的map任务
	if len(c.mapTaskingQ) > 0 {
		reply.TaskType = TaskWait
		Dprintf("some MapTasks are not completed, please wait...\n")

		return nil
	}

	// 有reduce任务
	if len(c.redTaskQ) > 0 {
		reply.TaskType = TaskReduce
		redTasks := c.assignRedTask()
		reply.RedTasks = append(reply.RedTasks, redTasks...)

		return nil
	}

	// 有正在执行的reduce任务
	if len(c.redTaskingQ) > 0 {
		reply.TaskType = TaskWait
		Dprintf("some ReduceTasks are not completed, please wait...\n")

		return nil
	}

	// reduce任务也处理完了
	reply.TaskType = TaskEnd
	c.isDone = true

	Dprintf("ALL tasks done,msg: closing -> worker...\n")

	return nil
}
  • 分配Map任务
func (c *Coordinator) assignMapTask() MapTask {
	task := c.mapTaskQ[0]
	c.mapTaskQ = append(c.mapTaskQ[:0], c.mapTaskQ[1:]...)
	c.mapTaskingQ = append(c.mapTaskingQ, task)

	Dprintf("assign MapTask, fileName..%v, mapId..%v, mReduce..%v\n",
		task.FileName, task.MapID, task.NReduce)

	return task
}
  • 分配reduce任务
// 分配reduce任务
func (c *Coordinator) assignRedTask() []ReduceTask {
	redTasks := make([]ReduceTask, 0)
	reduceId := c.redTaskQ[0].ReduceID

	// 取第一个reduce task
	for i := 0; i < len(c.redTaskQ); {
		if c.redTaskQ[i].ReduceID != reduceId {
			i++
			continue
		}

		task := c.redTaskQ[i]
		c.redTaskingQ = append(c.redTaskingQ, task)
		redTasks = append(redTasks, task)
		c.redTaskQ = append(c.redTaskQ[:i], c.redTaskQ[i+1:]...)
		Dprintf("assign ReduceTask, fileName..%v, mapId..%v, reduceId..%v\n",
			task.FileName, task.MapID, task.ReduceID)
	}

	return redTasks
}
  • worker完成,回复coordinator
func (c *Coordinator) TaskDone(args *TaskDoneReply, reply *ExampleReply) error {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	switch args.TaskType {
	case TaskMap:
		c.mapTaskingQDeleter(args.MapTask, args.RedTasks)
		break
	case TaskReduce:
		c.redTaskingQDeleter(args.RedTasks)
		break
	default:
		break
	}

	return nil
}
  • map任务完成,从mapTaskingQ中移除,同时还应该将map处理好的中间结果,存放到redTaskQ
func (c *Coordinator) mapTaskingQDeleter(mapTask MapTask, redTasks []ReduceTask) {
	Dprintf("MapTask done, fileName..%v, mapId..%v, nReduce..%v\n",
		mapTask.FileName, mapTask.MapID, mapTask.NReduce)

	// 找到 mapTask.MapID的任务,然后剔除
	for i := 0; i < len(c.mapTaskingQ); i++ {
		if c.mapTaskingQ[i].MapID == mapTask.MapID {
			c.mapTaskingQ = append(c.mapTaskingQ[:i], c.mapTaskingQ[i+1:]...)
			break
		}
	}

	// 将该map的结果放到reduceTask
	// reTasks = [{i-0},...,{i-9}]
	for _, v := range redTasks {
		c.redTaskQ = append(c.redTaskQ, v)
		Dprintf("add ReduceTask, fileName..%v, mapId..%v, reduceId..%v\n",
			v.FileName, v.MapID, v.ReduceID)
	}
}
  • reduce任务完成,从redTaskingQ移除
func (c *Coordinator) redTaskingQDeleter(redTasks []ReduceTask) {
	for i := 0; i < len(redTasks); i++ {
		task := redTasks[i]
		Dprintf("ReduceTask done, fileName..%v, mapId..%v, reduceId..%v\n",
			task.FileName, task.MapID, task.ReduceID)
	
		for j := 0; j < len(c.redTaskingQ); {
			if c.redTaskingQ[j].ReduceID == task.ReduceID {
				c.redTaskingQ = append(c.redTaskingQ[:j], c.redTaskingQ[j+1:]...)
				continue
			}
			j++
		}
	}
}
  • coordinator还需要定期检查是否有map或者reduce宕机,如果有宕机,应该重新由空闲的机器处理。这里我们采用设置一个等待时间FixedTimeOut=15,如果超过这个时间正在执行的任务还没有完成,应该将他们移动到任务队列中
func (c *Coordinator) tasking2task() {
	for {
		time.Sleep(FixedTimeOut * time.Second)
		c.mutex.Lock()
		// defer c.mutex.Unlock()

		// 还有正在执行的map任务,将它添加到待完成任务队列 mapTaskQ
		if len(c.mapTaskingQ) != 0 {
			for i, _ := range c.mapTaskingQ {
				c.mapTaskQ = append(c.mapTaskQ, c.mapTaskingQ[i])
			}

			c.mapTaskingQ = []MapTask{}
		}

		if len(c.redTaskingQ) != 0 {
			Dprintf("redTaskingQ..%v\n", c.redTaskingQ)
			for i, _ := range c.redTaskingQ {
				c.redTaskQ = append(c.redTaskQ, c.redTaskingQ[i])
			}

			c.redTaskingQ = []ReduceTask{}
		}
		c.mutex.Unlock()
	}
}

3.3 Worker

  • 因为我们是单机,所以这里需要模拟多机,我们会让Worker一直请求任务,知道所有任务处理完成
func Worker(mapf func(string, string) []KeyValue,
	reducef func(string, []string) string) {

	for {
		reply := CallAskTask()

		switch reply.TaskType {
		case TaskMap: // Map任务
			doMapTask(mapf, reply.MapTask)
			break
		case TaskReduce: // Reduce任务
			doReduceTask(reducef, reply.RedTasks)
			break
		case TaskWait: // 此时没有任务
			time.Sleep(1 * time.Second)
			break
		case TaskEnd: // 所有任务全部完成
			Dprintf("任务全部完成,关机...\n")
			return
		default:
			fmt.Println("reply.TaskType: ", reply.TaskType)
			Dprintf("Unknown fault\n")
			break
		}
	}
}
  • 请求任务
func CallAskTask() AskTaskReply {
	args := AskTaskArgs{}
	reply := AskTaskReply{}

	call("Coordinator.AskTask", &args, &reply)

	return reply
}
  • 处理map任务
func doMapTask(mapf func(string, string) []KeyValue, mapTask MapTask) {
	Dprintf("doing MapTask, fileName..%v, mapId..%v, nReduce..%v\n",
		mapTask.FileName, mapTask.MapID, mapTask.NReduce)

	fileName := mapTask.FileName

	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 open %v", fileName)
	}

	file.Close()

	// 通过map函数,统计每个单词
	kvs := mapf(fileName, string(content))
	sort.Sort(ByKey(kvs))

	// 将所有的单词都放到对应的 reduces[idx]中 idx = [0,...,NReduce)
	reduces := make([][]KeyValue, mapTask.NReduce)
	for _, kv := range kvs {
		idx := ihash(kv.Key) % mapTask.NReduce
		reduces[idx] = append(reduces[idx], kv)
	}

	// reduce任务
	redTasks := []ReduceTask{} // [{i-0}, ... ,{i-9}]

	for idx, reduce := range reduces {

		// reduce任务
		redTask := ReduceTask{
			FileName: mapTask.FileName,
			MapID:    mapTask.MapID,
			ReduceID: idx,
		}

		redTasks = append(redTasks, redTask)

		output := "mr-" + strconv.Itoa(redTask.MapID) + "-" + strconv.Itoa(redTask.ReduceID)
		file, err = os.Create(output)
		if err != nil {
			log.Fatalf("cannot create %v", output)
		}
		defer file.Close()

		enc := json.NewEncoder(file)

		for _, kv := range reduce {
			enc.Encode(&kv)
		}

	}

	Dprintf("MapTask to ReduceTask done, fileName..%v, mapId..%v\n", mapTask.FileName, mapTask.MapID)

	CallTaskDone(TaskMap, mapTask, redTasks)
}
  • 处理reduce任务
func doReduceTask(reducef func(string, []string) string, redTasks []ReduceTask) {

	intermediate := []KeyValue{}

	for _, v := range redTasks {
		Dprintf("doing ReduceTask, fileName..%v, mapId..%v, reduceId..%v\n",
			v.FileName, v.MapID, v.ReduceID)
		fileName := "mr-" + strconv.Itoa(v.MapID) + "-" + strconv.Itoa(v.ReduceID)
		file, err := os.Open(fileName)
		if err != nil {
			log.Fatalf("cannot open %v", fileName)
		}
		defer file.Close()

		// 反序列化JSON格式文件
		dec := json.NewDecoder(file)
		for {
			var kv KeyValue
			err := dec.Decode(&kv)
			if err != nil {
				//log.Print("read file done...")
				break
			}

			intermediate = append(intermediate, KeyValue{kv.Key, kv.Value})
		}
	}

	sort.Sort(ByKey(intermediate))

	oname := "mr-out-" + strconv.Itoa(redTasks[0].ReduceID)
	ofile, _ := os.Create(oname)
	defer ofile.Close()

	i := 0
	for i < len(intermediate) {
		// 找到每一组相同的key,统一放到一个slice
		j := i + 1
		for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {
			j++
		}

		// ["1","1",...,"1"]
		values := []string{}
		for k := i; k < j; k++ {
			values = append(values, intermediate[k].Value)
		}

		// 执行Reduce函数,计算key出现的次数
		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
	}

	Dprintf("ReduceTask done, fileName..%v, mapId..%v\n", redTasks[0].FileName, redTasks[0].MapID)

	CallTaskDone(TaskReduce, MapTask{}, redTasks)
}
  • 任务处理完,需要回应coordinator
func CallTaskDone(taskType int, mapTask MapTask, redTasks []ReduceTask) {
	args := TaskDoneReply{}
	args.TaskType = taskType
	args.MapTask = mapTask
	args.RedTasks = redTasks
	reply := ExampleReply{}
	call("Coordinator.TaskDone", &args, &reply)
}
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
掌握分布式mapreduce与raft算法与分布式数据库MapReduce是一种编程模型,用于大规模数据集(大于1TB)的并行运算。概念Map(映射)和Reduce(归约),是它们的主要思想,都是从函数式编程语言里借来的,还有从矢量编程语言里借来的特性。它极大地方便了编程人员在不会分布式并行编程的情况下,将自己的程序运行在分布式系统上。 当前的软件实现是指定一个Map(映射)函数,用来把一组键值对映射成一组新的键值对,指定并发的Reduce(归约)函数,用来保证所有映射的键值对中的每一个共享相同的键组。MapReduce是面向大数据并行处理的计算模型、框架和平台,它隐含了以下三层含义:1)MapReduce是一个基于集群的高性能并行计算平台(Cluster Infrastructure)。它允许用市场上普通的商用服务器构成一个包含数十、数百至数千个节点的分布和并行计算集群。2)MapReduce是一个并行计算与运行软件框架(Software Framework)。它提供了一个庞大但设计精良的并行计算软件框架,能自动完成计算任务的并行化处理,自动划分计算数据和计算任务,在集群节点上自动分配和执行任务以及收集计算结果,将数据分布存储、数据通信、容错处理等并行计算涉及到的很多系统底层的复杂细节交由系统负责处理,大大减少了软件开发人员的负担。3)MapReduce是一个并行程序设计模型与方法(Programming Model & Methodology)。它借助于函数式程序设计语言Lisp的设计思想,提供了一种简便的并行程序设计方法,用Map和Reduce两个函数编程实现基本的并行计算任务,提供了抽象的操作和并行编程接口,以简单方便地完成大规模数据的编程和计算处理Raft 是一种为了管理复制日志的一致性算法。它提供了和 Paxos 算法相同的功能和性能,但是它的算法结构和 Paxos 不同,使得 Raft 算法更加容易理解并且更容易构建实际的系统。为了提升可理解性,Raft 将一致性算法分解成了几个关键模块,例如leader人选举、日志复制和安全性。同时它通过实施一个更强的一致性来减少需要考虑的状态的数量。从一个用户研究的结果可以证明,对于学生而言,Raft 算法比 Paxos 算法更加容易学习。Raft 算法还包括一个新的机制来允许集群成员的动态改变,它利用重叠的大多数来保证安全性。 一致性算法允许一组机器像一个整体一样工作,即使其中一些机器出现故障也能够继续工作下去。正因为如此,一致性算法在构建可信赖的大规模软件系统中扮演着重要的角色。在过去的 10 年里,Paxos 算法统治着一致性算法这一领域:绝大多数的实现都是基于 Paxos 或者受其影响。同时 Paxos 也成为了教学领域里讲解一致性问题时的示例。 但是不幸的是,尽管有很多工作都在尝试降低它的复杂性,但是 Paxos 算法依然十分难以理解。并且,Paxos 自身的算法结构需要进行大幅的修改才能够应用到实际的系统中。这些都导致了工业界和学术界都对 Paxos 算法感到十分头疼。 和 Paxos 算法进行过努力之后,我们开始寻找一种新的一致性算法,可以为构建实际的系统和教学提供更好的基础。我们的做法是不寻常的,我们的首要目标是可理解性:我们是否可以在实际系统中定义一个一致性算法,并且能够比 Paxos 算法以一种更加容易的方式来学习。此外,我们希望该算法方便系统构建者的直觉的发展。不仅一个算法能够工作很重要,而且能够显而易见的知道为什么能工作也很重要。 Raft 一致性算法就是这些工作的结果。在设计 Raft 算法的时候,我们使用一些特别的技巧来提升它的可理解性,包括算法分解(Raft 主要被分成了leader人选举,日志复制和安全三个模块)和减少状态机的状态(相对于 Paxos,Raft 减少了非确定性和服务器互相处于非一致性的方式)。一份针对两所大学 43 个学生的研究表明 Raft 明显比 Paxos 算法更加容易理解。在这些学生同时学习了这两种算法之后,和 Paxos 比起来,其中 33 个学生能够回答有关于 Raft 的问题。 Raft 算法在许多方面和现有的一致性算法都很相似(主要是 Oki 和 Liskov 的 Viewstamped Replication),但是它也有一些独特的特性: 强leader:和其他一致性算法相比,Raft 使用一种更强的leader能力形式。比如,日志条目只从leader发送给其他的服务器。这种方式简化了对复制日志的管理并且使得 Raft 算法更加易于理解。leader选举:Raft 算法使用一个随机计时器来选举leader。这种方式只是在任何一致性算法都必须实现的心跳机制上增加了一点机制。在解决冲突的时候会更加简单快捷。成员关系调整:Raft 使用一种共同一致的方法来处理集群成员变换的问

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值