MapReduce是一种编程模型,用于大规模数据集(大于1TB)的并行运算。"Map(映射)“和"Reduce(归约)”,和它们的主要思想,都是从函数式编程语言里借来的,还有从矢量编程语言里借来的特性。它极大地方便了编程人员在不会分布式并行编程的情况下,将自己的程序运行在分布式系统上。 当前的软件实现是指定一个Map(映射)函数,用来把一组键值对映射成一组新的键值对,指定并发的Reduce(归约)函数,用来保证所有映射的键值对中的每一个共享相同的键组。
go语言基础知识(goroutine,slice,sync库,struct等)
MIT 6.824课程网站 6.824 Schedule: Spring 2020 (mit.edu)
Lab1 6.824 Lab 1: MapReduce (mit.edu)
阅读论文 MapReduce paper. http://research.google.com/archive/mapreduce-osdi04.pdf
deepin系统
Vmware虚拟机
Goland
我们可以看到源码中所展现的是MapReduce的串行化实现,我们可以以此为基础去完成并行化代码,由于该实验不能在分布式的环境下进行,所以实验是开多个线程来模拟分布式情况下的一系列操作,并通过Rpc的方式进行通信
我们所需要完成的只是完成mr包下中的,master.go,worker.go,rpc.go这三个文件
type MapTask struct {
TaskStruct
Filename string
}
type ReduceTask struct {
TaskStruct
IntermediateFilenames []string
}
type TaskStruct struct {
State TaskState
StartTime time.Time
Id int
}
type Task struct {
Operation TaskOperation
IsMap bool
NReduce int
Map MapTask
Reduce ReduceTask
}
定义map任务以及reduce任务的结构体,map首先会对某个文件进行映射操作,然后对每个key进行哈希映射从而分配给不同的reduce worker线程进行归约操作,所以每个reduce worker会对多个不同的文件进行规约操作,所以在这里我们要在ReduceTask中定义一个字符串切片数组。
// Task状态枚举变量
type TaskState int
const (
// 等待分配的一个任务
WAITING TaskState = iota
// 进行操作中的一个任务
WORKING
// 已经完成操作的一个任务
FINISHED
)
// 表示master当前需要分配哪些任务,是否已经结束
type JobState int
const (
MAPPING JobState = iota
REDUCING
DONE
)
type Master struct {
State JobState
NReduce int
MapTasks []*MapTask
ReduceTasks []*ReduceTask
MapIdSet []int
MaxTaskId int
Mutex sync.Mutex //信号量,用来进行线程的互斥操作
MapGroup sync.WaitGroup
ReduceGroup sync.WaitGroup
}
其中sync.WaitGroup类似于Java中的CountDownLatch,在这边我用来检测同个操作的所有任务是否已经完成,完成后将线程放行,进行下一个操作。
(init->map->reduce->done)
//
// create a Master.
// main/mrmaster.go calls this function.
// nReduce is the number of reduce tasks to use.
//
func MakeMaster(files []string, nReduce int) *Master {
m := Master{
NReduce: nReduce,
MaxTaskId: 0,
}
// map任务队列
for _, f := range files {
m.MapTasks = append(m.MapTasks, &MapTask{TaskStruct: TaskStruct{State: WAITING}, Filename: f})
m.MapGroup.Add(1)
}
// reduce任务队列
for i := 0; i < nReduce; i++ {
m.ReduceTasks = append(m.ReduceTasks, &ReduceTask{TaskStruct: TaskStruct{State: WAITING, Id: i}})
}
m.ReduceGroup.Add(nReduce)
m.State = MAPPING
m.server()
// 开启一个goroutine检测任务是否已经完成
go func() {
_ = m.checkFinal()
}()
// 开启一个goroutine检测任务是否超时了,如果超时了,将任务重置并重新分配
go func() {
_ = m.checkTime()
}()
return &m
}
其中checkFinal()和checkTime()方法如下
func (m *Master) checkFinal() error {
m.MapGroup.Wait()
m.State = REDUCING
m.ReduceGroup.Wait()
m.State = DONE
return nil
}
同上(其中sync.WaitGroup类似于Java中的CountDownLatch,在这边我用来检测同个操作的所有任务是否已经完成,完成后将线程放行,进行下一个操作。
(init->map->reduce->done))
const TIMEOUT = 10 * time.Second
func (m *Master) checkTime() error {
for {
if m.State == MAPPING {
for _, task := range m.MapTasks {
m.Mutex.Lock()
if task.State == WORKING && task.StartTime.Add(TIMEOUT).Before(time.Now()) {
task.State = WAITING
}
m.Mutex.Unlock()
}
} else if m.State == REDUCING {
for _, task := range m.ReduceTasks {
m.Mutex.Lock()
if task.State == WORKING && task.StartTime.Add(TIMEOUT).Before(time.Now()) {
task.State = WAITING
}
m.Mutex.Unlock()
}
}
if m.State == DONE {
return nil
}
}
}
不断for循环检测任务是否超时(因为在test中,它会模拟现实场景某些操作会莫名其妙的寄掉,将你的worker杀掉几个,所以你必须要对其判断你的worker是否寄了)
func (m *Master) RequestTask(_ *Placeholder, reply *Task) error {
reply.Operation = WAIT
if m.State == MAPPING {
for _, task := range m.MapTasks {
m.Mutex.Lock()
if task.State == WAITING {
task.StartTime = time.Now()
task.State = WORKING
m.MaxTaskId++
task.Id = m.MaxTaskId
m.Mutex.Unlock()
reply.Operation = RUN
reply.IsMap = true
reply.NReduce = m.NReduce
reply.Map = *task
return nil
}
m.Mutex.Unlock()
}
} else if m.State == REDUCING {
for _, task := range m.ReduceTasks {
m.Mutex.Lock()
if task.State == WAITING {
task.StartTime = time.Now()
task.State = WORKING
task.IntermediateFilenames = nil
for _, id := range m.MapIdSet {
task.IntermediateFilenames = append(task.IntermediateFilenames, fmt.Sprintf("mr-%d-%d", id, task.Id))
}
m.Mutex.Unlock()
reply.Operation = RUN
reply.IsMap = false
reply.NReduce = m.NReduce
reply.Reduce = *task
return nil
}
m.Mutex.Unlock()
}
}
return nil
}
分配任务的操作,将任务队列中还在等待的任务分配给worker。
func (m *Master) Finish(args *FinishArgs, _ *Placeholder) error {
if args.IsMap {
for _, task := range m.MapTasks {
if task.Id == args.Id {
task.State = FINISHED
log.Printf("finished task %d, total %d", task.Id, len(m.MapTasks))
m.MapIdSet = append(m.MapIdSet, task.Id)
m.MapGroup.Done()
break
}
}
} else {
for _, task := range m.ReduceTasks {
if task.Id == args.Id {
task.State = FINISHED
m.ReduceGroup.Done()
break
}
}
}
return nil
}
任务完成的操作,worker完成任务后会调用rpc通知master我这个任务已经完成了,这边会对任务标记完成,并且执行m.MapGroup.Done(),就是WaitGroup计数器减一
//
// main/mrworker.go calls this function.
//
func Worker(mapf MapF, reducef ReduceF) {
for {
time.Sleep(1 * time.Second)
task := Task{}
call("Master.RequestTask", &Placeholder{}, &task)
if task.Operation == WAIT {
continue
}
if task.IsMap {
log.Printf("received map job %s", task.Map.Filename)
err := handleMap(task, mapf)
if err != nil {
log.Fatalf(err.Error())
return
}
} else {
log.Printf("received reduce job %d %v", task.Reduce.Id, task.Reduce.IntermediateFilenames)
err := handleReduce(task, reducef)
if err != nil {
log.Fatalf(err.Error())
return
}
}
}
}
你可以理解为worker的构造函数,通知master给worker分配一个任务,并按照任务的类型去对应的执行handle操作
后续代码就和串行化的操作差不多了,这边不再叙述。
PASSED ALL TESTS
通过本次实验,让我对分布式系统有了初步的认知,同时让我的go语言编程能力得到了有效的提升