Hadoop MapReduce

Hadoop MapReduce

mapreduce-process-overview.png

整个MR的过程可以分解为下面几步

  1. 读取数据
  2. Map
  3. reduce
  4. output

Hadoop 读取数据

通过InputFormat决定读取的数据的类型,然后拆分成一个个InputSplit,每个InputSplit对应一个Map处理,RecordReader读取InputSplit的内容给Map

通过InputFormat决定读取的数据的类型

mapreduce-inputformat.png
功能
  1. 验证作业输入的正确性,如格式等
  2. 将输入文件切割成逻辑分片(InputSplit),一个InputSplit将会被分配给一个独立的Map任务
  3. 提供RecordReader实现,读取InputSplit中的"K-V对"供Mapper使用
方法:

List getSplits(): 获取由输入文件计算出输入分片(InputSplit),解决数据或文件分割成片问题
RecordReader <k,v>createRecordReader():</k,v> 创建RecordReader,从InputSplit中读取数据,解决读取分片中数据问题
TextInputFormat: 输入文件中的每一行就是一个记录,Key是这一行的byte offset,而value是这一行的内容
KeyValueTextInputFormat: 输入文件中每一行就是一个记录,第一个分隔符字符切分每行。在分隔符字符之前的内容为Key,在之后的为Value。分隔符变量通过key.value.separator.in.input.line变量设置,默认为(\t)字符。
NLineInputFormat: 与TextInputFormat一样,但每个数据块必须保证有且只有N行,mapred.line.input.format.linespermap属性,默认为1
SequenceFileInputFormat: 一个用来读取字符流数据的InputFormat,<key,value>为用户自定义的。字符流数据是Hadoop自定义的压缩的二进制数据格式。它用来优化从一个MapReduce任务的输出到另一个MapReduce任务的输入之间的数据传输过程。</key,value>

InputSplit

代表一个个逻辑分片,并没有真正存储数据,只是提供了一个如何将数据分片的方法,Split内有Location信息,利于数据局部化,一个InputSplit给一个单独的Map处理

public abstract class InputSplit {
      /**
       * 获取Split的大小,支持根据size对InputSplit排序.
       */
      public abstract long getLength() throws IOException, InterruptedException;

      /**
       * 获取存储该分片的数据所在的节点位置.
       */
      public abstract String[] getLocations() throws IOException, InterruptedException;
}

RecordReader

将InputSplit拆分成一个个<key,value>对给Map处理,也是实际的文件读取分隔对象</key,value>

