【MapReduce】Mapreduce基础知识整理 (三) shuffle机制、MapJoin、ReduceJoin、倒排序索引

AI权益加码!Claude Code、Cursor等20+工具免费用! 购周边限时加赠Coding Plan Lite,畅享主流AI工具!学习进阶更高效! 阅读详情

【MapReduce】Mapreduce基础知识整理(一)

1. Mapreduce的Shuffle机制

1.1概述

一个mapreduce过程:
map——>shuffle(排序、分组、分区、combiner)——>reduce

  1. MapReduce 中,mapper 阶段处理的数据如何传递给 reducer 阶段,是 MapReduce 框架中 最关键的一个流程,这个流程就叫 Shuffle
  2. Shuffle: 数据混洗 ——(核心机制:数据分区,排序,局部聚合,缓存,拉取,再合并 排序)
  3. 具体来说:就是将 MapTask 输出的处理结果数据,按照 Partitioner 组件制定的规则分发 给 ReduceTask,并在分发的过程中,对数据按 key 进行了分区和排序

shuffle过程三大组件及一优化组件的使用案例请点击

1.2 shuffle分析

1.2.1 主要工作流程

在这里插入图片描述
在这里插入图片描述
Shuffle 是 MapReduce 处理流程中的一个核心过程,它的每一个处理步骤是分散在各个 maptask 和 reducetask 节点上完成的,整体来看,分为核心 3 个操作:
1、分区 partition(如果 reduceTask 只有一个或者没有,那么 partition 将不起作用。设置没 设置都相当于没有)
2、Sort 根据 key 排序(MapReduce 编程中的 sort 是一定会做的,并且只能按照 key 排序, 当然如果没有 reducer 阶段,那么就不会对 key 排序)
3、Combiner 进行局部 value 的合并(Combiner 是可选的组件,作用只是为了提高任务的执 行效率)

说细流程:

  1. mapTask 收集我们的 map()方法输出的 kv 对,放到内存缓冲区 kvbuffer(环形缓冲区:内 存中的一种首尾相连的数据结构,kvbuffer 包含数据区和索引区)中
  2. 从内存缓冲区中的数据区的数据不断溢出本地磁盘文件 file.out,可能会溢出多次,则会 有多个文件,相应的内存缓冲区中的索引区数据溢出为磁盘索引文件 file.out.index
  3. 多个溢出文件会被合并成大的溢出文件
  4. 在溢出过程中,及合并的过程中,都要调用 partitoner 进行分区和针对 key 进行排序
  5. 在数据量大的时候,可以对 mapTask 结果启用压缩,将 mapreduce.map.output.compress 设为 true,并使用 mapreduce.map.output.compress.codec 设置使用的压缩算法,可以提高数 据传输到 reducer 端的效率
  6. reduceTask 根据自己的分区号,去各个 mapTask 机器上取相应的结果分区数据
  7. reduceTask 会取到同一个分区的来自不同 mapTask 的结果文件,reduceTask 会将这些文 件再进行合并(归并排序)
  8. 合并成大文件后,shuffle 的过程也就结束了,后面进入 reduceTask 的逻辑运算过程(从 文件中取出一个一个的键值对 group,调用用户自定义的 reduce()方法)

Shuffle 中的缓冲区大小会影响到 mapreduce 程序的执行效率,原则上说,缓冲区越大,磁 盘 io 的次数越少,执行速度就越快 缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb 默认 100M 缓冲区的溢写比也可以通过参数调整,参数:mapreduce.map.sort.spill.percent 默认 0.8

1.2.2 环形缓冲区

先看一下源码:
org.apache.hadoop.mapred.MapTask#MapOutputBuffer

