一些废话:学6.824是因为实在是喜欢存储,不想再做监控运维之类无聊的工作,想真正成长为一个专业的分布式存储工程师,也是看网上说6.824是分布式学习很好的入门课程,于是就利用空余时间开始学习这门课程。
这门课程的实验都是推荐用golang实现的。其实用其他语言也是可以的,不过我觉得作为初学者,我还是用推荐语言去写,后期遇到问题,也方便查询参考资料。而且语言不应该是障碍,于是,在完成实验中,我也用的是golang。一些go语言使用中,初学者会遇到的坑,也一一记录下来。
背景
首先了解一下MapReduce的背景,这个就直接搬课程内容啦
Google需要在TB级别的数据上做大量计算,比如,为所有网页创建索引,分析整个互联网的链接路径并得出最重要或最权威的网页。Google需要一种框架,可以让它的工程师能够进行任意的数据分析,例如排序,网络索引器,链接分析器以及任何的运算。工程师只需要实现应用程序的核心,就能将应用程序运行在数千台计算机上,而不用考虑如何将运算工作分发到数千台计算机,如何组织这些计算机,如何移动数据,如何处理故障等等这些细节。
原理
重点理解:纯函数式
Map函数使用一个key和一个value作为参数。入参中,key是输入文件的名字,通常会被忽略,因为我们不太关心文件名是什么,value是输入文件的内容content。
Reduce函数的入参是某个特定key的所有实例,Reduce函数的入参是某个特定key的所有实例(Map输出中的key-value对中,出现了一次特定的key就可以算作一个实例)。所以Reduce函数也是使用一个key和一个value作为参数,其中value是一个数组,里面每一个元素是Map函数输出的key的一个实例的value。
master给workers分配任务并记录进度
- master将map任务给workers知道所有map任务完成, 将中间数据写入磁盘,按哈希值将分割的输出映射到每个Reduce任务的一个文件中。
- map任务结束后,master来处理reduce任务,每个reduce任务获得map任务的中间输出,每个Reduce任务在GFS上写一个单独的输出文件。
实验准备
1.安装go
【参考】https://www.jianshu.com/p/49588471af18
$ wget https://golang.google.cn/dl/go1.15.6.linux-amd64.tar.gz
$ tar xz -C /usr/local -xzf go1.15.6.linux-amd64.tar.gz
创建工作目录
$ mkdir -p ~/code/go
$ vim ~/.bashrc
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
export GOPATH=/root/code/go
$ source ~/.bashrc
测试环境是否生效
$ go env
2.下载实验参考代码
6.824提供了完成实验必要的一些通讯相关的代码框架,可以帮助我们完成实验
$ git clone git://g.csail.mit.edu/6.824-golabs-2020 6.824
$ cd 6.824
$ ls
Makefile src
$ cd ~/6.824
$ cd src/main
$ go build -buildmode=plugin ../mrapps/wc.go
$ rm mr-out*
$ go run mrsequential.go wc.so pg*.txt
mrsequential.go是非分布式实现的单词计数任务,可以用来与分布式实现的mapreduce进行对比
课程说明提到开发一个分布式的mapreduce系统,由两个程序组成:master和worker。一个master进程,多个并行的worker进程。workers通过RPC和mater交互。每个worker进程问master要一个task,从这个task中的input读取一个或多个files,执行这个task,将task的output写入一个或多个文件,master需要注意一个worker是否在一段时间内(10s)完成了task,如果没完成,则把这个task给其他的worker。
课程设计已经给了一段代码帮助我们开始,The "main" routines for the master and worker are in main/mrmaster.go and main/mrworker.go,不要改这些files。应该把要实现的应用放在mr/master.go, mr/worker.go, and mr/rpc.go。
编写完mr里的代码之后,如何run your code在word-count app上?
1.首先确认wc.so已经创建
$ go build -buildmode=plugin ../mrapps/wc.go
2.在main目录,run master
$ rm mr-out*
$ go run mrmaster.go pg-*.txt
pg-*.txt参数是mrmaster.go的输入文件,每个文件对应一个“split”,是一个map task的输入
3.在一个或多个窗口执行workers
$ go run mrworker.go wc.so
4.通过a test script in main/test-mr.sh检查wc和indexer
测试脚本检查输出是否正确,是否并行执行,当crash的worker恢复后tasks是否可以恢复
$ cd ~/6.824/src/main
$ sh test-mr.sh
*** Starting wc test.
开始实验
虽然网上好多大神的经验贴都说lab01是洒洒水的,很好实现,但学习这个事就是小马过河,作为golang小白,我觉得是不太容易的,我做了整整1周。期间因为语言的不熟悉带来一些磕磕绊绊,也记录下来,帮助自己更快学会这门编程语言,为后面的实验打基础。
Lab01的任务是实现一个简单的mapreduce system,我根据6.824给的代码,自己根据学习进度做了一些简单的调整。
1.mrsequential.go是一个例子,告诉我们,这个实验不通过分布式,正常编码是怎么样的
1.1关于loadPlugin
实验指导上在go run之前先执行go build -buildmode=plugin ../mrapps/wc.go来创建wc.so作为程序的plugin,可是在操作过程中,发现windows对这个plugin的支持不是很好的样子,然后看代码,这一步的必要性是啥?原来是涉及到一个函数loadPlugin
mapf, reducef := loadPlugin(os.Args[1])
上面这句其实就是要从wc.so中加载Map和Reduce函数,那如果不想用插件呢,也是容易的,直接函数赋值就好了。
//mapf, reducef := loadPlugin(os.Args[1])
var mapf func(string) []KeyValue
var reducef func(string, []string) string
mapf = Map
reducef = Reduce
接着看下mrsequential里有什么内容
*KeyValue [stuct]
//属性
Key、Value
//要实现sort.Interface
Len、Less、Swap
*Map [func]
*Reduce [func]
*main [func]
Map:输入参数contents string,将contents分割成words,返回[]KeyValue{}数组。
e.g
输入:"This is a cat. That is a dog."
输出:[{"This", "1"}, {"is", "1"},{"a", "1"},{"cat", "1"},{"That", "1"},{"is", "1"},{"a", "1"},{"dog", "1"}]
Reduce:输入参数values []string,返回values数组的长度
*values就是"1"的数组
接下来看下main里面是怎么用Map和Reduce函数的,
Usage:mrsequential pg-*.txt
(1)intermediate是一个KeyValue的数组,每个key出现一次,就增加一个{word,"1"}
遍历每一个pg输入文件,将文件内容读入content,调用mapf,加入intermediate数组
(2)将intermediate数组按照key排序,这样,相同的word会挨着
(3)遍历intermediate数组
将拥有相同的key(word)的键值对合并,一个key(word)的values传递给一个reducef
将key(word)和它的值输出到文件mr-out-0
2.lab01需要的代码
我们把这个任务分成三个部分main, mr, wc
main:
package main
mr:
package mr
wc:
package wc
每个package要导入其他package中的代码一般用import来实现,这边有个golang的方法
$ cd /d/My/project/go/workspace/lab01/
$ go mod init lab01
这样lab01目录下就有一个go.mod
module lab01
go 1.15
这个时候代码里再import就不用担心GOROOT/GOPATH配置的问题
import lab01/mr
import lab01/wc
这边也是为了在windows上调试方便,对代码做出的小调整
3.golang rpc基本框架使用
知道了我们程序的最终目标,并且已经把代码框架提取出来,那分布式涉及到的rpc是怎么玩的呢?
main/mrmaster:
调用mr.MakeMaster启动一个master的server,通过Done函数来监听,每次等1s
main/mrworker:
获取wc定义的Map和Reduce函数,调用mr.Worker来执行mapf和reducef
那么接下来看下三个重要的步骤mr.MakeMaster,mr.Done,mr.Worker
都在mr里,好的,那先看下mr的代码结构
mr/master
*Master [stuct]
//属性…自己写
//方法
Example [func] //用来测试rpc
server [func] //启动一个server端,并通过go 开启一个线程监听
Done [func] //…自己写
*MakeMaster [func] //定义一个Master,…自己写,调用server()
mr/worker
*Worker [func]
//…自己写
CallExample()//用来测试rpc
<pre style="background:white">其中args和reply都是用interface{}这个空接口类型定义的,这是为什么呢?原来go 语言规定,如果希望传递任意类型的变参,变参类型应该制定为空接口类型,并且空接口可以指向任何数据对象,所以可以使用interface{}定义任意类型变量。</pre>
rpc测试中master监听,worker调用master中的Example方法,成功!
*CallExample [func]
call("Master.Example", &args, &reply) //调用Master中的Example方法
*call [func]
特别说明下这个函数
func call(rpcname string, args interface{}, reply interface{}) bool {
}
rpcname是要调用的Master中定义的函数名
args是rpcname的输入参数
reply是rpcname的返回值
其中args和reply都是用interface{}这个空接口类型定义的,这是为什么呢?原来go 语言规定,如果希望传递任意类型的变参,变参类型应该制定为空接口类型,并且空接口可以指向任何数据对象,所以可以使用interface{}定义任意类型变量。
测试一下:
$ cd /d/My/project/go/workspace/lab01/main
$ go build mrmaster.go
$ go build mrworker.go
$./mrmaster.exe ../pg-*.txt
2021/01/06 16:10:24 rpc.Register: method "Done" has 1 input parameters; needs exactly three
$./mrworker.txt
reply.Y 100
rpc测试中master监听,worker调用master中的Example方法,成功!
4.mapreduce实现
我画了一个结构图便于自己以后想起来再看自己的代码能懂的快一些(经常看不懂自己写的代码星人的自我拯救)
reference
第一个实验,确实上手很不容易,参考很多大神的资料,在此感谢!
https://github.com/yzongyue/6.824-golabs-2020
https://zhuanlan.zhihu.com/p/187473895
https://blog.csdn.net/bysui/article/details/52128221
https://pdos.csail.mit.edu/6.824/schedule.html
【MIT公开课】6.824 分布式系统 · 2020年春(完结·中英字幕·机翻)https://www.bilibili.com/video/BV1qk4y197bB?p=2
我的代码都放在-》https://github.com/mengmayang/6.824-2020