读取数据过程中的常见问题

  1. 大量小文件如何处理

    • Split的数目决定了Map的数目,大量的Mapper Task创建销毁开销将是巨大的
    • CombineFileInputFormat可以将若干个Split打包成一个,目的是避免过多的Map任务
  2. 怎么计算split的

    • 通常一个split就是一个block(FileInputFormat仅仅拆分比block大的文件),这样做的好处是使得Map可以在存储有当前数据的节点上运行本地的任务,而不需要通过网络进行跨节点的任务调度
    • 通过mapred.min.split.size, mapred.max.split.size, block.size来控制拆分的大小,如果mapred.min.split.size大于block size,则会将两个block合成到一个split,这样有部分block数据需要通过网络读取
    • 如果mapred.max.split.size小于block size,则会将一个block拆成多个split,增加了Map任务数(Map对split进行计算,且上报结果,关闭当前计算打开新的split均需要耗费资源)
    • 先获取文件在HDFS上的路径和Block信息,然后根据splitSize对文件进行切分( splitSize = computeSplitSize(blockSize, minSize, maxSize) ),默认splitSize 就等于blockSize的默认值(64m)
        public List<InputSplit> getSplits(JobContext job) throws IOException {
        // 首先计算分片的最大和最小值。这两个值将会用来计算分片的大小
        long minSize = Math.max(getFormatMinSplitSize(), getMinSplitSize(job));
        long maxSize = getMaxSplitSize(job);
    
        // generate splits
        List<InputSplit> splits = new ArrayList<InputSplit>();
        List<FileStatus> files = listStatus(job);
        for (FileStatus file: files) {
            Path path = file.getPath();
            long length = file.getLen();
            if (length != 0) {
                FileSystem fs = path.getFileSystem(job.getConfiguration());
                // 获取该文件所有的block信息列表[hostname, offset, length]
                BlockLocation[] blkLocations = fs.getFileBlockLocations(file, 0, length);
                // 判断文件是否可分割,通常是可分割的,但如果文件是压缩的,将不可分割
                if (isSplitable(job, path)) {
                    long blockSize = file.getBlockSize();
                    // 计算分片大小
                    // 即 Math.max(minSize, Math.min(maxSize, blockSize));
                    long splitSize = computeSplitSize(blockSize, minSize, maxSize);
    
                    long bytesRemaining = length;
                    // 循环分片。
                    // 当剩余数据与分片大小比值大于Split_Slop时,继续分片, 小于等于时,停止分片
                    while (((double) bytesRemaining)/splitSize > SPLIT_SLOP) {
                        int blkIndex = getBlockIndex(blkLocations, length-bytesRemaining);
                        splits.add(makeSplit(path, length-bytesRemaining, splitSize, blkLocations[blkIndex].getHosts()));
                        bytesRemaining -= splitSize;
                    }
                    // 处理余下的数据
                    if (bytesRemaining != 0) {
                        splits.add(makeSplit(path, length-bytesRemaining, bytesRemaining, blkLocations[blkLocations.length-1].getHosts()));
                    }
                } else {
                    // 不可split,整块返回
                    splits.add(makeSplit(path, 0, length, blkLocations[0].getHosts()));
                }
            } else {
                // 对于长度为0的文件,创建空Hosts列表,返回
                splits.add(makeSplit(path, 0, length, new String[0]));
            }
        }
    
        // 设置输入文件数量
        job.getConfiguration().setLong(NUM_INPUT_FILES, files.size());
        LOG.debug("Total # of splits: " + splits.size());
        return splits;
    }
    
    • 分片间的数据如何处理:split是根据文件大小分割的,而一般处理是根据分隔符进行分割的,这样势必存在一条记录横跨两个split,解决办法是只要不是第一个split,都会远程读取一条记录。不是第一个split的都忽略到第一条记录。
      [图片上传失败...(image-2e0ac6-1550993200611)]
        public class LineRecordReader extends RecordReader<LongWritable, Text> {
        private CompressionCodecFactory compressionCodecs = null;
        private long start;
        private long pos;
        private long end;
        private LineReader in;
        private int maxLineLength;
        private LongWritable key = null;
        private Text value = null;
    
        // initialize函数即对LineRecordReader的一个初始化
        // 主要是计算分片的始末位置,打开输入流以供读取K-V对,处理分片经过压缩的情况等
        public void initialize(InputSplit genericSplit, TaskAttemptContext context) throws IOException {
            FileSplit split = (FileSplit) genericSplit;
            Configuration job = context.getConfiguration();
            this.maxLineLength = job.getInt("mapred.linerecordreader.maxlength", Integer.MAX_VALUE);
            start = split.getStart();
            end = start + split.getLength();
            final Path file = split.getPath();
            compressionCodecs = new CompressionCodecFactory(job);
            final CompressionCodec codec = compressionCodecs.getCodec(file);
    
            // 打开文件,并定位到分片读取的起始位置
            FileSystem fs = file.getFileSystem(job);
            FSDataInputStream fileIn = fs.open(split.getPath());
    
            boolean skipFirstLine = false;
            if (codec != null) {
                // 文件是压缩文件的话,直接打开文件
                in = new LineReader(codec.createInputStream(fileIn), job);
                end = Long.MAX_VALUE;
            } else {
                // 只要不是第一个split,则忽略本split的第一行数据
                if (start != 0) {
                    skipFirstLine = true;
                    --start;
                    // 定位到偏移位置,下&#x#x6B21;读取就会从偏移位置开始
                    fileIn.seek(start);
                }
                in = new LineReader(fileIn, job);
            }
    
            if (skipFirstLine) {
                // 忽略第一行数据,重新定位start
                start += in.readLine(new Text(), 0, (int) Math.min((long) Integer.MAX_VALUE, end - start));
            }
            this.pos = start;
        }
    
        public boolean nextKeyValue() throws IOException {
            if (key == null) {
                key = new LongWritable();
            }
            key.set(pos);// key即为偏移量
            if (value == null) {
                value = new Text();
            }
            int newSize = 0;
            while (pos < end) {
                newSize = in.readLine(value, maxLineLength,    Math.max((int) Math.min(Integer.MAX_VALUE, end - pos), maxLineLength));
                // 读取的数据长度为0,则说明已读完
                if (newSize == 0) {
                    break;
                }
                pos += newSize;
                // 读取的数据长度小于最大行长度,也说明已读取完毕
                if (newSize < maxLineLength) {
                    break;
                }
                // 执行到此处,说明该行数据没读完,继续读入
            }
            if (newSize == 0) {
                key = null;
                value = null;
                return false;
            } else {
                return true;
            }
        }
    }
    

