Spark Shuffle 源码剖析

概念理论铺垫

一、 Spark 分区数量由谁决定

Spark source 如果是TextFile() 读取HDFS中的文件,2参数,第一个参数是路径,第二个是指定分区数量

  1. 如果指定分区数量,0或1,则分区数量的多少取决于文件数量的多少
  2. 如果没有指定分区数量,默认则是2,如果文件总大小为100m,100/2(分区数量)=50,50为goalSize,如果50会和Hdfs中设置得block块的大小比较,选出一个最小的值,然后最小的值的1.1倍不会参与切分,如果大于,则会进行while循环拆分,100-50=50,这里则会有2个切片,会有2个分区数量,也就是2个Task。

二 、Spark shuffle 理论知识铺垫

Spark Shuffle 有2个阶段,分别为Map和Reduce阶段,也可以是ShuffleWrite 阶段和 Shuffle Read 阶段,期间一定伴随着写磁盘的操作,先是缓存到内存中然后再溢写到磁盘当中,其中涉及到Spark内存管理机制,有兴趣可以去看以看。

大体流程 ShuffleMapStage -> Shuffle -> ResultStage

DAGScheduler 中 有 ShuffleMapStage ===== ResultStage

spark源码git spark shuffle源码_kafka

进去ResultTask中没有发现run方法 但是看到他的父类是Task

spark源码git spark shuffle源码_spark_02

父类中的Run方法被Finall 修饰 ,但是还是调用了 RunTask() 方法 ,这种写法叫做模板方法模式,给你一个骨架,让你自己去 写实现,具体怎么实现任务的运行由你自己决定!

spark源码git spark shuffle源码_寻址_03

然后又回到了runTask() 方法当中 ,其中有一个func(context, rdd.iterator(partition, context)) iterator 迭代器,点进去

final def iterator(split: Partition, context: TaskContext): Iterator[T] = {
   //  先判断存储等级  如果不是none  会进行获取并计算 
    if (storageLevel != StorageLevel.NONE) {
      getOrCompute(split, context)
    } else {
      computeOrReadCheckpoint(split, context)
    }
  }

点获取并计算,获取并进行计算,点进去

computeOrReadCheckpoint(partition, context)   // 进行计算  并做ck
private[spark] def computeOrReadCheckpoint(split: Partition, context: TaskContext): Iterator[T] =
  {
    if (isCheckpointedAndMaterialized) {
      firstParent[T].iterator(split, context)
    } else {
      compute(split, context)  // 计算  点进去是一个抽象方法  所以看子类 子类重写方法的实现
     }
  }

找ShuffleRDD

override def compute(split: Partition, context: TaskContext): Iterator[(K, C)] = {
    val dep = dependencies.head.asInstanceOf[ShuffleDependency[K, V, C]]
    SparkEnv.get.shuffleManager.getReader(dep.shuffleHandle, split.index, split.index + 1, context)
      .read()  // 进行读操作
      .asInstanceOf[Iterator[(K, C)]]
  }

Shuffle Write 阶段

ShuffleMapTask -> runTask -> Write -> ShuffleWrite -> Write-> SortShuffleWrite->writeIndexFileAndCommit-> DataFile IndexFile

如果ShuffleMapStage 前面还有阶段ShuffleMapStage 则是 writer.write(rdd.iterator(partition, context).asInstanceOf[Iterator[_ <: Product2[Any, Any]]]) 中的iterator 进行读操作 !

Spark 中沿用了分段日志 来解决数据寻址效率 ! 在Kafka中也是采用的是分段日志 来存储日志 分为 Segment 其中最重要的是 索引文件 ,时间戳索引文件,数据文件等 当然也含有 .clean、.deleted 结尾的文件 存在 解决了kafka 找数据时的速度 !!

在此补充kafka 高性能之道

Kafka 高性能之道

  • 顺序写和Memeory map file (提高写入性能)

因为kafka是基于磁盘,但是kafka能够轻松应对每秒百万极的写入,其中用到的就是顺序写和MMFile

顺序写: 分段日志 !!!

因为硬盘是机械结构,每次读写都会寻址,然后写入,其中寻址是一个机械动作,他是最耗时的,所以硬盘最讨厌随机IO,kafka利用顺序写,解决了大部分的寻址时间,因为kafka是基于内存的,所以不能和内存比较,所以他的数据写入时有一定的时间的。

MMFile:

kafka充分利用了现代操作系统分页存储来利用内存提高IO效率,MMFile叫做内存映射文件,在64位操作系统当中一般可以标是20g的数据文件,他的工作原理就时直接利用操作系统的page来实现文件到物理内存的直接映射,完成MMF后,用户对内存的所有操作会被操作系统自动刷新磁盘当中,来提高IO效率。

用户空间 运行在操作系统之上

内核空间 运行在操作系统之下

一般数据写入 除了顺序IO写入之外,伴随的是内存映射文件,将文件交给你pageCach,由操作系统自己决定何时刷新到磁盘当中

ZeroCopy:

kafka在响应客户端进行读出的时候,底层时采用的时0拷贝来实现,直接通过内核空间,传递输出,数据没有到达用户空间。

geCach,由操作系统自己决定何时刷新到磁盘当中

ZeroCopy:

kafka在响应客户端进行读出的时候,底层时采用的时0拷贝来实现,直接通过内核空间,传递输出,数据没有到达用户空间。

读请求 -> cpu -> 磁盘将数据读到自己的控制缓存区 -> 将磁盘缓冲区缓存到内核缓冲区当中 -> 将数据写出