拿到新项目第一步:按照教程看看能不能运行成功,然后分析跟步骤有关的代码
$ cd ~/6.824
$ cd src/main
$ go build -buildmode=plugin ../mrapps/wc.go
$ rm mr-out*
$ go run mrsequential.go wc.so pg*.txt
$ more mr-out-0
// go build -buildmode=plugin ../mrapps/wc.go这段会执行构建一个插件wc.so以便mrsequential.go调用。
// go run mrsequential.go wc.so pg*.txt表示编译并运行mrsequential.go。其中wc.so是插件,pg*是输入数据集。
// more mr-out-0可以查看输出数据集,其中mr-out-0即输出数据。
分析所涉及到的文件:
// wc.go
// map程序
// map程序对于每一个输入文件会执行一次
// 第一个参数是输入文件的名称,第二个参数是输入文件的完整内容
// 输出是key/value对的切片集合(相当于数组,数组里每一个对象是key/value对)
func Map(filename string, contents string) []mr.KeyValue {
// 定义一个方法,判断输入r是否是一个字母字符(可以用来切分文章中的单词遇到空格返回false)
// 输入是一个rune的数据类型(等同于int32,常用来处理unicode或utf-8字符)
// 返回一个布尔值,r是字母字符则返回false,否则返回true
ff := func(r rune) bool { return !unicode.IsLetter(r) }
// 将文章内容切分成单词数组
// 例如"hello world"就会切分成["hello" "world"]
// FieldsFunc是一个可以按照自定义规则切分字符的函数
words := strings.FieldsFunc(contents, ff)
// 生成一个key/value对集合,其中key是文章中的单词,value都为1
kva := []mr.KeyValue{}
for _, w := range words {
kv := mr.KeyValue{w, "1"}
kva = append(kva, kv)
}
return kva
}
// reduce程序
// 所有map任务生成的中间结果集会作为reduce的入参被执行一次
// 入参key是单词,values是key出现次数的集合,values中每一个的值都为1,所以values的大小就是单词key出现的次数
// 可参考下图:
// Input1 -> Map -> a,1 b,1 c,1
// Input2 -> Map -> b,1
// Input3 -> Map -> a,1 c,1
// | | |
// | | -> Reduce -> c,2
// | -----> Reduce -> b,2
// ---------> Reduce -> a,2
func Reduce(key string, values []string) string {
// strconv函数将int转化为string返回
return strconv.Itoa(len(values))
}
// mrsequential.go
// ByKey是用于将中间结果集进行排序用的
// 排序后key相同的就会排在一起,就方便作为reduce的入参
type ByKey []mr.KeyValue
func (a ByKey) Len() int { return len(a) }
func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByKey) Less(i, j int) bool { return a[i].Key < a[j].Key }
func main() {
// 运行时的参数个数若少于3则报错并退出程序
// 在go run mrsequential.go ../mrapps/wc.so pg*.txt中
// 参数0:mrsequential.go
// 参数1:wc.so
// 参数2:pg*.txt(这里其实输入文件算是多个参数)
if len(os.Args) < 3 {
fmt.Fprintf(os.Stderr, "Usage: mrsequential ../mrapps/xxx.so inputfiles...\n")
os.Exit(1)
}
// --------------------------map任务开始--------------------------
// 从参数1(wc.so)中读取map程序和reduce程序
mapf, reducef := loadPlugin(os.Args[1])
// 读取每一个文件作为map程序的入参,并输出中间结果
intermediate := []mr.KeyValue{}
for _, filename := range os.Args[2:] {
// 打开文件
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 read %v", filename)
}
file.Close()
// 输入map程序
kva := mapf(filename, string(content))
// 中间结果
intermediate = append(intermediate, kva...)
}
// --------------------------map任务结束--------------------------
// 对中间结果进行排序
sort.Sort(ByKey(intermediate))
// 创建输出文件mr-out-0
oname := "mr-out-0"
ofile, _ := os.Create(oname)
// --------------------------reduce任务开始--------------------------
// 由于中间结果集是有序的,所以相同的key/value对会连续放置在一起
// 只需要将key相同的中间结果集作为reduce程序的输入即可
i := 0
for i < len(intermediate) {
// i表示key相同的单词的第一个的位置
// j表示key相同的单词的最后一个的后一位
j := i + 1
for j < len(intermediate) && intermediate[j].Key == intermediate[i].Key {
j++
}
values := []string{}
// 遍历从i到j之间的key/value对,全部都是同样的key并且value都为1
// 并作为reduce的入参
for k := i; k < j; k++ {
values = append(values, intermediate[k].Value)
}
output := reducef(intermediate[i].Key, values)
// 输出reduce的结果到mr-out-0文件中
fmt.Fprintf(ofile, "%v %v\n", intermediate[i].Key, output)
i = j
}
// --------------------------reduce任务结束--------------------------
ofile.Close()
}
// 加载插件中的方法
// 入参插件文件名
// 返回值为map方法和reduce方法
func loadPlugin(filename string) (func(string, string) []mr.KeyValue, func(string, []string) string) {
p, err := plugin.Open(filename)
if err != nil {
log.Fatalf("cannot load plugin %v", filename)
}
// 查找插件中Map方法,并赋给mapf
xmapf, err := p.Lookup("Map")
if err != nil {
log.Fatalf("cannot find Map in %v", filename)
}
mapf := xmapf.(func(string, string) []mr.KeyValue)
// 查找插件中的Reduce方法并赋给reducef
xreducef, err := p.Lookup("Reduce")
if err != nil {
log.Fatalf("cannot find Reduce in %v", filename)
}
reducef := xreducef.(func(string, []string) string)
//返回map和reduce方法
return mapf, reducef
}
第二步:梳理文件结构
src
├─mrapps :定义了Map和Reduce两个函数,而不必关心后面的任务(分布式or单机式)
│ ├─wc.go
│ ├─indexer.go
│ └─...
├─main
│ ├─mrsequential.go : 单机式的MapReduce实现
│ ├─mrcoordinator.go :coordinator启动程序
| ├─mrworker.go : worker启动程序
| ├─pg*.txt
| └─...
├─mr
| ├─coordinator.go : coordinator的具体实现
| ├─worker.go :worker的具体实现
| └─rpc.go :规定了 RPC 通信中需要传递的变量等信息
- mrapps:包括一些已经编写好map和reduce程序的示例代码,如wc.go是计数程序,indexer.go是一个文本索引器程序
- main:包括一个串行调度mapreduce的程序mrsequential.go以及一些输入数据pg*.go
- mr:我们包括了mster.go、worker.go、rpc.go,是我们需要实现的并行版本的mapreduce调度器
第三步:根据课程要求初步写出相应的模块功能:
未解之谜:
nReduce是干什么的?