Map & Reduce

mapreduce-shuffle.png

Shuffle的中文意思是“洗牌”,如果我们这样看:一个map产生的数据,结果通过hash过程分区缺分配给了不同的reduce任务,是不是一个对数据洗牌的过程呢?

Map端流程分析

  1. 每个输入分片会让一个map任务来处理,默认情况下,以HDFS的一个块的大小(默认64M)为一个分片,当然我们也可以设置块的大小。map输出的结果会暂且放在一个环形内存缓冲区中(该缓冲区的大小默认为100M,由io.sort.mb属性控制),当该缓冲区快要溢出时(默认为缓冲区大小的80%,由io.sort.spill.percent属性控制),会在本地文件系统中创建一个溢出文件,将该缓冲区中的数据写入这个文件。
  2. 在写入磁盘之前,线程首先根据reduce任务的数目将数据划分为相同数目的分区,也就是一个reduce任务对应一个分区的数据。这样做是为了避免有些reduce任务分配到大量数据,而有些reduce任务却分到很少数据,甚至没有分到数据的尴尬局面。其实分区就是对数据进行hash的过程。然后对每个分区中的数据进行排序,如果此时设置了Combiner,将排序后的结果进行Combianer操作,这样做的目的是让尽可能少的数据写入到磁盘。
  3. 当map任务输出最后一个记录时,可能会有很多的溢出文件,这时需要将这些文件合并。合并的过程中会不断地进行排序和combiner操作,目的有两个:1、尽量减少每次写入磁盘的数据量;2、尽量减少下一复制阶段网络传输的数据量。最后合并成了一个已分区且已排序的文件。为了减少网络传输的数据量,这里可以将数据压缩,只要将mapred.compress.map.out设置为true就可以。数据压缩:Gzip、Lzo、snappy。
  4. 将分区中的数据拷贝给相对应的reduce任务。有人可能会问:分区中的数据怎么知道它对应的reduce是哪个呢?其实map任务一直和其父TaskTracker保持联系,而TaskTracker又一直和obTracker保持心跳。所以JobTracker中保存了整个集群中的宏观信息。只要reduce任务向JobTracker获取对应的map输出位置就OK了。

Map端shuffle的过程:

