什么是Map-Reduce呢?

Map指的是一个形如下面定义的函数。

def Map(k, v): //return [(k1, v1), (k2, v2), (k3, v3), ...]
pass

它接受一个key和一个value,返回一组所谓的中间值。注意,返回的不是一个dict,所以k1可能等于k2。

Reduce指的是一个形如下面定义的函数。

def Reduce(k, [v1, v2, v3, ....])://return v
pass

它接受一个key和该key对应的所有在Map函数中返回的value,返回k值的最终结果。所谓Reduce,就是把多个值“消减”为一个。

那么Map-Reduce作为一个框架,通常是这样运行的。

  • 多个节点在分别,独立地执行Map函数。

Map1 ----> (k1, v1), (k2, v2), (k4, v4), (k8, v8)

Map2 ----> (k1, v9), (k4, v10)

Map3 ----> (k8, v11)

  • 然后,多个节点分别,独立地执行Reduce函数。

Reduce1 (k1, [v1, v9])     ----> (k1, <some value>)

Reduce2 (k2, [v2])           ----> (k2, <some value>)

Reduce3 (k4, [v4, v10])   ---->  (k4, <some value>)

Reduce4 (k8, [v8, v11])   ---->  (k8, <some value>)

框架保证了Map们独立地跑,并且每个Reduce接收到的参数,都是经过“总结“不同map的返回值得到的。程序员只需要实现自己的Map和Reduce函数就行了。一个典型的应用就是word count。

Map-Reduce有很多优点,主要就是节点之间在执行函数的过程中并不要通信,而且有很好的扩展。

Google的Map-Reduce实现

有几个要点。

1. 输入和输出以文件的形式放在GFS上。这个文件系统有个特点,虽然是分布式系统,但是写文件和创建新文件是原子操作。

2. 输入的所有文件被分成M份,存放在GFS上。

3. 一个Master节点控制M个worker执行Map操作。v = 其中的一个文件内容。

4. Map操作完成的结果被存放在本地磁盘上,并将存放位置告诉Master

5. 完成所有Map操作以后,Master并行地执行R个独立的Reduce操作。

6. Map生成的中间值的k传给了,第 hash(k) % R个Reduce worker操作。

容错

1. Master周期性ping 所有worker,如果失败,新的闲置worker将重新执行失败的worker上的Map/Reduce操作。

2. Map/Reduce函数都是可以重入的,他们的中间结果,不会被其他节点看见,除非完成。

3. Worker的数目小于需要执行的M或者R

Lab1

这个课程所有Lab都是用Go语言实现的。答案用的Go 1.7,我用了1.8,发现有一些不一样,待会说。

首先git clone这个项目。

git clone git://g.csail.mit.edu/6.824-golabs-2017 6.824

你会看到一个src 文件目录,里面包含各个Lab的子目录。

在MapReduce这个实验,几乎忠实地按照论文 MapReduce: Simplified Data Processing on Large Clusters 实现了Google的MapReduce框架。完成实验对理解论文内容帮助蛮大。

理解这个Go语言实现的框架不是太难,就不笔记了。代码都有。有一点在Go 1.8里要注意。

在common_map.go和common_reduce.go中,需要写文件存放中间结果,comment推荐使用json格式,并使用如下代码。

// enc := json.NewEncoder(file)
// for key := ... {
// enc.Encode(KeyValue{key, reduceF(...)})
// }
// file.Close()

如果你和我一样使用的1.8,并且在doReduce的实现中按照上面的方法去写文件,第一个task是怎么都过不了的。为啥呢?

在Go1.8中,如果按照上面的Encode json,生成的文件是如下的格式

[{key:value}, {key: value}, ....]

打开master_splitmerge.go文件,看一下merge函数,发现他在一个一个decode这些KeyValue,类似下面这样

// deco := json.NewDecoder(file)
// for key := ... {
// deco.Decode(&kv) <---这个就不行
// }
// file.Close()

而在merge函数中的Decode 居然在遇到"["的时候报错=_=b。不知道是不是encoder和decoder在1.8被deprecate,否则以同样方式写却没办法以同样方式读。

建议改用json.marshal/unmarshal

Part I: Map/Reduce input and output

这个就是让你实现doMap和doReduce。注意不是Map和Reduce函数,而是call 客户实现的Map和Reduce函数的逻辑。

首先是doMap。

该函数读取某一个输入文件的内容,然后调用客户实现的Map函数,再把返回值写到文件中。下面是我实现的代码部分。