public static class MapOutputBuffer<K extends Object, V extends Object>
      implements MapOutputCollector<K, V>, IndexedSortable {
   
   
    private int partitions;
    private JobConf job;
    private TaskReporter reporter;
    private Class<K> keyClass;
    private Class<V> valClass;
    private RawComparator<K> comparator;
    private SerializationFactory serializationFactory;
    private Serializer<K> keySerializer;
    private Serializer<V> valSerializer;
    private CombinerRunner<K,V> combinerRunner;
    private CombineOutputCollector<K, V> combineCollector;

    // Compression for map-outputs
    private CompressionCodec codec;

    // k/v accounting
    private IntBuffer kvmeta; // metadata overlay on backing store
    int kvstart;            // marks origin of spill metadata
    int kvend;              // marks end of spill metadata
    int kvindex;            // marks end of fully serialized records

    int equator;            // marks origin of meta/serialization
    int bufstart;           // marks beginning of spill
    int bufend;             // marks beginning of collectable
    int bufmark;            // marks end of record
    int bufindex;           // marks end of collected
    int bufvoid;            // marks the point where we should stop
                            // reading at the end of the buffer

    byte[] kvbuffer;        // main output buffer
    private final byte[] b0 = new byte[0];

    private static final int VALSTART = 0;         // val offset in acct
    private static final int KEYSTART = 1;         // key offset in acct
    private static final int PARTITION = 2;        // partition offset in acct
    private static final int VALLEN = 3;           // length of value
    private static final int NMETA = 4;            // num meta ints
    private static final int METASIZE = NMETA * 4; // size in bytes

    // spill accounting
    private int maxRec;
    private int softLimit;
    boolean spillInProgress;;
    int bufferRemaining;
    volatile Throwable sortSpillException = null;

    int numSpills = 0;
    private int minSpillsForCombine;
    private IndexedSorter sorter;
    final ReentrantLock spillLock = new ReentrantLock();
    final Condition spillDone = spillLock.newCondition();
    final Condition spillReady = spillLock.newCondition();
    final BlockingBuffer bb = new BlockingBuffer();
    volatile boolean spillThreadRunning = false;
    final SpillThread spillThread = new SpillThread();

    private FileSystem rfs;

    // Counters
    private Counters.Counter mapOutputByteCounter;
    private Counters.Counter mapOutputRecordCounter;
    private Counters.Counter fileOutputByteCounter;

    final ArrayList<SpillRecord> indexCacheList =
      new ArrayList<SpillRecord>();
    private int totalIndexCacheMemory;
    private int indexCacheMemoryLimit;
    private static final int INDEX_CACHE_MEMORY_LIMIT_DEFAULT = 1024 * 1024;

    private MapTask mapTask;
    private MapOutputFile mapOutputFile;
    private Progress sortPhase;
    private Counters.Counter spilledRecordsCounter;

    public MapOutputBuffer() {
   
   
    }

    @SuppressWarnings("unchecked")
    public void init(MapOutputCollector.Context context
                    ) throws IOException, ClassNotFoundException {
   
   
      job = context.getJobConf();
      reporter = context.getReporter();
      mapTask = context.getMapTask();
      mapOutputFile = mapTask.getMapOutputFile();
      sortPhase = mapTask.getSortPhase();
      spilledRecordsCounter = reporter.getCounter(TaskCounter.SPILLED_RECORDS);
      partitions = job.getNumReduceTasks();
      rfs = ((LocalFileSystem)FileSystem.getLocal(job)).getRaw();

      //sanity checks
      final float spillper =
        job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);
      final int sortmb = job.getInt(JobContext.IO_SORT_MB, 100);
      indexCacheMemoryLimit = job.getInt(JobContext.INDEX_CACHE_MEMORY_LIMIT,
                                         INDEX_CACHE_MEMORY_LIMIT_DEFAULT);
      if (spillper > (float)1.0 || spillper <= (float)0.0) {
   
   
        throw new IOException("Invalid \"" + JobContext.MAP_SORT_SPILL_PERCENT +
            "\": " + spillper);
      }
      if ((sortmb & 0x7FF) != sortmb) {
   
   
        throw new IOException(
            "Invalid \"" + JobContext.IO_SORT_MB + "\": " + sortmb);
      }
      sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class",
            QuickSort.class, IndexedSorter.class), job);
      // buffers and accounting
      int maxMemUsage = sortmb << 20;
      maxMemUsage -= maxMemUsage % METASIZE;
      kvbuffer = new byte[maxMemUsage];
      bufvoid = kvbuffer.length;
      kvmeta = ByteBuffer.wrap(kvbuffer)
         .order(ByteOrder.nativeOrder())
         .asIntBuffer();
      setEquator(0);
      bufstart = bufend = bufindex = equator;
      kvstart = kvend = kvindex;

      maxRec = kvmeta.capacity() / NMETA;
      softLimit = (int)(kvbuffer.length * spillper);
      bufferRemaining = softLimit;
      LOG.info(JobContext.IO_SORT_MB + ": " + sortmb);
      LOG.info("soft limit at " + softLimit);
      LOG.info("bufstart = " + bufstart + "; bufvoid = " + bufvoid);
      LOG.info("kvstart = " + kvstart + "; length = " + maxRec);

      // k/v serialization
      comparator = job.getOutputKeyComparator();
      keyClass = (Class<K>)job.getMapOutputKeyClass();
      valClass = (Class<V>)job.getMapOutputValueClass();
      serializationFactory = new SerializationFactory(job);
      keySerializer = serializationFactory.getSerializer(keyClass);
      keySerializer.open(bb);
      valSerializer = serializationFactory.getSerializer(valClass);
      valSerializer.open(bb);

      // output counters
      mapOutputByteCounter = reporter.getCounter(TaskCounter.MAP_OUTPUT_BYTES);
      mapOutputRecordCounter =
        reporter.getCounter(TaskCounter.MAP_OUTPUT_RECORDS);
      fileOutputByteCounter = reporter
          .getCounter(TaskCounter.MAP_OUTPUT_MATERIALIZED_BYTES);

      // compression
      if (job.getCompressMapOutput()) {
   
   
        Class<? extends CompressionCodec> codecClass =
          job.getMapOutputCompressorClass(DefaultCodec.class);
        codec = ReflectionUtils.newInstance(codecClass, job);
      } else {
   
   
        codec = null;
      }

      // combiner
      final Counters.Counter combineInputCounter =
        reporter.getCounter(TaskCounter.COMBINE_INPUT_RECORDS);
      combinerRunner = CombinerRunner.create(job, getTaskID(), 
                                             combineInputCounter,
                                             reporter, null);
      if (combinerRunner != null) {
   
   
        final Counters.Counter combineOutputCounter =
          reporter.getCounter(TaskCounter.COMBINE_OUTPUT_RECORDS);
        combineCollector= new CombineOutputCollector<K,V>(combineOutputCounter, reporter, job);
      } else {
   
   
        combineCollector = null;
      }
      spillInProgress = false;
      minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3);
      spillThread.setDaemon(true);
      spillThread.setName("SpillThread");
      spillLock.lock();
      try {
   
   
        spillThread.start();
        while (!spillThreadRunning) {
   
   
          spillDone.await();
        }
      } catch (InterruptedException e) {
   
   
        throw new IOException("Spill thread failed to initialize", e);
      } finally {
   
   
        spillLock.unlock();
      }
      if (sortSpillException != null) {
   
   
        throw new IOException("Spill thread failed to initialize",
            sortSpillException);
      }
    }

    /**
     * Serialize the key, value to intermediate storage.
     * When this method returns, kvindex must refer to sufficient unused
     * storage to store one METADATA.
     */
    public synchronized void collect(K key, V value, final int partition
                                     ) throws IOException {
   
   
      reporter.progress();
      if (key.getClass() != keyClass) {
   
   
        throw new IOException("Type mismatch in key from map: expected "
                              + keyClass.getName() + ", received "
                              + key.getClass().getName());
      }
      if (value.getClass() != valClass) {
   
   
        throw new IOException("Type mismatch in value from map: expected "
                              + valClass.getName() + ", received "
                              + value.getClass().getName());
      }
      if (partition < 0 || partition >= partitions) {
   
   
        throw new IOException("Illegal partition for " + key + " (" +
            partition + ")");
      }
      checkSpillException();
      bufferRemaining -= METASIZE;
      if (bufferRemaining <= 0) {
   
   
        // start spill if the thread is not running and the soft limit has been
        // reached
        spillLock.lock();
        try {
   
   
          do {
   
   
            if (!spillInProgress) {
   
   
              final int kvbidx = 4 * kvindex;
              final int kvbend = 4 * kvend;
              // serialized, unspilled bytes always lie between kvindex and
              // bufindex, crossing the equator. Note that any void space
              // created by a reset must be included in "used" bytes
              final int bUsed = distanceTo(kvbidx, bufindex);
              final boolean bufsoftlimit = bUsed >= softLimit;
              if ((kvbend + METASIZE) % kvbuffer.length !=
                  equator - (equator % METASIZE)) {
   
   
                // spill finished, reclaim space
                resetSpill();
                bufferRemaining = Math.min(
                    distanceTo(bufindex, kvbidx) - 2 * METASIZE,
                    softLimit - bUsed) - METASIZE;
                continue;
              } else if (bufsoftlimit && kvindex != kvend) {
   
   
                // spill records, if any collected; check latter, as it may
                // be possible for metadata alignment to hit spill pcnt
                startSpill();
                final int avgRec = (int)
                  (mapOutputByteCounter.getCounter() /
                  mapOutputRecordCounter.getCounter());
                // leave at least half the split buffer for serialization data
                // ensure that kvindex >= bufindex
                final int distkvi = distanceTo(bufindex, kvbidx);
                final int newPos = (bufindex +
                  Math.max(2 * METASIZE - 1,
                          Math.min(distkvi / 2,
                                   distkvi / (METASIZE + avgRec) * METASIZE)))
                  % kvbuffer.length;
                setEquator(newPos);
                bufmark = bufindex = newPos;
                final int serBound = 4 * kvend;
                // bytes remaining before the lock must be held and limits
                // checked is the minimum of three arcs: the metadata space, the
                // serialization space, and the soft limit
                bufferRemaining = Math.min(
                    // metadata max
                    distanceTo(bufend, newPos),
                    Math.min(
                      // serialization max
                      distanceTo(newPos, serBound),
                      // soft limit
                      softLimit)) - 2 * METASIZE;
              }
            }
          } while (false);
        } finally {
   
   
          spillLock.unlock();
        }
      }

      
MapReduceMapJoinReduceJoin 先看元数据格式,有两张表商品表和订单表p开头的列代表商品ID,我们要通过商品ID实现Join操作,在MapReduce有两种方式,MapJoinReduceJoin: product.txt p0001,小米5,1000,2000 p0002,锤子T1,1000,3000 order.txt 1001,20150710,p0001,2 1002,20150710,p0002,3 1003,20150710,p0001,3 ReduceJoin 先来看看ReduceJoin操作,数据的大致过程如下图: 阅读详情

相关推荐

使用Broadcast变量与map类算子实现join操作,进而完全规避掉shuffle类的操作

概述 有的时候,我们可能会遇到大数据计算中一个最棘手的问题——数据倾斜,此时Spark作业的性能会比期望差很多。数据倾斜调优,就是使用各种技术方案解决不同类型的数据倾斜问题,以保证Spark作业的性能。该篇博客参考美团的spark高级版,使用scala代码进行简单的模拟。 方案适用场景 在对RDD使用join类操作,或者是在Spark SQL中使用join语句时,而且join操作中的一个...

4171

hadoopshuffle 过程

hadoopshuffle 过程 huffle过程是MapReduce的核心,也被称为奇迹发生的地方。要想理解MapReduceShuffle是必须要了解的。我看过很多相关的资料,但每次看完都云里雾里的绕着,很难理清大致的逻辑,反而越搅越混。前段时间在做MapReduce job 性能调优的工作,需要深入代码研究MapReduce的运行机制,这才对Shuffle探了个究竟。考虑到之前我在看相关资料而看不懂时很恼火,所以在这里我尽最大的可能试着把Shuffle说清楚,让每一位想了解它原理的朋友都能有

qq_40383147的博客 700

hive的join操作

** hivejoin种形式 ** shuffle join:是hive中的普通的join方式,基于map/reduce实现,join的key通过shuffle汇集到相应的reduce里做join。这种join方式不考虑数据量和数据模型设计,比较耗费资源,是较慢的join策略。 map joinjoin时,将小表load到每个节点的内存中,和大表在该节点上的数据进行join,在map端完成...

qq_43227570的博客 597

【SQL优化】Hive的种连接方式-ReduceJoinMapJoin、Sort Merge Bucket Join

在进行表连接上根据情景的不同选择不同的连接方式也是一种sql的优化。

qq_68076599的博客 968

spark特殊join算子,join时何时产生shuffle,何时不产生shuffle

1、 什么是宽窄依赖, 宽依赖: 发生shuffle时,一定会产生宽依赖,宽依赖是一个RDD中的一个Partition被多个子Partition所依赖(一个父亲多有儿子),也就是说每一个父RDD的Partition中的数据,都可能传输一部分到下一个RDD的多个partition中,此时一定会发生shuffle 窄依赖: 一个RDD中的一个 Partition最多 被一个 子 Partition...

muyingmiao的专栏 3612

MapReduce join

1. Reduce端的join ReduceJoin/ShuffleJoin/CommonJoin 真正的join都是的在Reduce端完成的 必然是会产生shuffle的 以join条件作为map数据的key,不同来源的数据需要打上一个标签 经过shuffle,相同key的数据会落到一个reduce里面 我们的join操作就是reduce端来完成 这会倒数数据倾斜,因为 =>相同key的数据会落到一个reduce里面 2. Map端的join MapJoinjoin真正是在map端完成的,也就是说

weixin_43866666的博客 212

map的环形内存缓冲区

hadoop在执行MapReduce任务时,在map阶段,map函数产生的输出,并不是直接写入磁盘的。为了提高效率,它将输出结果先写入到内存中(即环形内存缓冲区,默认大小100M),再从缓冲区(溢)写入磁盘。 下面我们就来看看这段代码。 [size=medium][b]1、找到环形内存缓冲区[/b][/size] 在运行job时,有条输出: 09/04/07 12:34:35 ...

Thinking in Hadoop 998

MapReduce原理讲解(带源码)

MapReduce原理讲解(带原码)

m0_72766690的博客 887

Hadoop-MapReduce排序(超级详细)

如果使用某一个字段进行辅助排序,那么这个字段"必须"在之前"有过排序"的处理,所有"辅助"顾名思义就是在前者排序好的基础上发挥的作用, 单独使用的辅助排序 很可能生成的结果顺序是乱的,最好不要使用。使用对象的某字段值分组,对象间的某字段的值相同,则这些对象就会组成一个"变化key,就是一个key",而value会聚集成迭代器,而这个变化key是根据遍历迭代器产生value"对应的对象做为key"。阶段的排序,负责接接收shuffle处理好的数据,直接循环迭代( key,valus{..} )即可。

互联网知识分享 3581

MapReduce的4个阶段

1、split阶段: 此阶段,每个输入文件被分片输入到map。如一个文件有200M,默认会被分成2片,因为每片的默认最大值和每块的默认值128M相同。 如果输入为大量的小文件,则会造成过多的map数,导致效率下降,可采用压缩输入格式CombineFileInputFormat。 2、map阶段: 此阶段,执行map任务。map数由分片决...

weixin_33795093的博客 5214

MapReduce工作流程

MapReduce MapReduce主要思想:分而治之 map阶段主要负责"分",将一个file文件分成若干个小文件 reduce阶段负责"合",将map阶段分开的小文件合成一个reduce输出. MapReduce主要分为四个阶段Split、MapShuffle、Reduce 这四个阶段. 其中1.Split(分片输出) 2.map阶段 Split 阶段的输出作为 Map 阶段的输入,sp...

UUSUU的博客 1094

1.MapReduce Shuffle阶段的理解

Shuffle的中文意思是洗牌,是mapReduce程序最重要的一个过程,可以将mapTask阶段输出的结果分发给不同的reduceTask,在分发过程中还会对key进行分区,排序,以及局部聚合的操作。 Shuffle是MR处理过程中的一个流程,其处理的步骤是分散在mapTask和reduceTask各个节点上完成的。 ...

qq_44350553的博客 444

java mapreduce实例_Mapreduce实例——排序

原理Map、Reduce任务中Shuffle和排序的过程图如下:流程分析:1.Map端:(1)每个输入分片会让一个map任务来处理,默认情况下,以HDFS的一个块的大小(默认为64M)为一个分片,当然我们也可以设置块的大小。map输出的结果会暂且放在一个环形内存缓冲区中(该缓冲区的大小默认为100M,由io.sort.mb属性控制),当该缓冲区快要溢出时(默认为缓冲区大小的80%,由io.sort...

weixin_42465953的博客 1033

MapReduce排序

MapReduce排序、全排序、二次排序、区内排序

功不唐捐,玉汝于成 1856

一个简单的MapReduce多文件数据排序程序

通过本文的步骤,读者可以掌握在Hadoop平台上使用MapReduce框架进行大规模数据处理的基本方法,并能灵活应用于类似的任务中。

weixin_63290813的博客 1487

大数据学习之十——倒排索引

倒排索引 1.了解概念 "倒排索引"是文档检索系统中最常用的数据结构,被广泛地应用于全文搜索引擎。它主要是用来存储某个单词(或词组)在一个文档或一组文档中的存储位置的映射,即提供了一种根据内容来查找文档的方式。由于不是根据文档来确定文档所包含的内容,而是进行相反的操作,因而称为倒排索引(Inverted Index)。 2.实例描述通常情况下,倒排索引由一个单词(或词组)以及相关的...

weixin_30586085的博客 423

深入理解MapReduce中环形缓冲区的概念

MapReduce过程图解: 标题 环形缓冲区图示: 环形缓冲区                      Map的输出结果是由collector处理的,每个Map任务不断地将键值对输出到在内存中构造的一个环形数据结构中。使用环形数据结构是为了更有效地使用内存空间,在内存中放置尽可能多的数据。                          这个数据结构其实就是个字节数组byte[]...

FullStackDeveloper0的博客 7498

Hadoopmapreduce环形缓冲区

mapreduce过程解析 数据在map中怎么写入磁盘? 数据:经过map逻辑处理过后的数据(key,value)… 磁盘:本地磁盘 环形缓冲区 1.为什么要环形缓冲区? 答:使用环形缓冲区,便于写入缓冲区和写出缓冲区同时进行。 2.为什么不等缓冲区满了再spill? 答:会出现阻塞。 3.数据的分区和排序是在哪完成的? 答:分区是根据元数据meta中的分区号partition来分区的,排序是...

BIG BOSS的博客 3580

MapReduce核心源码分析之MapTask OutPut(有对环形缓冲区的详细介绍以及详细的环形缓冲区的源码分析,让你对map输出阶段不在疑惑)

前文已经分析了Map Task的输入,这次我们来分析较为复杂的输出,看看Map Task的输出到底做了哪些事情,分析完之后,将会对我们学习MapReduce有很大的帮助 Map Task OutPut源码分析 这里依旧用的Hadoop的版本为2.7.2 ,工具是IDEA 由于上文我们已经有输入的分析,所以,这里直接找到MatpTask的run方法 我们直接往下看 , private <I...

weixin_44022416的博客 1282
上一篇: 【MapReduce】Mapreduce基础知识整理 (一) 基础介绍、task、并行度机制、切片机制、
下一篇: 【Activiti】Activiti 6 部署初体验
时间的美景
博客等级 码龄15年 44粉丝 109原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值