mapreduce-map-shuffle.png
  1. 每个map task都有一个内存缓冲区,存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后在对磁盘中这个map task产生的所有临时文件做一个合并,生成最终的正式输出文件,然后等待reduce task来拉数据

  2. 在map task执行时,它的输入数据来源于HDFS的block,当然在MapReduce概念中,map task只读取split。split与block对应关系可能是多对一,默认是一对一。主要根据split bloc的配置 ,在wordcount例子里,假设map的输入数据都是是像“aaa”这样的字符串。

  3. 在经过mapper的运行后,我们得知mapper的输出是这样一个key/value对:key是“aaa”,value是数值1。因为当前map端只做加1的操作,在reduce task里采取合并结果集。前面我们知道这个job有3个reduce task。那到底当前的“aaa”究竟该丢给哪个reduce去处理呢?是需要现在做决定的。

  4. MapReduce提供Partitioner接口,作用就是根据key或value及reduce的数量来决定当前的输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数据取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以定制并设置到job上。

  5. 在例子中,“aaa”经过Partition后返回0,也就是这对值应当交由第一个reduce来处理。接下来,需要将数据写入内存缓冲区中,缓冲区的作用是批量收集map结果,减少磁盘IO的影响。我们的key/value对以及Partition的结果都会被写入缓冲区。当然,写入之前,key与value值都会被序列化成字节数组。

  6. 内存缓冲区是有大小限制的,默认是100MB。当map task的输出结果很多时,就可能会撑爆内存,所以需要在一定条件下将缓冲区中的数据临时写入磁盘,然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为spill,中文可理解为溢写。溢写是由单独线程来完成,不影响往缓冲区写map结果的线程。溢写线程启动时不应该阻止map的结果输出,所以整个缓冲区有个溢写的比例spill.percent。比例默认是0.8,也就是当缓冲区的数据值已经达到阈值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执行溢写过程。map task的输出结果还可以往剩下的20MB内存中写,互不影响。

  7. 当溢写线程启动后,需要对这80MB空间内的key做排序(sort)。排序是MapReduce模型默认的行为,这里的排序也是对序列化的字节做的排序。

  8. combine

    • 因为map task的输出是需要发送到不同的reduce端去,而内存缓冲区没有对将发送到相同reduce端的数据做合并,那么这种合并应该是体现在磁盘文件中的。从官方图上也可以看到写到磁盘中的一些文件是对不同的reduce端的数值做过合并。所以溢写过程一个很重要的细节在于,如果有很多个key/value对需要发送到某个reduce端去,那么需要将这些key/value值拼接到一块,减少与partition相关的索引记录。
    • 在针对每个reduce端而合并数据时,有些数据可能像这样:“aaa”/1,“aaa”/1。对于wordcount例子,只是简单地统计单词出现的次数,如果在同一个map task的结果中有很多像“aaa”一样出现多次的key,我们就应该把它们的值合并到一块,这个过程叫reduce也叫combine。但MapReduce的术语中,reduce只值reduce端执行从多个map task取数据做计算的过程。除reduce外,非正式地合并数据只能算作combine了。其实大家知道的,MapReduce中将Combiner等同于Reducer。
    • 如果client设置过Combiner,那么现在就是使用Combiner的时候了。将有相同key的key/value对的value加起来,减少溢写到磁盘的数据量。Combiner会优化MapReduce的中间结果,所以它在整个模型中会多次使用。那哪些场景才能使用Combiner呢?从这里分析,Combiner的输出是Reducer的输入,Combiner绝不能改变最终的计算结果。所以从我的想法来看,Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner的使用一定得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果。
  9. Group

    • 每次溢写会在磁盘上生成一个溢写文件,如果map的输出结果真的很大,有多次这样的溢写发生,磁盘上相应的就会有多个溢写文件存在。当map task真正完成时,内存缓冲区中的数据也全部溢写到磁盘中形成一个溢写文件。最终磁盘中会至少有一个这样的溢写文件存在(如果map的输出结果很少,当map执行完成时,只会产生一个溢写文件),因为最终的文件只有一个,所以需要将这些溢写文件归并到一起,这个过程就叫Merge。Merge是怎样的?如前面的例子,“aaa”从某个map task读取过来时值是5,从另外一个map读取时值是8,因为他们有相同的key,所以要merge成group。
    • 什么是group:对于“aaa”就是像这样:{“aaa”,[5,8,2,...]},数组中的值就是从不同的溢写文件中读取出来的,然后再把这些值加起来。请注意,因为merge是将多个溢写文件合并到一个文件,所以可能也有相同的key存在,在这个过程中,如果client设置过Combiner,也会使用Combiner来合并相同的key
  10. map端的所有工作都已经结束,最终生成的这个文件也存放在TaskTracker够得到的某个本地目录中。每个reduce task不断地通过RPC从JobTRacker那获取map task是否完成的信息,如果reduce task得到通知,获知某台TaskTracker上的map task执行完成,Shuffle的后半段过程开始启动。

Reduce端流程分析

  1. reduce会接收到不同map任务传来的数据,并且每个map传来的数据都是有序的。如果reduce端接收的数据量相当小,则直接存储在内存中(缓冲区大小由mapred.job.shuffle.input.buffer.percent属性控制,表示用作此用途的堆空间百分比),如果数据量超过了该缓冲区大小的一定比例(由mapred.job.shuffle.merg.percent决定),则对数据合并后溢写到磁盘中。

  2. 随着溢写文件的增多,后台线程会将它们合并成一个更大的有序的文件,这样做是为了给后面的合并节省空间。其实不管在map端还是在reduce端,MapReduce都是反复地执行排序,合并操作,现在终于明白了有些人为什么会说:排序是hadoop的灵魂。

  3. 合并的过程中会产生许多的中间文件(写入磁盘了),但MapReduce会让写入磁盘的数据尽可能地少,并且最后一次合并的结果并没有写入磁盘,而是直接输入到reduce函数。

  4. Reducer的输入文件。不断地merge后,最后会生成一个“最终文件”。为什么加引号?因为这个文件可能存在于磁盘上,也可能存在于内存中。对我们来说,希望它存放于内存中,直接作为Reducer的输入,但默认情况下,这个文件是存放于磁盘中的。当Reducer的输入文件已定,整个Shuffle才最终结束。然后就是Reducer执行,把结果放到HDSF上
    对MapReduce的调优在很大程度上就是对MapReduce Shuffle的性能的调优。

