system desing 系统设计(六):MapReduce原理
前面介绍的GFS和BigTable本质上都是数据的分布式存储,核心思路并不复杂:一个集群有多个节点,通过特定的算法(比如consistent hash)找一个合适的slave存放就行了!读的时候通过同样的算法找到slave读取就行,整个流程理解起来很容易,没啥特别的!除了分布式存储,还有个很重要的需求: 分布式计算!其原理和分布式存储一样:把大块的数据切分成小块小块的,然后分别在各个不同的slave上去计算,然后把计算结果汇总一下就行!思路也挺简单的,单需要具体落地实现时的问题来了:
- 因为是集群,所以slave众多,每个slave怎么计算数据了?并且肯定是要预留接口给开发人员,让其自定义数据计算逻辑的,否则这种分布式计算的框架根本没法推广
- 每个slave计算完毕,怎么整合结果了?整合的逻辑是啥?这个整合逻辑肯定也是要开放给开发人员的,不可能在出厂的时候就写死!
google不愧为google,汇聚了很多顶尖人才,人家在十几年前就想到了一个框架, 成为了分布式计算的鼻祖,并且直接促进了后续hadoop的诞生!解决以上两个问题的核心思路:
- 分布式计算的接口肯定是要开放给开发人员去自定义数据的处理逻辑,这个本质就是个接口,框架运行时回调这个接口函数就行了,实现起来并不难
- 众多slave节点分布式计算的结果要以Key、Value的形式保存,汇聚时才能根据key来统一集合到一起,然后计算value!只有这样才能最大程度地匹配不同的场景,让这个计算框架推广!
以大家都知道的word count为例,介绍一下
开放给开发人员的是map和reduce两个接口。Map里面开发人员可以自定义处理逻辑,然后以KV形式存放在硬盘,核心代码如下:
/** * Mapper中的map方法: * void map(K1 key, V1 value, Context context) * 映射一个单个的输入k/v对到一个中间的k/v对 * 输出对不需要和输入对是相同的类型,输入对可以映射到0个或多个输出对。 * Context:收集Mapper输出的<k,v>对。 * Context的write(k, v)方法:增加一个(k,v)对到context * 程序员主要编写Map和Reduce函数.这个Map函数使用StringTokenizer函数对字符串进行分隔,通过write方法把单词存入word中 * write方法存入(单词,1)这样的二元组到context中 */ publicvoid map(Object key, Text value, Context context) throws IOException, InterruptedException { StringTokenizer itr =new StringTokenizer(value.toString()); while (itr.hasMoreTokens()) { word.set(itr.nextToken()); context.write(word, one); }
最后一行代码context.write就是把KV的形式的数据写入磁盘,供后续的reduce处理!
reduce函数以KV形式把数据读出来,然后用户又可以按照自己的逻辑处理,核心代码如下:
/** * Reducer类中的reduce方法: * void reduce(Text key, Iterable<IntWritable> values, Context context) * 中k/v来自于map函数中的context,可能经过了进一步处理(combiner),同样通过context输出 */ publicvoid reduce(Text key, Iterable<IntWritable> values, Context context ) throws IOException, InterruptedException { int sum =0; for (IntWritable val : values) { sum += val.get(); } result.set(sum); context.write(key, result); } }
看到redcue的参数了么?典型的KV形式,只不过value可能不止一个,所以是Iterable的形式;merge sort是框架已经实现的功能,通过K-way merge sort,收集了每个key的所有value,供开发人员浅入自己特定的业务逻辑!有个这个框架,理论上讲,只要能拆分成key、value形式的计算逻辑,都能用MapReduce框架来做分布式计算!比如大家每天都要用的搜索引擎,后台是要建倒排索引的,用MapReduce建的流程如下:
实战中,可能会遇到这样一种场景: 有10个key,其中9个key的value只有几个,剩下一个key的value有million甚至更多(所谓的hotkey),reduce阶段9个key都处理完毕,还剩最后这个key在处理,直接拖慢了整个task的进程。甚至有些时候value数据过多,直接导致Iterable装不下而爆出OutOfMemory的错误,这可咋整?这里进一步仿照MapReduce的思路:单个key的value太多,那就继续拆分key?比如key拆分成key1、key2、key3等,分别在mapReduce处理,当然得到的结果也是分开的key1、key2、key3,最后再把这些keys合并起来即可!