论文:
MapReduce: Simplified Data Processing on Large Clusters
Jeffrey Dean and Sanjay Ghemawat
https://pdos.csail.mit.edu/6.824/papers/mapreduce.pdf
MapReduce 是一种分布式系统中处理大数据方法。他提出是在 2004, jeff dean 和 Sanjay Ghemawat 的作品,和 GFS、BigTable 并列 Google 分布式系统的三驾马车。后面,基于 mapreduce 的框架由于种种问题已经很少使用了(2014 Google I/O),但是 map reduce 的思想(感觉也是人很自然想到的分治思想)不过时。
mapreduce 框架淘汰的原因是,对于简单的操作,map reduce 的严格框架方便了下层,但是上层写协同的处理时候必须很复杂。而底层的实现上仍然涉及很多部署和运维、整体的性能优化等问题,并不是写一个 map + reduce 函数就能成功了。结果就是,反而这层隔离抽象又好像没有了,仍然需要技术团队根据不同的业务数据特征折腾不同的性能优化和配置(属于是超参数)。
未来的层次抽象可能希望集中上层业务注意力到写 OLAP 的 DML 上,分布式的 HTAP、OLAP 数据库负责搞定下层的各种脏活(spanner/f1)?
整体论文看了我好久,而且最后感觉我自己的笔记好像也没做到什么。但是在读论文之前看别人的笔记或者简略的话我又感觉不知道他在说什么,缺失了某种上下文信息。
看完论文之后再看别人的笔记,好像的确是这么回事,而且比我打的精美多了。
看来看的细节的东西确实有用,变成某种常识了。但是又不能直接一上来就给你丢几个概念,这样是去掉了分析法的过程,只剩下综合法不知道他怎么来的,最近用不同的材料复习数学也有同感。
以后可以直接找别人写好的笔记,自己只是读一遍过理解细节之后,这样效率可能高一点,因为其实笔记也做不了什么,反而分心,利用前人的笔记其实均摊下来别人写笔记的花费也分摊了。不过对一些细节没弄懂的地方,思考和阅读其他资料的过程是不能丢掉的了。
更新:后来想了一下,原论文没有 motivation 和 background,所以以问题的形式存在的笔记效果会好一点,因为这样强迫你去回答要点,而不是含糊的暧昧跳过。所以回来补充以下关键的一些点。我的一句话总结就是,map - shuffle - reduce = select - group by - aggregation。
总结一下:2022/5/30 14:50
- map reduce 即 merge sort。数据更与一定原理分片,然后得到中间结果,中间结果要先做 aggregation 和 shuffle 基于优化。然后再 reduce 即再 merge 得到结果。
- 实现:RPC、GFS。
- 错误容忍:
- 处理会死的 worker(同时要避免误判,避免重复运行);
- master 的 logging+checkpoint;
- 优化:
- worker 的任务分配执行文件就近调度;
- 调参;
- 备份冗余 worker,看谁先完成。
- 结论:
- 严格的编程模型可以方便的进行上面所的各种问题的解决和设计(其实就是类似于 IR 吧)。
- 网络带宽很贵,要做优化主要减少 RPC call/data send (类似减少 DrawCall 吧)。
- 冗余任务能避免有人没睡醒(很棒的思路)。
MapReduce 是什么概念? |
他是一个编程模型,然后基于这个严格的编程模型,可以隔离上层和下层的逻辑,从而能够思想分布式的大数据计算。上层通过在严格的 map reduce 编程模型下编写业务逻辑(计算),下层框架统一解决分布式计算的 dirty work 细节,并行运算、错误容忍、数据分发、负载均衡等。 |
第二次总结:2022/6/8 14:25
MapReduce 的编程模型思想就是 map(函数,e.g. std::apply)+reduce(aggregate, e.g. std::accumulate)。然后,但是这个模型这里有一些细节需要理解的。
首先是模型里面处理的数据模式,我们必须明白到底为什么这样设计。
我们知道,mapreduce 里面数据是以 key-value pair 的形式存在的,编程的时候,有时候用 `unordered_map/map`, 有时候用 `unordered_set/set`, 那么什么时候用 `set` 什么时候用 `map` 呢(当然我们都知道 `map` 就是用 `set` 实现的,不过底层 `node` 类型是一个 `pair<const KeyType, ValueType>` 而已。)?
所以这里要理解一下为什么 mapreduce 的编程模型是 kv pair。
为什么 MapReduce 是数据是基于 key - value 的? | ||
普通的 map + reduce (源自 lisp 函数是编程模型)的表达能力有限,可以认为是限定了某个 key 下做 map + reduce,比如对一个数组做平方再求和。大数据下,除了 aggregate 全局数据,一般还需要一个 group by 的语义,这一点是类似于 SQL 的设计的。(当然,下面的 SQL 语句举例中,没有 map 的步骤,这一点了解就好,实际如果我们对 * 进行一些 projection 是不是就能当作是一种 map 呢,笑)。 | ||
即,values(set) 的 mapReduce =
而 key-value 的 mapReduce =
|
然后是编程模型的细节,mapreduce 的编程模型中,用户需要编写的有两个部分,就是 map 和 reduce。用户需要注意到的有三个部分,分别是 map、shuffle、reduce。其中并行多机处理的阶段是 map 和 reduce,而 shuffle 阶段要处理中间结果,数据重新分派等工作。
首先 map。
MapReduce 的 map 是什么意思?他的输入和输出分别是什么? |
Map 就是对源输入进行一些处理产生一些输出。这里的输入 1 可能输出 1 或者输出 n,就是一个处理过程。他的输入是一些 key value PAIRS,输出另一些 key value PAIRS。由于一般来说 map 都是做一个一对多的mapping,因此一般理解为拆解split,所以才有下面的图片里面切面包和切黄瓜,这一种 map 其实更难理解,因为他要把同样的面包片映射到不同的输出商品去。另一种 map 比如收集到n个用户的所有资料,我们要丢掉用户,而拆解为用户对 A 的喜爱,对B的喜爱 etc...。 |
(k1, val1), (k1, val2) , (k2,val3) ---> (k3, val4), (k3, val5), (k3, val6), (k4, val7), (k5, val8)... |
shuffle 是一个框架处理的中间过程,结果送 reduce。
MapReduce 的 shuffle 是什么意思?他的输入和输出是什么? |
shuffle 不是打乱的意思而是把同一个 key 的都分配给同一个 reducer。这里实际做的是中间结果的 group by 操作! |
(k3, val4), (k3, val5), (k3, val6), (k4, val7), (k5, val8) -->(k3: {val4, val5, val6}), (k4, {val7}), (k5, {val8}) |
这里 shuffle 阶段之前,其实还有一个 combine 优化 (论文没有)
Combine 优化 |
在 hadoop 的 mapreduce 里面,这里涉及一个二次分片 的类似 hash partition 的东西,从而方便进行 group by,而且还有局部聚合,这样方便,比如多个 mapper 先内部 partition,local group by 之后,再送 reduce 做,这个过程叫做 combine 优化。combine 优化不需要 shuffle,就提前 reduce,可以是单机多核 mapper 的情况下看作是单机做 reduce。 |
Reduce 就是写一个聚合函数而已。
MapReduce 的 reduce 是什么意思?他的输入和输出分别是什么? |
reduce 就是做 aggregation, 不过 reduce 的 key value 也可以重新 map 的,看你喜欢。 |
(k3: {val4, val5, val6}), (k4, {val7}), (k5, {val8}) ---> (k3: sth3), (k4: sth4), (k5: sth5) |
实际尽管编程模型已经严格了,在技术能手的把玩之下,也是可以绕过一些限制的,这主要还是依赖于问题的同构性质和一些 workaround 之类的技术。
说到这里,下面我之前做的的笔记其实就是垃圾啊!其实我根本不在乎 infra 是怎么做的(因为论文本来也只是 high level 提了一下,只要知道下面的那张图 task 是怎么分配的就差不多了,具体的怎么做 rpc 的,怎么做分布式文件系统的还得看 gfs 论文)。。。。
下一个问题是这次实验作业要做的 spark。前面提到 mapreduce (指 hadoop 的 mapreduce)淘汰了,所以要讲一个继任者。Spark,spark 的编程模型同样是基于 mapreduce,但是不再严格,灵活性更高。这里就不是这个博客的内容了,到此为止。
基于上面的故事,附上一些直观的图片(可能有版权问题):
简介
- 框架:传统的并行主要是单机上获得的。基于分布式系统上的并行涉及到许多其他问题,包括时钟的不同步、潜在的故障、主机间消息通信的网络问题等。实际要处理这些东西,用户(工程师)就需要处理大量的 accidental 的事情,而不是专注在业务处理上。
- map 和 reduce:在函数式编程里面,map 和 reduce 是针对列表的很常见的两种编程思路。map 顾名思义就是完成一个映射,可以认为是 std::apply ,而 reduce 是能够让一个函数在一个列表上一直执行下去。
- reduce 意义:当时学 CS61A 的时候,reduce 的用法好像是用来实现同一个操作的 uncurrying (uncurrying 的指代范围更大吧)。比如基于 std::max 做支持任意长度参数的 max。或者做递归求和、减法、乘法。
- Map + Reduce 使用举例
- map 的一对多:值得注意的是 map 的返回结果可以是更多的数量的,比如这样:
- 图例
其实从这幅图里面就大概能理解很多细节上为什么还有很多中间的一些操作,比如 shuffle 过程。下面具体讲的 map reduce 是基于 key value pair 上的操作,所以细节是和上面的简单例子是不一样的。
Introduction
- 问题:大数据的数据和计算都是分布式的,可能有成百上千个机器,需要解决的问题包括怎么并行运算,怎么分发数据,怎么处理失败。为了解决这些问题,很简单的运算也变成了非常复杂的代码。
- 框架:MapReduce 编程模型主要是上层基于这个模型来写程序,下面的底层细节(parallelization, fault-tolerance, data distribution, load balancing)不需要程序员关心。提出一个固定的模型,这样底层做优化和解决各种分布式系统的问题也更加方便。
所以论文主要就是从两方面讲解,一个是这个简单的 Programming model,是一个接口。第二个就是接口的实现。
Programming Model
- 数据类型:主要的数据类型是 kv pairs,主要的逻辑是 Map and Reduce。
- 编程模型:首先大致看一下整个 map reduce 再框架里面的过程:
- Map 实现:
比如写一个统计文档中各个单词数量的程序,可以这样写 map:
这里的 EmitIntermediate 这样写,其实是类似数据库里面 Query Processing 里面为了提高并行度而采用的火山模型(迭代器模型)那样,才有更高的并行度(底层可以任意实现)。如果只是加入到某个列表中,就还是同步的单机模型。实际 MapReduce 的也是通过迭代器模型来实现。
- Reduce 的执行是类似数据库 Query Processing 里面的 pipeline breaker 必须打断流水线的操作。所有的 aggregate 都是 breaker,reduce 其实也类似于一个 aggregate 的过程。
还是在统计词频程序里面,注意的是 MapReduce library 会把 map 产生的所有相同 key 的中间结果做一个 aggregate 再传给 reduce 函数的。
应用举例:
- 分布式的 grep
- url 访问频率(这种其实和词频统计是一类问题)
- Reverse Web Link graph
- term vector per host
- inverted index
- distributed sort
接口
- Hadoop:由于 MapReduce 是一个分布式部署的框架,Google 的论文中的 sample code 是用 C++ 写的,但是 Google 的框架没有开源。广泛使用的其实是 Apache 的 Java 平台的 Hadoop (HDFS + MapReduce + BigTable 的实现)。一般来说大数据主要为文本信息,为了支持多语言,Hadoop 提供了 Hadoop Streaming 和 Hadoop Pipes 等方法借助标准 IO 和管道等进程间通信来支持多种语言编写 mapper 和 reducer。
- 调试版:如果学习的时候希望在单机只是练习 MapReduce 的使用的话,可以用一些单机的多线程模拟 MapReduce 的库,但是那样其实没什么意思。
- Docker:单机实践的时候,可以使用 docker 来完成集群的模拟。
Docker 的主要原理其实在 6.s081 学虚拟化和 dune 的时候基本都明白了,虚拟机是模拟整个 OS 通过硬件虚拟化运行在 host 上,而沙盒或者 docker 这种容器系统只是虚拟了一个让进程运行的环境出来,减少浪费,主要的 kernel 还是用 host 的(docker 只支持 linux 系统,当然,win 上运行 linux container 的方法就是,先做一个 vm,再基于他跑多个 docker container 就行了)。
- Hadoop 使用:hadoop 的配置需要一系列配置,包括配置好 HDFS。可以参照 apache 官方的中文教程。配置好后第一个例子是 WordCount:
public class WordCount {
public static class Map extends MapReduceBase implements Mapper<LongWritable, Text, Text, IntWritable> {
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
public void map(LongWritable key, Text value, OutputCollector<Text, IntWritable> output, Reporter reporter) throws IOException {
String line = value.toString();
StringTokenizer tokenizer = new StringTokenizer(line);
while (tokenizer.hasMoreTokens()) {
word.set(tokenizer.nextToken());
output.collect(word, one);
}
}
}
public static class Reduce extends MapReduceBase implements Reducer<Text, IntWritable, Text, IntWritable> {
public void reduce(Text key, Iterator<IntWritable> values, OutputCollector<Text, IntWritable> output, Reporter reporter) throws IOException {
int sum = 0;
while (values.hasNext()) {
sum += values.next().get();
}
output.collect(key, new IntWritable(sum));
}
}
public static void main(String[] args) throws Exception {
JobConf conf = new JobConf(WordCount.class);
conf.setJobName("wordcount");
conf.setOutputKeyClass(Text.class);
conf.setOutputValueClass(IntWritable.class);
conf.setMapperClass(Map.class);
conf.setCombinerClass(Reduce.class);
conf.setReducerClass(Reduce.class);
conf.setInputFormat(TextInputFormat.class);
conf.setOutputFormat(TextOutputFormat.class);
FileInputFormat.setInputPaths(conf, new Path(args[0]));
FileOutputFormat.setOutputPath(conf, new Path(args[1]));
JobClient.runJob(conf);
}
}
- 迭代器模型:以这个作为例子就了解了具体是怎么写 map 和 reduce 的了,很显然的,就是简单的迭代器模型,完全像我们学数据库里面 Query Processing 过程的火山模型。通过用户层是 Next 来做的,最后底层就能进行中间的其他非关键性工作的扩展,比如分发到集群里面的不同机器、调度、保证安全东西等。
while (tokenizer.hasMoreTokens()) {
word.set(tokenizer.nextToken());
output.collect(word, one);
}
这里,output 是传进来的,所以 collect 的时候实际做的可以是进行某个 RPC/RMI 然后 reducer 前面有一个流程会做 按 key group 操作,这个最直接的思路是之前学的 hash aggregate(具体是什么实现之后 lab1 就是写一个 mapreduce 框架,所以不急),之后再传给 reducer。
到实现这一步的时候,考虑的东西其实比较多。单机多核的分布式对于 NUMA 架构(现在的 CPU 基本都是 NUMA,Non Uniform Memory Access, 即各核心划分独立的内存控制器,和 UMA 比少总线竞争)来说,要尽量减少非本地内存的访问,容易出现性能不稳定。对于分布式系统,共享内存无法使用。论文主要讨论分布式系统的,他们的数据通信需要通过网络实现。
具体讨论前提是:
Execution
- Execution:这个下面这个图非常详细的描述。每个 core 我都圈起来了(output file 的不应卷进来,这是我的标注错误)。worker 是用来执行的,至于是执行 map 还是 reduce,看 master 的调度。可能一个 worker 一开始也执行是 map,等到一个 split 完成了之后,master 会把他指派为执行 reduce 的继续工作。
- 数据分区:其中要注意的是,split 是对应 worker 的数量的,而 intermediate result 的 R partiotions 是 match 执行 reduce 的 worker 的数量的(which is specified by the user)。
- 数据存放:两部分数据,一部分是 local(图中的 intermediate files),不过这部分马上也要流转给 reduce worker 的,map worker 需要一直努力工作,工作成果需要不断上交给 reduce worker,他们换得的是所有 map tasks 完成之后无事可做的空虚。另外一部分数据是 global 的,这里图片的 6 实际是要输出给最终的地方(global)的(reduce worker 也不是 user program 的受益者,只是搬砖工,遍身罗绮者,不是养蚕人,风口过去了之后他也会变回 map worker 的)。不过 global 的文件数量是 one per reduce task,这个不能 share 的,不然你也没办法并行 append 文件。
- RPC :remote read (对应 hadoop mapreduce 框架里面的 Iterator<XXX> 那个的 next 函数)是通过 RPC 从 MapReduce 框架完成一次远程的硬盘读写的。
- Master Data Structure:对于 master 而言,要存储一些 meta 信息,以及 XControll Block,包括 task state、worker identifier。基本的网络编程的调度器了属于,基本和 POSAv2 中 Reactor 模式里面的 Reactor 做的差不多。master 还是一个信息发布获取中心,他是 the conduit through which the location of intermediate file regions is propagated from map tasks to reduce tasks. 对于 map worker 完成的时候,他要负责获取中间结果的远程访问信息,从而能让 reduce worker 通过RPC 访问。
Fault Tolerance
- worker failure: 通过 keepalive 技术,如果 worker 死了,rollback running tasks which is on the dead worker,然后 reschedule 就行了。因为部分完成了的数据存在 local disk,可能会重复做一些东西,这是不可避免的。reducer 的情况轻松一点,因为他的输出是字节到达 global disk 的. 当然如果 worker 死了,task 重新分配,reducer 也要知道从新的 worker 调用 RPC 而不是已经死的。
- Master failure:用数据库、OS 磁盘文件系统经典做法,checkpoint 写 log,把上面提到的 master data 写进文件里面。不过 Google 的实现里面没有做这部分。
However, given that there is only a single master, its failure is unlikely; therefore our current implementation aborts the MapReduce computation if the master fails. Clients can check for this condition and retry the MapReduce operation if they desire.
- 误判:(这个只是我对原文的 Semantics in the Presence of Failures 前面的理解)worker failure 还有一个副作用,就是 reduce task 可能会重复运行。考虑一个 reduce worker failure 误判的情况,此时重启了一个新的 reducer,问题就会发生。解决方案是 reduce worker 必须通过底层 OS 的文件锁提供 atomic 支持。
- 语义支持:确定性的 map 和 reduce 函数,框架保证行 produces the same output as would have been produced by a non-faulting sequential execution of the entire program。为对于非确定性的程序,只能保证一个 reducer task intra 的顺序等价,inter 的情况可能会有 interleave 。
优化
- 分布式存储就近调度: 这个我不想拙略地加工翻译一遍了,很简单的思路。至于具体实现的 GFS (2003)以及 HDFS (实现 MapReduce 需要基于一个分布式文件系统,Hadoop 就自己改进了一个)之后在 6.824 和校内课程的阅读作业里都还会读的,这个不急。
- Task Granularity:这个主要是 split M 和 R 的时候调参,这个这里不分析了,涉及时间空间、RPC 网速、负载均衡等抉择。MapReduce 框架真正实现了之后,要调的参数太多了,谷歌自己都没有玩明白(Google AI Blog: Sorting Petabytes with MapReduce - The Next Episode (googleblog.com))
- backup Tasks:就是说有时候有些机器或者环境到了最后变差了(straggler,落伍者),与其干等,不如做些冗余,当一个MapReduce计算接近完成时,master会调度一个备用(backup)任务来执行剩下的处于正在执行中(in-progress)的任务。论文说这样做能提速44%,具体是什么情况倒置这个是最后任务落伍,论文的例子:
虽然上面好像感觉很多东西了,但是仔细想一下感觉也是很想当然的思路,好像这些思路之前的其他课都讲过了。不过实际要做出来,还是有很多细节。
论文第四节讲的是一些扩展功能。时间关系,我暂时略过这部分。第五部分讲的是 performance。理论上读论文最关心的其实就是 performance,包括运行性能和稳定性,但是学习理论的时候反而没什么好看的(除非有什么奇妙细节分析)。第六部分是一些经验,讲解了用上 mapreduce 的一个搜索引擎应用 Large-Scale Indexing,感觉也是一些 PPT 的东西,这里也略过了。
Refinements
略