bytes, err := ioutil.ReadFile(inFile)
if err != nil {
fmt.Errorf("doMap failed to read file %s due to %s", inFile, err)
return
}
contents := string(bytes)
kvs := mapF(inFile, contents) if len(kvs) == 0 {
return
} sort.Slice(kvs, func(i, j int) bool {
return ihash(kvs[i].Key) % nReduce < ihash(kvs[j].Key) % nReduce
}) var pr int = -1
var pkvs []KeyValue
for _, kv := range kvs {
r := ihash(kv.Key) % nReduce
if r != pr {
writeOutKeyValueArrays(jobName, mapTaskNumber, pr, pkvs)
pr = r
pkvs = nil
}
pkvs = append(pkvs, kv)
}
//write last one
writeOutKeyValueArrays(jobName, mapTaskNumber, pr, pkvs)

mapF就是客户的业务逻辑,注意到它返回一个(k, v)的list。这个中间值需要放到文件中。不过放文件,按照规则,需要根据k的hash值,分别放到R个(nReduce个)文件中。由于一共有M个doMap执行,实际上生成的中间文件有M x R个。这个框架生成的中间文件类似这样的格式。

mrtmp.xxx-0-0
mrtmp.xxx-0-1
mrtmp.xxx-0-2
mrtmp.xxx-1-0
mrtmp.xxx-1-1
mrtmp.xxx-1-2

这表示2 (M)X 3(R)

为了节省时间,避免频繁打开关闭文件,我们可以先按照k的hash值排序一下,然后依次打开文件存放它们。

再看一下doReduce函数。一共R个doReduce函数的执行,第r个会读取mrtmp.xxx-*-r 文件。例如,第1个doReduce要读的文件就是mrtmp.xxx-0-1和mrtmp.xxx-1-1。它会读取这些文件中产生的中间值,然后合并同样的key所对应的value们,并传入到客户定义的Reduce函数中。将返回值写到结果文件中。

var aOfKV []KeyValue
for i := 0; i < nMap; i++ {
var kvs []KeyValue
inFilename := reduceName(jobName, i, reduceTaskNumber)
inFile, e := ioutil.ReadFile(inFilename)
if e != nil {
fmt.Errorf("Failed to read file %s due to %v", inFilename, e)
continue
}
json.Unmarshal(inFile, &kvs)
aOfKV = append(aOfKV, kvs...)
}
sort.Slice(aOfKV, func(i, j int) bool {
return aOfKV[i].Key < aOfKV[j].Key
}) //end
aOfKV = append(aOfKV, KeyValue{"", ""}) var curKey string
var curValues []string
var outputKVS []KeyValue
for _, kv := range aOfKV {
if kv.Key != curKey {
if len(curKey) > 0 {
outputStr := reduceF(curKey, curValues)
outputKVS = append(outputKVS, KeyValue{curKey, outputStr})
}
curKey = kv.Key
curValues = nil
}
curValues = append(curValues, kv.Value)
}
marshalledKVs, _ := json.Marshal(outputKVS)
err := ioutil.WriteFile(outFile, marshalledKVs, 0644)
if err != nil {
log.Fatal("Failed to write reduce file:", err)
}

注意这里使用了类似的doMap的逻辑:先排序,再依次合并。所以在所有doReduce执行完成以后,应该有R个最后输出文件,然后框架会调用merge函数做合并。

Part II: Single-worker word count

让你实现word count,没啥说的。就是看一下strings里面的FieldsFunc。

在Map函数中,文件内容都传给你了,要做的就是份成一个词一个词。

func mapWF(filename string, contents string) []mapreduce.KeyValue {
f := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
var kvs []mapreduce.KeyValue
for _, w := range strings.FieldsFunc(contents, f) {
kvs = append(kvs, mapreduce.KeyValue{w, ""})
}
return kvs
}

在Reduce函数中,传进来已经是所有的词的出现了。看一下value 的len好了。

func mapWF(filename string, contents string) []mapreduce.KeyValue {
f := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
var kvs []mapreduce.KeyValue
for _, w := range strings.FieldsFunc(contents, f) {
kvs = append(kvs, mapreduce.KeyValue{w, ""})
}
return kvs
}

Part III: Distributing MapReduce tasks/Part IV: Handling worker failures

我们之前实现的doMap和doReduce都是串行地执行的,也就是,每个Map依次执行,完成以后,再依次执行Reduce。这个从源代码里面的master.go的Sequential函数很容易看出来。

在该函数里面,对每一个input file,都依次调用doMap,在reducePhase,则是把中间结果(就是上面的 mrtmp.xxx-*-*)依次传给doReduce。

// Sequential runs map and reduce tasks sequentially, waiting for each task to
// complete before running the next.
func Sequential(jobName string, files []string, nreduce int,
mapF func(string, string) []KeyValue,
reduceF func(string, []string) string,
) (mr *Master) {
mr = newMaster("master")
go mr.run(jobName, files, nreduce, func(phase jobPhase) {
switch phase {
case mapPhase:
for i, f := range mr.files {
doMap(mr.jobName, i, f, mr.nReduce, mapF)
}
case reducePhase:
for i := 0; i < mr.nReduce; i++ {
doReduce(mr.jobName, i, mergeName(mr.jobName, i), len(mr.files), reduceF)
}
}
}, func() {
mr.stats = []int{len(files) + nreduce}
})
return
}