Reduce端的shuffle过程:

mapreduce-reduce-shuffle.png
  1. copy过程,简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过http方式请求map task所在的TaskTracker获取map task的输出文件。因为map task早已结束,这些文件就归TaskTracker管理在本地磁盘中。
  2. Merge阶段。这里的merge和map端的merge动作相同,只是数组中存放的是不同map端copy来的数值。copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端更为灵活,它基于JVM的heap size设置,因为Shuffle阶段Reducer不运行,所以应该把绝大部分的内存都给Shuffle使用
  3. Merge有三种形式:1、内存到内存;2、内存到磁盘;3、磁盘到磁盘。默认情况下第一种形式不启用,让人比较困惑。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map端类似,这也是溢写的过程,在这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的那个文件

内存缓冲区 MapOutputBuffer

mapreduce-mapoutputbuffer.png
  1. kvoffsets缓冲区:也叫偏移量索引数组,用于保存key/value信息在位置索引kvindices中的偏移量。当kvoffsets的使用率超过io.sort.spill.percent(默认为80%)后,便会触发一次SpillThread线程的“溢写”操作,也就是开始一次spill阶段的操作。
  2. kvindices缓冲区:也叫位置索引数组,用于保存key/value在数据缓冲区kvbuffer中的起始位置。
  3. kvbuffer数据缓冲区:用于保存实际的key/value的值。默认情况下该缓冲区最多可以使用io.sort.mb的95%,当kvbuffer使用率超过io.sort.spill.percent(默认80%)后,便会触发一次SpillThread线程的“溢写”操作,也就是开始一次spill阶段的操作。

总结

总的来说整个Map reduce的过程可以切分为下面几部分

  1. recordread,
    1. splitjob的处理,split的数量决定了map的数据,可以通过设置min和max的split的size来控制
  2. Map
    1. 数据进入环形内存缓冲区,如果大于配置的百分比(默认80%),数据会spill到磁盘文件,输出结束的时候会开始shuffle工作,将多个文件数据排序,分区,合并,然后根据reduce的数量分区到产生不同的文件,默认对key hash后再以reduce task数据取模。默认的取模方式只是为了平均reduce的处理能力
    2. 对输出的文件进行合并的时候会做group操作,即合并相同的key的值。
    3. 如果设置了combiner的,数据会根据key做合并操作。类似reduce动作,但是注意,combiner可以提高效率,但是不是所有的都适合使用combine的,用法不对的时候可能导致最终结果出错。
  3. reduce
    1. 数据从不同的datanode过来也是先要做shuffle操作,原理和map的shuffle一样,同样要考虑spill的文件,合并,group,combine。
    2. 执行reduce操作
    3. 输出结果
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,408评论 4 371
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,690评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,036评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,726评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,123评论 3 296
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,037评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,178评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,964评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,703评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,863评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,333评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,658评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,374评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,195评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,988评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,167评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,970评论 2 279

推荐阅读更多精彩内容

  • 1、导读 正如谷歌的MapReduce论文所说,MapReduce是一个编程模型,也是一个处理和生成超大数据集的算...
    远o_O阅读 720评论 0 3
  • 一个Map/Reduce 作业(job) 通常会把输入的数据(input file)切分为若干独立的数据块(spl...
    Alukar阅读 6,507评论 0 14
  • 前言 本文是个人之前纪录的MapReduce学习笔记,主要涉及到MapReduce基本概念、Hadoop 经典示例...
    LY丶Smile阅读 1,088评论 0 3
  • 转载自:https://my.oschina.net/u/2293326/blog/607540 欢迎阅读原创。 ...
    陌城小川阅读 3,612评论 0 4
  • 有个女生说,“我读初中时候很喜欢一个男生, 有一天,他不知道为什么就被打了, 一个人坐在操场上很可怜的样子。 我就...
    絔箜阅读 309评论 0 1