而这个任务,就是要把串行执行的Map和Reduce,变成并行的。当然,整个Map-Reduce还是运行在单机上的,只不过用了go里面的RPC来模拟论文中master和worker之间的通信。我们可以看看框架已经为我们做了什么。

// Distributed schedules map and reduce tasks on workers that register with the
// master over RPC.
func Distributed(jobName string, files []string, nreduce int, master string) (mr *Master) {
mr = newMaster(master)
mr.startRPCServer()
go mr.run(jobName, files, nreduce,
func(phase jobPhase) {
ch := make(chan string)
go mr.forwardRegistrations(ch)
schedule(mr.jobName, mr.files, mr.nReduce, phase, ch)
},
func() {
mr.stats = mr.killWorkers()
mr.stopRPCServer()
})
return
}

首先,master启动一个RPC server。在这个master server中,它会暴露一个叫做”Register“的函数。这个Register函数的主要作用就是接收worker的Register(好废话),把worker(它其实也是一个RPC server)的RPC地址,通过调用RPC 调用master上的Register,放到master的一个list里面。

// Register is an RPC method that is called by workers after they have started
// up to report that they are ready to receive tasks.
func (mr *Master) Register(args *RegisterArgs, _ *struct{}) error {
mr.Lock()
defer mr.Unlock()
debug("Register: worker %s\n", args.Worker)
mr.workers = append(mr.workers, args.Worker) // tell forwardRegistrations() that there's a new workers[] entry.
mr.newCond.Broadcast() return nil
}

接下来,forwardRegistrations把master中的registered worker从list中提取出来,依次发送到channel。

而我们要实现的具体代码在schedule.go里面。其中schedule函数需要完成指定数目的task。指定数目指的是,如果是做的Map 阶段,那指定数目就是输入文件的数目M;如果是在Reduce阶段,就是R。那么schedule应该有下面的步骤:

  1. 如果指定数目的task还没有完成那么 从channel中提取一个注册的worker RPC的地址;否则3。
  2. 异步调用该worker上的DoTask方法。 在DoTask 方法中,已经有对doMap和doReduce的调用的逻辑,DoTask成功返回以后,重新把worker放回到channel,以待重用。goto 1。
  3. 等待所有task完成后退出。

在2中,有一个限制,我们不能给同一个worker RPC 地址发送多次DoTask请求。要实现这个,我们可以在把worker从channel提取出来以后,不放回去,然后待到DoTask成功返回后,才放入待重用。

另外,在Task 4中,当发送一个DoTask请求以后,该请求可能超时或者失败。这时候需要重试。我们的想法是在在第二中的异步调用逻辑里面,如果DoTask 返回失败,不把worker重新放入channel,而是直接丢弃。再从channel中取一个worker。

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, and only once all of
// them have been completed successfully should the function return.
// Remember that workers may fail, and that any given worker may finish
// multiple tasks.
//
doneCh := make(chan int)
doneCnt := 0
var mux sync.Mutex
for nt := 0; nt < ntasks; nt++ {
go func(fileIndex int) {
for {
workerName := <-registerChan
ok := call(workerName,
"Worker.DoTask",
DoTaskArgs{jobName, mapFiles[fileIndex], phase, fileIndex, n_other},
nil)
if ok == false {
fmt.Printf("Failed to RPC call %v at %d, retrying\n", workerName, nt)
continue
}
mux.Lock()
doneCnt += 1
if doneCnt == ntasks {
doneCh <- 1
}
mux.Unlock()
registerChan <- workerName
break
} } (nt)
}
<-doneCh
fmt.Printf("Schedule: %v phase done\n", phase)
}

注意这个nt一定要传到goroutine里面去,而不是作为一个闭包变量,因为它在循环中会变。

Part V: Inverted index generation (optional for extra credit)

这又是一个Map-Reduce应用。是让你看某个单词在哪些文档里面出现过。

同样的,在Map函数中分词,词作为key,而value是文档名字。

而Reduce函数中,仅仅是把文档列出来,配上文档数目。代码比较简单就不贴了。

第一次做课程,觉得作业设计上很用心。

[分布式系统学习] 6.824 LEC1 MapReduce 笔记的更多相关文章

  1. [分布式系统学习] 6.824 LEC3 GFS 笔记

    Google File System 第三课的准备是阅读论文GFS.该论文是分布式系统中经典论文之一. 读完做一点小总结. GFS的feature 1. 非POXIS接口API,支持对文件和文件夹的创 ...

  2. [分布式系统学习] 6.824 LEC2 RPC和线程 笔记

    6.824的课程通常是在课前让你做一些准备.一般来说是先读一篇论文,然后请你提一个问题,再请你回答一个问题.然后上课,然后布置Lab. 第二课的准备-Crawler 第二课的准备不是论文,是让你实现G ...

  3. 第2节 mapreduce深入学习:14、mapreduce数据压缩-使用snappy进行压缩

    第2节 mapreduce深入学习:14.mapreduce数据压缩-使用snappy进行压缩 文件压缩有两大好处,节约磁盘空间,加速数据在网络和磁盘上的传输. 方式一:在代码中进行设置压缩 代码: ...

  4. 第2节 mapreduce深入学习:7、MapReduce的规约过程combiner

    第2节 mapreduce深入学习:7.MapReduce的规约过程combiner 每一个 map 都可能会产生大量的本地输出,Combiner 的作用就是对 map 端的输出先做一次合并,以减少在 ...

  5. 第2节 mapreduce深入学习:6、MapReduce当中的计数器

    第2节 mapreduce深入学习:6. MapReduce当中的计数器 计数器是收集作业统计信息的有效手段之一,用于质量控制或应用级统计.计数器还可辅助诊断系统故障.如果需要将日志信息传输到map ...

  6. 学习参考《零基础入门学习Python》电子书PDF+笔记+课后题及答案

    国内编写的关于python入门的书,初学者可以看看. 参考: <零基础入门学习Python>电子书PDF+笔记+课后题及答案 Python3入门必备; 小甲鱼手把手教授Python; 包含 ...

  7. 学习《零基础入门学习Python》电子书PDF+笔记+课后题及答案

    初学python入门建议学习<零基础入门学习Python>.适合新手入门,很简单很易懂.前一半将语法,后一半讲了实际的应用. Python3入门必备,小甲鱼手把手教授Python,包含电子 ...

  8. 前端程序员学习 Golang gin 框架实战笔记之一开始玩 gin

    原文链接 我是一名五六年经验的前端程序员,现在准备学习一下 Golang 的后端框架 gin. 以下是我的学习实战经验,记录下来,供大家参考. https://github.com/gin-gonic ...

  9. [分布式系统学习]阅读笔记 Distributed systems for fun and profit 之一 基本概念

    因为工作的原因,最近打算看一些分布式学习的资料.其中这个http://book.mixu.net/distsys/就是一篇非常适合分布式入门的介绍. 这个短小的材料有下面5个小的章节,图文并茂,也没有 ...

随机推荐

  1. MySQL修改密码和忘记ROOT密码

    1.关闭数据库 脚本:[root@mysql etc]# service mysql stop 2.使用脚本: mysqld_safe --skip-grant-tables 启动数据库 使用/usr ...

  2. Do you want a timeout?

    Do you want a timeout?   You’re feeling accomplished and excited; the new features for your applicat ...

  3. Gibbs采样

    (学习这部分内容大约需要50分钟) 摘要 Gibbs采样是一种马尔科夫连蒙特卡洛(Markov Chain Monte Carlo, MCMC)算法, 其中每个随机变量从给定剩余变量的条件分布迭代地重 ...

  4. scala spray 概念性内容的总结

    spray 是基于 akka 的轻量级 scala 库,可用于编写 REST API 服务.了解 spray 的 DSL 后可以在很短的时间内写出一个 REST API 服务,它的部署并不需要 tom ...

  5. Java获取项目当前请求的全部URL,Java获取Referer,Java获取完整链接地址URL

    Java获取项目当前请求的全部URL,Java获取Referer,Java获取完整链接地址URL >>>>>>>>>>>>> ...

  6. Redis存读取数据

    这一节演示下载.NET中怎样使用Redis存储数据.在.net中比较常用的客户端类库是ServiceStack,看下通过servicestack怎样存储数据. DLL下载:https://github ...

  7. beef 安装使用

    http://www.freebuf.com/articles/web/5511.html

  8. Windows下mysql重设密码

    Windows下的实际操作如下 .关闭正在运行的MySQL,即关闭服务. .打开DOS窗口,转到mysql\bin目录. .输入 mysqld --skip-grant-tables 回车.如果没有出 ...

  9. iOS - UITextView在调用textViewDidChange方法,九宫格相关中文输入的问题

    问题一 iOS textView在调用 UITextViewDelegate 的 textViewDidChange方法,九宫格相关中文输入的问题 有时候,需要在textViewDidChange处理 ...

  10. 在MyEclipse(2015)中上传项目到github的步骤(很详细)

    (图文)在MyEclipse(2015)中上传项目到github的步骤(很详细) git|smartGit使用详解 SmartGit使用教程