Spark-SparkEnv简析之二

SerializerManager 序列化管理器 简析

Spark 中很多对象在通过网络传输或者写入存储体系时,都需要序列化,SparkEnv 中有两个序列化的组件,分别是 SerializerManager 和 closureSerializer

    def instantiateClassFromConf[T](propertyName: String, defaultClassName: String): T = {
      instantiateClass[T](conf.get(propertyName, defaultClassName))
    }
    // 这里创建的 serializer 默认 org.apache.spark.serializer.JavaSerializer,用户可以通过 spark.serializer 属性配置其他的序列化实现,如 org.apache.spark.serializer.KryoSerializer
    val serializer = instantiateClassFromConf[Serializer](
      "spark.serializer", "org.apache.spark.serializer.JavaSerializer")
    logDebug(s"Using serializer: ${serializer.getClass}")

    val serializerManager = new SerializerManager(serializer, conf, ioEncryptionKey)
   // closureSerializer 的实际类型固定为 org.apache.spark.serializer.JavaSerializer,用户不能够自己指定。JavaSerializer 采用 Java 语言自带的序列化API实现
    val closureSerializer = new JavaSerializer(conf)
SerializerManager 的属性

SerializerManager 给各种 Spark 组件提供序列化、压缩及加密的服务,下面是这个对象的属性

  • defaultSerializer : 默认的序列化器。此 defaultSerializer 即为上面代码中实例化的 serializer,也就是说defaultSerializer 的类型是JavaSerializer
  • conf : 即SparkConf
  • encryptionKey : 加密使用的密钥
  • kryoSerialize : Spark提供的另一种序列化器。kryoSerializer 的实际类型是 KryoSerializer,其采用 Google 提供的 Kryo 序列化库实现
  • stringClassTag : 字符申类型标记,即 ClassTag[String]。
  • primitiveAndPrimitiveArrayClassTags : 原生类型及原生类型数组的类型标记的集合,包括 : Boolean、Array[boolean]、Int、Array[int]、Long、Array[long]、Byte、Array[byte]、Null、Array[scala.runtime.Null$]、Char、Array[char]、Double、Array[double]、Float、Array[float]、Short、Array[short]等
  • compressBroadcast : 是否对广播对象进行压缩,可以通过 spark.broadcast.compress 属性配置,默认为true
  • compressShuffle : 是否对 Shuffle 输出数据压缩,可以通过 spark.shuffle.compress 属性配置,默认为true
  • compressRdds : 是否对RDD压缩,可以通过 spark.rdd.compress 属性配置,默认false
  • compressShuffleSpill : 是否对溢出到磁盘的 Shuffle 数据压缩,可以通过 spark.shuffle.spill.compress 属性配置,默认为true
  • compressionCodec : SerializerManager 使用的压缩编解码器。compressionCodec 的类型是CompressionCodec。在Spark1.x.x版本中,compresionCodec 是 BlockManager 的成员之一,现在把compressionCodec 和序列化、加密等功能都集中到 SerializerManager 中
CompressionCodec 的创建简析

为了节省磁盘存储空间,有些情况下需要对数据进行压缩

// 可以看到 compressionCodec 通过 lazy 关键字修饰为延迟初始化,即等到真正使用时才对其初始化
private lazy val compressionCodec: CompressionCodec = CompressionCodec.createCodec(conf)
 private val shortCompressionCodecNames = Map(
    "lz4" -> classOf[LZ4CompressionCodec].getName,
    "lzf" -> classOf[LZFCompressionCodec].getName,
    "snappy" -> classOf[SnappyCompressionCodec].getName,
    "zstd" -> classOf[ZStdCompressionCodec].getName)


// val DEFAULT_COMPRESSION_CODEC = "lz4"
// 如果没有指定spark.io. compression.codec,默认是lz4压缩编解码器 
  def getCodecName(conf: SparkConf): String = {
    conf.get(configKey, DEFAULT_COMPRESSION_CODEC)
  }

  def createCodec(conf: SparkConf): CompressionCodec = {
    createCodec(conf, getCodecName(conf))
  }

  def createCodec(conf: SparkConf, codecName: String): CompressionCodec = {
    // 从 shortCompressionCodecNames 缓存中获取编解码器名称对应的编解码器的类名
    val codecClass =
      shortCompressionCodecNames.getOrElse(codecName.toLowerCase(Locale.ROOT), codecName)
    val codec = try {
      // 通过 Java 反射生成编解码器的实例
      val ctor = Utils.classForName(codecClass).getConstructor(classOf[SparkConf])
      Some(ctor.newInstance(conf).asInstanceOf[CompressionCodec])
    } catch {
      case _: ClassNotFoundException | _: IllegalArgumentException => None
    }
    // 如果发现无法使用配置的编解码器 那么建议使用 val FALLBACK_COMPRESSION_CODEC = "snappy"
    codec.getOrElse(throw new IllegalArgumentException(s"Codec [$codecName] is not available. " +
      s"Consider setting $configKey=$FALLBACK_COMPRESSION_CODEC"))
  }

在 Spark 1.x.x 版本中,默认的压缩算法是 snappy,2.x.x 是 lz4

SerializerManager 的方法简析
  • encryptionEnabled : 当前 SerializerManager 是否支持加密。SerializerManager 要想支持加密,必须在构造 SerializerManager 的时候就传入 encryptionKey。可以通过 spark.io.encryption.enabled( 允许加密)、spark.io.encryption.keySizeBits (密钥长度,有128、192、256三种长度)、spark.io.encryption.keygen.algorithm (加密算法,默认为HmacSHA1 )等属性进行具体的配置
  • canUseKryo(ct:ClassTag[]):对于指定的类型标记 ct,是否能使用 kryoSerializer 进行序列化。当类型标记ct 属于 primitiveAndPrimitiveArrayClassTags 或者 stringClassTag 时,canUseKryo 方法才返回真
  • getSerializer(ct:ClassTag[],autoPick:Boolean) : 获取序列化器。如果 autoPick 为 true(即 Blockid 不为StreamBlockId 时)并且调用 canUseKryo 的结果为 true 时选择 kryoSerializer,否则选择 defaultSerializer
  • getSerializer(keyClassTag:ClassTag[],valueClassTag:ClassTag[]) : 获取序列化器。如果对于 keyClassTag 和 valueClassTag,调用 canUseKryo 的结果都 true 时选择 kryoSerializer,否则选择defaultSerializer
  • wrapStream(blockId:BlockId,s:InputStream) : 对 Block的输人流进行压缩与加密
  • wrapStream(blockId:BlockId,s:OutputStream ) : 对Block的输出流进行压缩与加密
  • wrapForEncryption(s:InputStream) : 对输入流进行加密
  • wrapForEncryption(s:OutputStream) : 对输出流进行加密
  • dataSerializeStreamT:ClassTag (blockId:BlockId,outputStream:OutputStream,values:Iterator[T]) : 对 Block 的输出流序列化。
  • dataSerialize[T:ClassTag] (blockId:BlockId,values:Iterator[T],allowEncryption:Boolean=true) : 序列化成分块字节缓冲区(ChunkedByteBuffer)。
  • dataSerializeWithExplicitClassTag(blockId:BlockId,values:Iterator[],classTag:ClassTag[],allowEncryption:Bolean=true) : 使用明确的类型标记,序列化成分块字节缓冲区(ChunkedByteBuffer)
  • dataDeserializeStream[T](blockId:BlockId,inputStream:InputStream,maybeEncrypted:Boolean=true)(classTag:ClassTag[T]) : 将输入流反序列化为值的迭代器 Iterator[T]

BroadcastManager 广播管理器简析

BroadcastManager 用于将配置信息和序列化后的 RDD、Job 及 ShuffleDependency 等信息在本地存储,如果为了容灾,也会复制到其他节点上

    val broadcastManager = new BroadcastManager(isDriver, conf, securityManager)

BroadcastManager除了构造器定义的三个成员属性外,内部还有三个成员

  • initialized : 表示 BroadcastManager 是否初始化完成的状态
  • broadcastFactory : 广播工厂实例
  • nextBroadcastId : 下一个广播对象的广播 ID,类型为 AtomicLong

BroadcastManager 在其初始化的过程中就会调用自身的 initialize 方法,当 initialize 执行完毕,BroadcastManager 就正式生效

  private def initialize() {
    synchronized {
      // 首先判断 BroadcastManager 是否已经初始化,以保证 BroadcastManager 只被初始化一次
      if (!initialized) {
        // 新建 TorrentBroadcastFactory 作为 BroadcastManager 的广播工厂实例
        broadcastFactory = new TorrentBroadcastFactory
        // 之后调用 TorrentBroadcastFactory 的 initialize 方法对 TorentBroadcastFactory进行初始化
        broadcastFactory.initialize(isDriver, conf, securityManager)
        // 最后将 BroadcastManager 自身标记为初始化完成状态
        initialized = true
      }
    }
  }

BroadcastManager 提供了三个方法

  def stop() {
    broadcastFactory.stop()
  }

  private val nextBroadcastId = new AtomicLong(0)  

  def newBroadcast[T: ClassTag](value_ : T, isLocal: Boolean): Broadcast[T] = {
    broadcastFactory.newBroadcast[T](value_, isLocal, nextBroadcastId.getAndIncrement())
  }

  def unbroadcast(id: Long, removeFromDriver: Boolean, blocking: Boolean) {
    broadcastFactory.unbroadcast(id, removeFromDriver, blocking)
  }

对应的在 TorrentBroadcastFactory 中也提供了这三个方法的实现

// 用于生成 TorrentBroadcast 实例,其作用为广播 TorrentBroadcast 中的 value,表面看只是生成了 TorrentBroadcast 但其实不然
override def newBroadcast[T: ClassTag](value_ : T, isLocal: Boolean, id: Long): Broadcast[T] = {
    new TorrentBroadcast[T](value_, id)
  }

  override def stop() { }

  override def unbroadcast(id: Long, removeFromDriver: Boolean, blocking: Boolean) {
    TorrentBroadcast.unpersist(id, removeFromDriver, blocking)
  }
TorrentBroadcastFactory 的属性
  • compressionCodec : 用于广播对象的压缩编解码器。可以设置 spark.broadcast.compress 属性为 true 启用,默认是启用的。compressionCodec的类型前面介绍过的CompressionCodec,而且最终采用的压缩算法与 SerializerManager 中的 CompressionCodec 是一致的
  • blockSize : 每个块的大小。它是个只读属性,可以使用 spark.broadcast.blockSize 属性进行配置,默认为4MB
  • broadcastId : 广播Id。broadcastId 实际是样例类 BroadcastBlockId
  • checksumEnabled : 是否给广播块生成校验和。可以通过 spark.broadcast.checksum 属性进行配置,默认为true
  • checksums : 用于存储每个广播块的校验和的数组
  • numBlocks : 广播变量包含的块的数量。numBlocks 通过调用 writeBlocks 方法获得。由于 numBlocks 是个 val 修饰的不可变属性,因此在构造 TorrentBroadcast 实例的时候就会调用 writeBlocks 方法将广播对象写入存储体系
  • _value : 从 Executor 或者 Driver 上读取的广播块的值。value 是通过调用 readBroadcastBlock 方法获得的广播对象。由于 value 是个 lazy 及 val 修饰的属性,因此在构造 TorrentBroadcast 实例的时候不会调用 readBroadcastBlock 方法,而是等到明确需要使用 value 的值时
广播对象的写操作简析
 private def writeBlocks(value: T): Int = {
    import StorageLevel._
    // 获取当前 SparkEnv 的 BlockManager 组件
    val blockManager = SparkEnv.get.blockManager
   // 调用 BlockManager 的 putSingle 方法将广播对象写入本地的存储体系。当 Spark 以 local 模式运行时,则会将广播对象写人 Driver 本地的存储体系,以便于任务也可以在 Driver 上执行。由于MEMORY_AND_DISK 对应的 StorageLevel 的 _replication 属性固定为 1,因此此处只会将广播对象写入 Driver 或 Executor 本地的存储体系
    if (!blockManager.putSingle(broadcastId, value, MEMORY_AND_DISK, tellMaster = false)) {
      throw new SparkException(s"Failed to store $broadcastId in BlockManager")
    }
    try {
      // 调用 TorrentBroadcast 的 blockifyObject 方法,将对象转换成一系列的块。每个块的大小由 blockSize 决定,使用当前 SparkEnv 中的 JavaSerializer 组件进行序列化,使用TorrentBroadcast 自身的 compressionCodec 进行压缩
      val blocks =
        TorrentBroadcast.blockifyObject(value, blockSize, SparkEnv.get.serializer, compressionCodec)
      // 如果需要给分片广播块生成校验和,则创建和上一步转换的块的数量一致的 checksums 数组
      if (checksumEnabled) {
        checksums = new Array[Int](blocks.length)
      }
      // 对每个块进行如下处理 
      blocks.zipWithIndex.foreach { case (block, i) =>
        if (checksumEnabled) {
          // 如果需要给分片广播块生成校验和,则给分片广播块生成校验和
          checksums(i) = calcChecksum(block)
        }
        // 给当前分片广播块生成分片的 BroadcastBlockId,分片通过 BroadcastBlockld 的 field 属性区别,例如,pieceO、piece1...
        val pieceId = BroadcastBlockId(id, "piece" + i)
        val bytes = new ChunkedByteBuffer(block.duplicate())
        // 调用 BlockManager 的 putBytes 方法将分片广播块以序列化方式写人 Driver 本地的存储体系。由于 MEMORY_AND_DISK_SER 对应的 StorageLeve l的 _replication 属性也固定为1,因此此处只会将分片广播块写人 Driver 或 Executor 本地的存储体系
        if (!blockManager.putBytes(pieceId, bytes, MEMORY_AND_DISK_SER, tellMaster = true)) {
          throw new SparkException(s"Failed to store $pieceId of $broadcastId " +
            s"in local BlockManager")
        }
      }
      // 返回块的数量
      blocks.length
    } catch {
      case t: Throwable =>
        logError(s"Store broadcast $broadcastId fail, remove all pieces of the broadcast")
        blockManager.removeBroadcast(id, tellMaster = true)
        throw t
    }
  }

广播对象的写入

广播对象的读操作简析

只有当 TorrentBroadcast 实例的 _value 属性值在需要的时候,才会调用 readBroadcastBlock方法获取值

  private def readBroadcastBlock(): T = Utils.tryOrIOException {
    TorrentBroadcast.synchronized {
      
      val broadcastCache = SparkEnv.get.broadcastManager.cachedValues

      Option(broadcastCache.get(broadcastId)).map(_.asInstanceOf[T]).getOrElse {
        setConf(SparkEnv.get.conf)
        // 获取当前 SparkEnv 的 BlockManager 组件
        val blockManager = SparkEnv.get.blockManager
        // 调用 BlockManager 的 getLocalValues 方法从本地的存储系统中获取广播对象,即通过 BlockManager 的 putSingle 方法写人存储体系的广播对象
        blockManager.getLocalValues(broadcastId) match {
          case Some(blockResult) =>
            if (blockResult.data.hasNext) {
              // // 如果从本地的存储体系中可以获取广播对象,则调用 releaseLock 方法(这个锁保证当块被一个运行中的任务使用时,不能被其他任务再次使用,但是当任务运行完成时,则应该释放这个锁),释放当前块的锁并返回此广播对象
              val x = blockResult.data.next().asInstanceOf[T]
              releaseLock(broadcastId)

              if (x != null) {
                broadcastCache.put(broadcastId, x)
              }

              x
            } else {
              throw new SparkException(s"Failed to get locally stored broadcast data: $broadcastId")
            }
          case None =>
          // 如果从本地的存储体系中没有获取到广播对象,那么说明数据是通过 BlockManager 的 putBytes 方法以序列化方式写人存储体系的。此时首先调用 readBlocks 方法从 Driver 或 Executor 的存储体系中获取广播块
            logInfo("Started reading broadcast variable " + id)
            val startTimeMs = System.currentTimeMillis()
            val blocks = readBlocks()
            logInfo("Reading broadcast variable " + id + " took" + Utils.getUsedTimeMs(startTimeMs))

            try {
              // 然后调用 TorrentBroadcast 的 unBlockifyObject 方法,将一系列的分片广播块转换回原来的广播对象
              val obj = TorrentBroadcast.unBlockifyObject[T](
                blocks.map(_.toInputStream()), SparkEnv.get.serializer, compressionCodec)
							// 最后再次调用 BlockManager 的 putSingle 方法将广播对象写人本地的存储体系,以便于当前Executor 的其他任务不用再次获取广播对象
              val storageLevel = StorageLevel.MEMORY_AND_DISK
              if (!blockManager.putSingle(broadcastId, obj, storageLevel, tellMaster = false)) {
                throw new SparkException(s"Failed to store $broadcastId in BlockManager")
              }

              if (obj != null) {
                broadcastCache.put(broadcastId, obj)
              }

              obj
            } finally {
              blocks.foreach(_.dispose())
            }
        }
      }
    }
  }
private def readBlocks(): Array[BlockData] = {
    // 新建用于存储每个分片广播块的数组 blocks
    val blocks = new Array[BlockData](numBlocks)
    // 获取当前 SparkEnv 的 BlockManager 组件
    val bm = SparkEnv.get.blockManager
    // 对各个广播分片进行随机洗牌,避免对广播块的获取出现“热点”,提升性能
    for (pid <- Random.shuffle(Seq.range(0, numBlocks))) {
      val pieceId = BroadcastBlockId(id, "piece" + pid)
      logDebug(s"Reading piece $pieceId of $broadcastId")
      // 调用 BlockManager 的 getLocalBytes 方法从本地的存储体系中获取序列化的分片广播块,
      bm.getLocalBytes(pieceId) match {
        // 如果本地可以获取到,则将分片广播块放入 blocks,并且调用 releaseLock 方法释放此分片广播块的锁
        case Some(block) =>
          blocks(pid) = block
          releaseLock(pieceId)
        // 如果本地没有,则调用 BlockManager 的 getRemoteBytes 方法从远端的存储体系中获取分片广播块
        case None =>
          bm.getRemoteBytes(pieceId) match {
           
            case Some(b) =>
              if (checksumEnabled) {
               // 对于获取的分片广播块再次调用 calcChecksum 方法计算校验和
                val sum = calcChecksum(b.chunks(0))
               // 将此校验和与调用 writeBlocks 方法时存入 checksums 数组的校验和进行比较
               // 校验和不相同,说明块的数据有损坏,此时抛出异常
                if (sum != checksums(pid)) {
                  throw new SparkException(s"corrupt remote block $pieceId of $broadcastId:" +s" $sum != ${checksums(pid)}")
                }
              }
              // 如果校验和相同,则调用 BlockManager 的 putBytes 方法将分片广播块写入本地存储体系,以便于当前 Executor 的其他任务不用再次获取分片广播块
              if (!bm.putBytes(pieceId, b, StorageLevel.MEMORY_AND_DISK_SER, tellMaster = true)) {
                throw new SparkException(
                  s"Failed to store $pieceId of $broadcastId in local BlockManager")
              }
            // 最后将分片广播块放人 blocks
              blocks(pid) = new ByteBufferBlockData(b, true)
            case None =>
              throw new SparkException(s"Failed to get $pieceId of $broadcastId")
          }
      }
    }
  // 返回 blocks 中的所有分片广播块
    blocks
  }

广播对象的读取

广播对象的去持久化简析
// TorrentBroadcastFactory 的 unbroadcast 方法实际调用了 TorrentBroadcast 的 unpersist 方法对由 id标记的广播对象去持久化  
  def unpersist(id: Long, removeFromDriver: Boolean, blocking: Boolean): Unit = {
    logDebug(s"Unpersisting TorrentBroadcast $id")
    // 可以看到 TorrentBroadcast 的 unpersist 方法实际调用了 BlockManager的子组件BlockManagerMaster 的 removeBroadcast 方法来实现对广播对象去持久化
    SparkEnv.get.blockManager.master.removeBroadcast(id, removeFromDriver, blocking)
  }

map任务输出跟踪器 简析

mapOutputTracker 用于跟踪 map 任务的输出状态,此状态便于 reduce 任务定位 map 输出结果所在的节点地址,进而获取中间输出结果

每个 map 任务或者 reduce 任务都会有其唯一标识,分别为 mapId 和 reduceId,每个 reduce 任务的输入可能是多个 map 任务的输出,reduce 会到各个 map任 务所在的节点上拉取 Block,这一过程叫做Shuffle,每次Shuffle都有唯一的标识shuffleId

需要先对 SparkEnv 中用于注册 RpcEndpoint 或者查找 RpcEndpoint 的方法 registerOrLookupEndpoint 进行简析

    def registerOrLookupEndpoint(
        name: String, endpointCreator: => RpcEndpoint):
      RpcEndpointRef = {
        
      if (isDriver) {
        logInfo("Registering " + name)
        // 如果当前实例是 Driver,则调用 setupEndpoint 方法向 Dispatcher 注册 Endpoint
        rpcEnv.setupEndpoint(name, endpointCreator)
      } else {
        // 如果是 Executor,则调用工具类 RpcUtils 的 makeDriverRef 方法向远端的 NettyRpcEnv 询问获取相关 RpcEndpoint 的 RpcEndpointRef
        RpcUtils.makeDriverRef(name, conf, rpcEnv)
      }
    }
    val mapOutputTracker = 
   if (isDriver) {
     // 如果当前应用程序是 Driver,则创建 MapOutputTrackerMaster,然后创建 MapOutputTrackerMasterEndpoint,并且注册到 Dispatcher 中,注册名为 MapOutputTracker
      new MapOutputTrackerMaster(conf, broadcastManager, isLocal)
    } else {
     // 如果当前应用程序是 Executor,则创建 MapOutputTrackerWorker,并从远端 Driver 实例的 NettyRpcEnv 的 Dispatcher 中查找 MapOutputTrackerMasterEndpoint 的引用
      new MapOutputTrackerWorker(conf)
    }
    // 无论是Driver还是Executor,最后都由 mapOutputTracker 的属性 trackerEndpoint 持有 MapOutputTrackerMasterEndpoint 的引用
    mapOutputTracker.trackerEndpoint = registerOrLookupEndpoint(MapOutputTracker.ENDPOINT_NAME,
      new MapOutputTrackerMasterEndpoint(
        rpcEnv, mapOutputTracker.asInstanceOf[MapOutputTrackerMaster], conf))
MapOutputTracker 的实现简析

无论是 MapOutputTrackerMaster 还是 MapOutputTrackerWorker,它们都继承自抽象类 MapOutputTracker

MapOutputTracker 内部定义了任务输出跟踪器的规范

MapOutputTracker 的属性
  • trackerEndpoint : 用于持有 Driver 上 MapOutputTrackerMasterEndpoint 的 RpcEndpointRef
  • epoch : 用于 Executor 故障转移的同步标记。每个 Executor 在运行的时候会更新 epoch,潜在的附加动作将清空缓存。当 Executor 丢失后增加 epoch
  • epochLock : 用于保证 epoch 变量的线程安全性
MapOutputTracker 的方法
  • askTraker方法
  protected def askTracker[T: ClassTag](message: Any): T = {
    try {
      // 调用 RpcEndpointRef 的 askSync 方法
      trackerEndpoint.askSync[T](message)
    } catch {
      case e: Exception =>
        logError("Error communicating with MapOutputTracker", e)
        throw new SparkException("Error communicating with MapOutputTracker", e)
    }
  }
  • sendTracker 方法
  protected def sendTracker(message: Any) {
    // 用于向 MapOutputTrackerMasterEndpoint 发送消息,并期望在超时时间之内获得的返回值为 true
    val response = askTracker[Boolean](message)
    if (response != true) {
      throw new SparkException(
        "Error reply received from MapOutputTracker. Expecting true, got " + response.toString)
    }
  }
MapOutputTrackerMaster 的实现简析

通常而言,我们所说的 mapOutputTracker 都是指 MapOutputTrackerMaster ,而不是 MapOutputTrackerWorker

MapOutputTrackerWorker 将 map 任务的跟踪信息,通过 MapOutputTrackerMasterEndpoint 的 RpcEndpointRef 发送给MapOutputTrackerMaster ,由 MapOutputTrackerMaster 负责整理和维护所有的 map 任务的输出跟踪信息

MapOutputTrackerMasterEndpoint 位于 MapOutputTrackerMaster 内部,二者只存在于 Driver 上

MapOutputTrackerMaster 的属性
  • minSizeForBroadcast : 用于广播的最小大小。可以使用 spark.shuffle.mapOutput.minSizeForBroadcast 属性配置,默认为512KB。minSizeForBroadcast 必须小于 maxRpcMessageSize
  • shuffleLocalityEnabled : 是否为 reduce 任务计算本地性的偏好。可以使用 spark.shuffle.reduceLocality.enabled 属性进行配置,默认为 true
  • shuffleStatuses : 用于存储 shuffleld 与 Array[ShuffleStatus] 的映射关系。 ShuffleStatus 作为一个辅助类维护了一些属性如下
    • mapStatus : 存储了类型为 MapStatus ,长度为 map 分区数量的数组,其中 MapStatus 维护了 map 输出 Block 的地址 BlockManagerId,所以 reduce 任务知道从何处获取 map 任务的中间输出
    • cachedSerializedStatuses : 用于存储 shuffleld 与序列化后的状态的映射关系。其中 key 对应 shuffleId, value为对 MapStatus 序列化后的字节数组
    • cachedSerializedBroadcast : 用于缓存序列化的广播变量,保持与 cachedSerializedStatuses 的同步。当需要移除 shuffleld 在 cachedSerializedStatuses 中的状态数据时,此缓存中的数据也会被移除
  • maxRpcMessageSize : 最大的 Rpc 消息的大小。此属性可以通过 spark.rpc.message.maxSize 属性进行配置,默认为 128,单位是 MB。minSizeForBroadcast 必须小于 maxRpcMessageSize。maxRpcMessageSize 实际通过调用RpcUtils 的 maxMesageSizeBytes 方法获得
  • mapOutputRequests : 使用阻塞队列来缓存 GetMapOutputMessage (获取 map 任务输出)的请求。
  • threadpool : 用于获取 map 输出的固定大小的线程池。此线程池提交的线程都以后台线程运行,且线程名以 map-output-dispatcher 为前缀,线程池大小可以使用 spark.shuffle.mapOutput.dispatcher.numThreads 属性配置,默认大小为8
MapOutputTrackerMaster 的运行原理

在创建 MapOutputTrackerMaster 的最后,会创建对 map 输出请求进行处理的线程池 threadpool

  private val threadpool: ThreadPoolExecutor = {
    // 获取此线程池的大小numThreads。此线程池的大小默认为8,也可以使用spark.shuffle.mapOutput.dispatcher.numThreads 属性配置
    val numThreads = conf.getInt("spark.shuffle.mapOutput.dispatcher.numThreads", 8)
    // 创建线程池。此线程池是固定大小的线程池,并且启动的线程都以后台线程方式运行,且线程名以map-output-dispatcher为前缀
    val pool = ThreadUtils.newDaemonFixedThreadPool(numThreads, "map-output-dispatcher")
    for (i <- 0 until numThreads) {
      // 启动与线程池大小相同数量的线程,每个线程执行的任务都是 MessageLoop
      pool.execute(new MessageLoop)
    }
    // 返回此线程池的引用
    pool
  }
 private class MessageLoop extends Runnable {
    override def run(): Unit = {
      try {
        while (true) {
          try {
            // 从 mapOutputRequests 中获取 GetMapOutputMessage。
            // 由于 mapOutputRequests 是个阻塞队列,所以当 mapOutputRequests 中没有 GetMapOutputMessage 时,MessageLoop 线程会被阻塞。
            // GetMapOutputMessage 是个样例类,包含了 shuffleId 和 RpcCallContext 两个属性,RpcCallContext 用于服务端回复客户端
            val data = mapOutputRequests.take()
            // 如果取到的 GetMapOutputMessage 是 PoisonPill(“毒药”),那么此 MessageLoop 线程将退出(通过 return 语句)。这里有个动作,就是将 PoisonPill 重新放人到 mapOutputRequests 中,这是因为 threadpool 线程池极有可能不止一个MessageLoop 线程,为了让大家都“毒发身亡”,还需要把“毒药”放回到 receivers 中,这样其他“活着”的线程就会再次误食“毒药”,达到所有 MessageLoop 线程都结束的效果 
            if (data == PoisonPill) {
              mapOutputRequests.offer(PoisonPill)
              return
            }
            val context = data.context
            val shuffleId = data.shuffleId
            val hostPort = context.senderAddress.hostPort
            logDebug("Handling request to send map output locations for shuffle " + shuffleId +
              " to " + hostPort)
            // 如果取到的 GetMapOutputMessage 不是“毒药”,那么通过 GetMapOutputMessage 获取 shuffleId,然后再获取到 shuffleStatus
            val shuffleStatus = shuffleStatuses.get(shuffleId).head
            // 调用 RpcCallContext 的回调方法 reply,将序列化的 map 任务状态信息返回给客户端(即其他节点的 Executor)
            context.reply(
              // 调用 serializedMapStatus 方法获取 shuffleStatus 携带的 cachedSerializedMapStatus 进行回应
              shuffleStatus.serializedMapStatus(broadcastManager, isLocal, minSizeForBroadcast))
          } catch {
            case NonFatal(e) => logError(e.getMessage, e)
          }
        }
      } catch {
        case ie: InterruptedException => // exit
      }
    }
  }

MapOutputTrackerMaster 中的 MessageLoop 任务在不断地消费着阻塞队列 mapOutputRequests 中的 GetMapOutputMessage

在MapOutputTrackerMaster 中要将 GetMapOutputMessage 放入 mapOutputRequests,主要通过调用 MapOutputTrackerMaster的post方法

  def post(message: GetMapOutputMessage): Unit = {
    mapOutputRequests.offer(message)
  }

MapOutputTrackerMasterEndpoint 组件用于接收获取 map 中间状态和停止对 map 中间状态进行跟踪的请求,它实现了特质RpcEndpoint 并重写了 receiveAndReply 方法

private[spark] class MapOutputTrackerMasterEndpoint(
    override val rpcEnv: RpcEnv, tracker: MapOutputTrackerMaster, conf: SparkConf)
  extends RpcEndpoint with Logging {

  logDebug("init") // force eager creation of logger
  // 可以看到MapOutputTrackerMasterBndpoint将接收两种类型的请求
  override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
    // 获取 map 中间输出状态。当接收到 GetMapOutputStatuses 消息后,将调用 MapOutputTrackerMaster 的 post 方法投递GetMapOutputMessage 类型的消息
    case GetMapOutputStatuses(shuffleId: Int) =>
      val hostPort = context.senderAddress.hostPort
      logInfo("Asked to send map output locations for shuffle " + shuffleId + " to " + hostPort)
      val mapOutputStatuses = tracker.post(new GetMapOutputMessage(shuffleId, context))
    // 停止 map 中间输出的跟踪。首先回调 RpcCallContext 的 reply 方法向客户端返回 true,然后调用MapOutputTrackerMaster 的 stop 方法停止 MapOutputTrackerMaster
    case StopMapOutputTracker =>
      logInfo("MapOutputTrackerMasterEndpoint stopped!")
      context.reply(true)
      stop()
  }
}
 override def stop() {
   // 向 mapOutputRequests 投递 PoisonPil(“毒药”)。此操作会向 mapOutputRequests 队列中添加 PoisonPil,进而停止MapOutputTrackerMaster 中的所有 MessageLoop 线程
   mapOutputRequests.offer(PoisonPill)
   // 关闭 threadpool。由于 MapOutputTrackerMaster 中的所有 MessageLoop 线程都已经停止,因而可以平滑关闭threadpool
   threadpool.shutdown()
   // 调用 sendTracker 向 MapOutputTrackerMasterEndpoint 发送 StopMapOutputTracker 消息
   sendTracker(StopMapOutputTracker)
   // 回收 MapOutputTrackerMasterEndpoint
   trackerEndpoint = null
   // 清空 shuffleStatuses
   shuffleStatuses.clear()
 }
MapOutputTrackerWorker 的实现简析
MapOutputTrackerWorker 的属性
  • mapStatuses : 用于存储 shuffleId 与 Array[MapStatus] 的映射关系。由于 MapStatus 维护了 map 输出 Block 的地址BlockManagerId,所以 reduce 任务知道从何处获取 map 任务的中间输出
  • fetching : shuffle 获取集合。数据类型为 HashSet[Int],用来记录当前 Executor 正在从哪些 map 输出的位置拉取数据
MapOutputTrackerWorker 的实现
  • getStatuses 方法
private def getStatuses(shuffleId: Int): Array[MapStatus] = {
    // 从当前 MapOutputTrackerWorker 的 mapStatuses 缓存中获取 MapStatus 数组
    val statuses = mapStatuses.get(shuffleId).orNull
    // 如果缓存中没有 MapStatus
    if (statuses == null) {
      logInfo("Don't have map outputs for shuffle " + shuffleId + ", fetching them")
      val startTime = System.currentTimeMillis
      var fetchedStatuses: Array[MapStatus] = null
      // 如果 shuffle 获取集合(即 fetching )中已经存在要取的 shuffleId (这说明已经有其他线程对此 shuffleId 的数据进行远程拉取了),那么就等待其他线程获取。等待会一直持续
      fetching.synchronized {
        while (fetching.contains(shuffleId)) {
          try {
            fetching.wait()
          } catch {
            case e: InterruptedException =>
          }
        }
      // 直到 fetching 中不存在要取的 shuffleId (这说明其他线程对此shuffleId的数据进行远程拉取的操作已经结束),并再次从 mapStatuses 缓存中获取 MapStatus 数组
        fetchedStatuses = mapStatuses.get(shuffleId).orNull
        // 如果 fetching 中不存在要取的 shuffleId,那么当前线程需要将 shuffleId 加入 fetching,以表示已经有线程对此shuffleId 的数据进行远程拉取了
        if (fetchedStatuses == null) {
          fetching += shuffleId
        }
      }

      if (fetchedStatuses == null) {
        logInfo("Doing the fetch; tracker endpoint = " + trackerEndpoint)
        try {
          // 调用 askTracker 方法向 MapOutputTrackerMasterEndpoint 发送 GetMapOutputStatuses 消息,以获取 map任务的状态信息,MapOutputTrackerMasterEndpoint 接收到 GetMapOutputStatuses 消息后,将 GetMapOutputStatuses 消息转换为 GetMapOutputMessage 消息,异步线程MessageLoop从队列中取出GetMapOutputMessage,将请求的map任务状态信息序列化后返回给请求方
          val fetchedBytes = askTracker[Array[Byte]](GetMapOutputStatuses(shuffleId))
          // 请求方接收到 map 任务状态信息后,调用 MapOutputTracker 的 deserializeMapStatuses 方法对 map 任务状态进行反序列化操作,然后放入本地的 mapStatuses 缓存中
          fetchedStatuses = MapOutputTracker.deserializeMapStatuses(fetchedBytes)
          logInfo("Got the output locations")
          // 放人 mapOutputRequests 队列
          mapStatuses.put(shuffleId, fetchedStatuses)
        } catch {
          case e: SparkException =>
            throw new MetadataFetchFailedException(shuffleId, -1,
              s"Unable to deserialize broadcasted map statuses for shuffle $shuffleId: " +
                e.getCause)
        } finally {
          // 拉取结束后无论如何需要将 shuffleId 从 fetching 中移除,并唤醒那些在 fetching 的锁上等待的线程,以便这些线程能够获取自己需要的 MapStatus 数组
          fetching.synchronized {
            fetching -= shuffleId
            fetching.notifyAll()
          }
        }
      }
      logDebug(s"Fetching map output statuses for shuffle $shuffleId took " +
        s"${System.currentTimeMillis - startTime} ms")

      if (fetchedStatuses != null) {
        fetchedStatuses
      } else {
        logError("Missing all output locations for shuffle " + shuffleId)
        throw new MetadataFetchFailedException(
          shuffleId, -1, "Missing all output locations for shuffle " + shuffleId)
      }
    } else {
      // 如果获取到 MapStatus 数组,返回得到的MapStatus数组
      statuses
    }
  }
  • getMapSizesByExecutorId 方法
//  通过 shuffleId 和 reduceId 获取存储了 reduce 所需的 map 中间输出结果的 BlockManager 的 BlockManagerId,以及map 中间输出结果每个 Block 块的 BlockId 与大小
MapOutputTracker的convertMapStatuses方法用于将Array[MapStatus]转换为Seql(BlockManagerld,Seq[(Blockid,Long)])]  
override def getMapSizesByExecutorId(shuffleId: Int, startPartition: Int, endPartition: Int)
      : Iterator[(BlockManagerId, Seq[(BlockId, Long)])] = {
    logDebug(s"Fetching outputs for shuffle $shuffleId, partitions $startPartition-$endPartition")
    val statuses = getStatuses(shuffleId)
    try {
      MapOutputTracker.convertMapStatuses(shuffleId, startPartition, endPartition, statuses)
    } catch {
      case e: MetadataFetchFailedException =>
        // We experienced a fetch failure so our mapStatuses cache is outdated; clear it:
        mapStatuses.clear()
        throw e
    }
  }
  • updateEpoch 方法
// 当 Executor 运行出现故障时,Master 会再分配其他 Executor 运行任务,此时会调用 updateEpoch 方法更新纪元,并且清空mapStatuses  
def updateEpoch(newEpoch: Long): Unit = {
    epochLock.synchronized {
      if (newEpoch > epoch) {
        logInfo("Updating epoch to " + newEpoch + " and clearing cache")
        epoch = newEpoch
        mapStatuses.clear()
      }
    }
  }
  • unregisterShuffle 方法
// unregisterShuffle 方法用于 ContextCleaner 清除 shuffleId 对应 MapStatus 的信息
def unregisterShuffle(shuffleId: Int): Unit = {
    mapStatuses.remove(shuffleId)
  }
Shuffle 的注册

DAGScheduler 在创建 ShuffleMapStage 的时候,将调用 MapOutputTrackerMaster 的containsShuffle 方法,查看是否已经存在 shuffleId 对应的 ShuffleStatus。如果 MapOutputTrackerMaster 中未注册此 shuffleld,那么调用 MapOutputTrackerMaster 的registerShuffle 方法注册 shuffle

containsShuffle 方法用于查找 MapOutputTrackerMaster 的 shuffleStatuses 是否已经注册了 shuffleId

 def containsShuffle(shuffleId: Int): Boolean = shuffleStatuses.contains(shuffleId)

registerShuffle 方法用于向 MapOutputTrackerMaster 的 shuffleStatuses 注册 shuffleId 与对应的 ShuffleStatus 的关系

  def registerShuffle(shuffleId: Int, numMaps: Int) {
    if (shuffleStatuses.put(shuffleId, new ShuffleStatus(numMaps)).isDefined) {
      throw new IllegalArgumentException("Shuffle ID " + shuffleId + " registered twice")
    }
  }

当 ShuffleMapStage 内的所有 ShuffleMapTask 运行成功后,将调用 MapOutputTrackerMaster 的 registerMapOutputs 方法

  def registerMapOutput(shuffleId: Int, mapId: Int, status: MapStatus) {
    shuffleStatuses(shuffleId).addMapOutput(mapId, status)
  }

registerMapOutputs 方法将把 ShuffleMapStage 中每个 ShuffleMapTask 的 MapStatus 保存到 shuffleld 在 ShuffleStatus 中对应的数组中

存储体系的构建 简析

BroadcastManager 的底层都依赖于 SparkEnv 的存储体系。存储体系中最重要的组件包括 Shuffle 管理器 ShuffleManager、内存管理器MemoryManager、块传输服务 BlockTransferService、对所有 BlockManager 进行管理的 BlockManagerMaster、磁盘块管理器DiskBlockManager、块锁管理器 BlockInfoManager 及块管理器 BlockManager。这里简单介绍SparkEnv中存储体系各个组件的实例化

   
   val shortShuffleMgrNames = Map(
      "sort" -> classOf[org.apache.spark.shuffle.sort.SortShuffleManager].getName,
      "tungsten-sort" -> classOf[org.apache.spark.shuffle.sort.SortShuffleManager].getName)
    val shuffleMgrName = conf.get("spark.shuffle.manager", "sort")
    val shuffleMgrClass =
      shortShuffleMgrNames.getOrElse(shuffleMgrName.toLowerCase(Locale.ROOT), shuffleMgrName)
   // 根据 spark.shuffle.manager 属性,实例化 ShuffleManager。Spark2.x.x版本提供了 sort 和 tungsten-sort 两种ShuffleManager 的实现。无论是 sort 还是 tungsten-sort,我们看到其实现类都是 SortShuffleManager 
    val shuffleManager = instantiateClass[ShuffleManager](shuffleMgrClass)

    val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false)
    // MemoryManager 的主要实现有 StaticMemoryManager 和 UnifiedMemoryManager。StaticMemoryManager 是 Spark 早期版本遗留下来的内存管理器实现,可以配置 spark.memory.useLegacyMode 属性来指定,该属性默认为 false,因此默认的内存管理器是 UnifiedMemoryManager
    val memoryManager: MemoryManager =
      if (useLegacyMemoryManager) {
        new StaticMemoryManager(conf, numUsableCores)
      } else {
        UnifiedMemoryManager(conf, numUsableCores)
      }
  // 获取当前 SparkEnv 的 块传输服务 BlockTransferService 对外提供的端口号。如果当前实例是 Driver,则从 SparkConf 中获取由常量 DRIVER_BLOCK_MANAGER_PORT 指定的端口。如果当前实例是 Executor,则从 SparkConf 中获取由常量BLOCK_MANAGER_PORT指定的端口
 // 可以通过指定 spark.driver.blockManager.port 属性或者 spark.blockManager.port 属性对 BlockTransferService 的端口进行配置
    val blockManagerPort = if (isDriver) {
      conf.get(DRIVER_BLOCK_MANAGER_PORT)
    } else {
      conf.get(BLOCK_MANAGER_PORT)
    }

// 创建块传输服务 BlockTransferService。这里使用的是 BlockTransferService 的子类NettyBlockTransferService,NettyBlockTransferService 将提供对外的块传输服务。也正是因为 MapOutputTracker 与NettyBlockTransferService 的配合,才实现了Spark 的 Shuffle。 因此:

    val blockTransferService =
      new NettyBlockTransferService(conf, securityManager, bindAddress, advertiseAddress,
        blockManagerPort, numUsableCores)
// 查找或注册BlockManagerMasterBndpoint。这里的registerOrLookupEndpoint方法会根据不同应用程序来进行注册
// 1 当前应用程序是 Driver,则创建 BlockManagerMasterEndpoint,并且注册到 Dispatcher 中,注册名为BlockManagerMaster
// 2 当前应用程序是 Executor,则从远端 Driver 实例的 NettyRpcEnv的Dispatcher 中查找 BlockManagerMasterEndpoint 的引用
// 无论是 Driver 还是 Executor,最后都由 BlockManagerMaster 的属性 driverEndpoint 持有BlockManagerMasterEndpoint 的引用
// 创建 BlockManagerMaster
    val blockManagerMaster = new BlockManagerMaster(registerOrLookupEndpoint(
      BlockManagerMaster.DRIVER_ENDPOINT_NAME,
      new BlockManagerMasterEndpoint(rpcEnv, isLocal, conf, listenerBus)),
      conf, isDriver)
// 创建 BlockManager。此处只创建了 BlockManager,只有其 init 方法被调用后,BlockManager 才能正常工作
    val blockManager = new BlockManager(executorId, rpcEnv, blockManagerMaster,
      serializerManager, conf, memoryManager, mapOutputTracker, shuffleManager,
      blockTransferService, securityManager, numUsableCores)

度量系统的创建 简析

   val metricsSystem = if (isDriver) {
     // 当前实例为 Driver : 创建度量系统,并且制定度量系统的实例名为 driver。此时虽然创建了,但是并未启动,目的是等待SparkContext 中的任务调度器 TaskScheculer 告诉度量系统应用程序 ID 后再启动
      MetricsSystem.createMetricsSystem("driver", conf, securityManager)
    } else {
     // 当前实例为 Executor : 设置 spark.executor.id 属性为当前 Executor 的 ID,然后创建并启动度量系统
      conf.set("spark.executor.id", executorId)
      val ms = MetricsSystem.createMetricsSystem("executor", conf, securityManager)
      ms.start()
      ms
    }
  // 创建度量系统使用了伴生对象 MetricsSystem 的 createMetricsSystem 方法
	def createMetricsSystem(
      instance: String, conf: SparkConf, securityMgr: SecurityManager): MetricsSystem = {
    new MetricsSystem(instance, conf, securityMgr)
  }
度量系统 MetricsSystem 的属性 简析
  • instance : 度量系统的实例名。例如,Master、Worker、Application、Driver 及Executor 等
  • metricsConfig : 度量配置,metricsConfig 的类型为 MetricsConfig,主要提供对度量配置的设置、加载、转换等功能,MetricsConfig 中的度量配置包括了 Sink 和 Source,因此 MetricsSystem 将根据 MetricsConfig 构建度量系统的所有 Sink 和 Source
  • sinks : Sink的数组,sinks 用于缓存所有注册到 MetricsSystem 的度量输出
  • sources : Source的数组,sources 用于缓存所有注册到 MetricsSystem 的 Source
  • registry : 度量注册点 MetricRegistry。Source 和 Sink 实际都是通过 MetricRegistry 注册到 Metrics 的度量仓库中的。Metrics 是codahale 提供的第三方度量仓库,这里的 MetricRegistry 是 Metrics 提供的API
  • running : 用于标记当前 MetricsSystem 是否正在运行
  • metricsServlet : metricsServlet 将在添加 ServletContextHandler 后通过 WebUI 展示。metricsServlet 的类型是Option[MetricsServlet]
MetricsConfig 简析
MetricsConfig 的属性 简析
  • properties : 度量的属性信息。类型为 Properties
  • perlnstanceSubProperties : 每个实例的子属性。缓存每个实例与其属性的映射关系,类型为 HashMap[String,Properties]
MetricsConfig 的内部方法 简析
// 用于给 MetricsConfig 的 properties 中添加默认的度量属性,这里有四个默认的度量属性
private def setDefaultProperties(prop: Properties) {
    prop.setProperty("*.sink.servlet.class", "org.apache.spark.metrics.sink.MetricsServlet")
    prop.setProperty("*.sink.servlet.path", "/metrics/json")
    prop.setProperty("master.sink.servlet.path", "/metrics/master/json")
    prop.setProperty("applications.sink.servlet.path", "/metrics/applications/json")
  }
// 从指定的文件为 MetricsConfig 的 properties 加载度量属性
// 我们知道如果没有指定度量属性文件或者此文件不存在,那么将从类路径下的属性文件 metrics.properties(即常量DEFAULT_METRICS_CONF_FILENAME 的值)中加载度量属性。读取类路径下的文件时使用了工具类 Utils 的 getSparkClassLoader 方法  
private[this] def loadPropertiesFromFile(path: Option[String]): Unit = {
    var is: InputStream = null
    try {
      is = path match {
        case Some(f) => new FileInputStream(f)
        case None => Utils.getSparkClassLoader.getResourceAsStream(DEFAULT_METRICS_CONF_FILENAME)
      }

      if (is != null) {
        properties.load(is)
      }
    } catch {
      case e: Exception =>
        val file = path.getOrElse(DEFAULT_METRICS_CONF_FILENAME)
        logError(s"Error loading configuration file $file", e)
    } finally {
      if (is != null) {
        is.close()
      }
    }
  }
// 对于 properties 中的每个属性 kv,通过正则表达式匹配找出 kv 的 key 的前缀和后缀,以前缀为实例,后缀作为新的属性 kv2 的key,kv 的 value 作为新的属性 kv2 的 value,最后将属性 kv2 添加到实例对应的属性集合中作为实例的属性  
def subProperties(prop: Properties, regex: Regex): mutable.HashMap[String, Properties] = {
    val subProperties = new mutable.HashMap[String, Properties]
    prop.asScala.foreach { kv =>
      if (regex.findPrefixOf(kv._1.toString).isDefined) {
        val regex(prefix, suffix) = kv._1.toString
        subProperties.getOrElseUpdate(prefix, new Properties).setProperty(suffix, kv._2.toString)
      }
    }
    subProperties
  }

这里以KV对".sink.servlet.class"->“org.apache.spark.metrics.sink.MetricsServlet” 和 ".sink.servlet.path"->"/metrics/json"为例

首先使用正则表达式 ^(*|[a-zA-Z]+).(.+) 匹配出两个元组,那么第一个KV对的两个元组分别为 * 和 sink.servlet.class 。第二个KV对的两个元组分别为 * 和 sink.servlet.path 。然后以第一个元组作为前缀,第二个元组作为后缀。

以前缀为实例,后缀为新属性的 key,KV 对的 value 作为新属性的 value。最后将新属性添加到实例对应的 Properties 中,将实例与 Properties 之间的映射关系存入 Map。结果如下:

Мар(
    *->{
        sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet,
        sink.servlet.path=/metrics/json
        }
   )

根据这个例子,4个默认度量属性提取出实例的属性后为 : 即实例 applications、master、* 与它们对应的属性集合

Map(
    applications->{
       sink.servlet.path=/metrics/applications/json
    }.
    master->{
       sink.servlet.path=/metrics/master/json
    }*->{
        sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet,
        sink.servlet.path=/metrics/json
    }
   )
// MetricsConfig的初始化过程  
def initialize() {
    // 设置默认属性
    setDefaultProperties(properties)
    // 从文件中加载度量属性。可以通过 spark.metrics.conf 属性指定度量属性文件
    loadPropertiesFromFile(conf.getOption("spark.metrics.conf"))
    val prefix = "spark.metrics.conf."
    conf.getAll.foreach {
    // 从 SparkConf 中查找以 spark.metrics.conf. 为前缀的配置属性,并且截取 key 的前缀后的部分作为度量属性的key/value 不变
      case (k, v) if k.startsWith(prefix) =>
        properties.setProperty(k.substring(prefix.length()), v)
      case _ =>
    }
   // 提取实例的属性。这里指定了正则表达式 ^(*I[a-zA-Z]+).(.+) 找出 properties 中各个实例的属性
      perInstanceSubProperties = subProperties(properties, INSTANCE_REGEX)
    if (perInstanceSubProperties.contains(DEFAULT_PREFIX)) {
      val defaultSubProperties = perInstanceSubProperties(DEFAULT_PREFIX).asScala
      // 向子属性中添加缺失的默认子属性(所谓默认子属性,即在 Map 中以 * 为 key 的属性)
      for ((instance, prop) <- perInstanceSubProperties if (instance != DEFAULT_PREFIX);
           (k, v) <- defaultSubProperties if (prop.get(k) == null)) {
        prop.put(k, v)
      }
    }
  }

为了理解更加简单,这里以上面说明中得到的Map为例

默认子属性中的 sink.servlet.class 属性是其他两个(applications 和 master)子属性中不具有的属性,因此需要将此属性添加到以 applications 和 master 为 key 的子属性中。因此得到的结果为:

Map(applications->{
       sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet,
       sink.servlet.path=/metrics/applications/json
    },
    master->{
        sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet,
        sink.servlet.path=/metrics/master/json
    }*->{
        sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet,
        sink.servlet.path=/metrics/json
    }
   )

经过以上处理,每个度量实例与其属性集合的信息都存储在 perlnstanceSubProperties 了

在默认配置下,Spark 度量系统的属性配置只有 Sink,却没有 Source,没有了度量源,光有输出能有什么用?度量系统中的 Sink 都通过 Spark 属性或者从文件中加载,而 Source 除了与 Sink 相同的两种方式外,Spark 的实现中,很多地方都是调用 MetricsSystem 的 registerSource 方法注册各种 Source 的。虽然如此,有些 Source 依然需要你在 Spark 属性或者指定的文件中明确配置,就像这样: master.source.jvm.class=org.apache.spark.metrics.source.JvmSource

// getlnstance 方法优先从 perinstanceSubProperties 中获取指定实例的属性,如果 perInstanceSubProperties 中没有存储对应的实例,则返回默认实例(即*)的属性  
def getInstance(inst: String): Properties = {
    perInstanceSubProperties.get(inst) match {
      case Some(s) => s
      case None => perInstanceSubProperties.getOrElse(DEFAULT_PREFIX, new Properties)
    }
  }
MetricsSystem 的常用方法 简析
buildRegistryName 方法
// 用于给Source生成向MetricRegistry中注册的注册  
private[spark] def buildRegistryName(source: Source): String = {
  // metricsNamespace : 度量命名空间。可以通过 spark.metrics.namespace 属性进行配置,默认读取spark.app.id属性的值
    val metricsNamespace = conf.get(METRICS_NAMESPACE).orElse(conf.getOption("spark.app.id"))
  // executorld :当前 Executor 的身份标识,通过读取 spark.executor.id 属性获得
    val executorId = conf.getOption("spark.executor.id")
    val defaultName = MetricRegistry.name(source.sourceName)
  // defaultName : 调用 MetricRegistry 的 name 方法生成的默认注册名
    if (instance == "driver" || instance == "executor") {
      if (metricsNamespace.isDefined && executorId.isDefined) {
        // 如果定义了 metricsNamespace 和 executorId,那么调用 MetricRegistry 的 name 方法生成${metricsNamespace}.${executorld}.${defaultName} 格式的注册名
        MetricRegistry.name(metricsNamespace.get, executorId.get, source.sourceName)
      } else {
        if (metricsNamespace.isEmpty) {
          logWarning(s"Using default name $defaultName for source because neither " +
            s"${METRICS_NAMESPACE.key} nor spark.app.id is set.")
        }
        if (executorId.isEmpty) {
          logWarning(s"Using default name $defaultName for source because spark.executor.id is " +
            s"not set.")
        }
        // 如果metricsNamespace或executorld没有定义,那么采用defaultName为注册名
        defaultName
      }
    } else { defaultName }
  }
registerSource 方法
 // 用于向MetricsSystem中注册度量源
def registerSource(source: Source) {
    sources += source
    try {
      val regName = buildRegistryName(source)
      registry.register(regName, source.metricRegistry)
    } catch {
      case e: IllegalArgumentException => logInfo("Metrics already registered", e)
    }
  }
getServletHandlers 方法
// 为了将度量系统和 SparkU I结合起来使用,即将SparkUI的Web展现也作度量系统的Sink之一,需要能将度量输出到 SparkUI 的页面。MetricsServlet 是特质 Sink 的实现之一,但是它还不是一个 ServletContextHandler,因此需有一个转换  
def getServletHandlers: Array[ServletContextHandler] = {
    require(running, "Can only call getServletHandlers on a running MetricsSystem")
   // 调用了 MetricsServlet 的 getHandlers 方法来实现转换
    metricsServlet.map(_.getHandlers(conf)).getOrElse(Array())
  }
// getHandlers 也调用了 JettyUtils 的 createServletHandler 方法创建 ServletContextHandler
def getHandlers(conf: SparkConf): Array[ServletContextHandler] = {
    Array[ServletContextHandler](
      createServletHandler(servletPath,
        new ServletParams(request => getMetricsSnapshot(request), "text/json"), securityMgr, conf)
    )
  }
MetricsSystem 的启动 简析
  def start(registerStaticSources: Boolean = true) {
    require(!running, "Attempting to start a MetricsSystem that is already running")
    // 将当前 MetricsSystem 的 running 字段置为 true,即表示 MetricsSystem 已经处于运行状态
    running = true
    if (registerStaticSources) {
    // 将静态的度量来源 CodegenMetrics 和 HiveCatalogMetrics 注册到 MetricRegistry
      StaticSources.allSources.foreach(registerSource)
      // // 从初始化完成的 MetricsConfig 中获取当前实例的度量来源属性,并调用 registerSources 方法,将这些度量来源注册到MetricRegistry
      registerSources()
    }
    registerSinks()
    sinks.foreach(_.start)
  }
  private def registerSources() {
    // 获取当前实例的度量属性。根据 MetricsConfig 的 getlnstance 方法的逻辑,当实例不存在时将返回默认实例的属性。以 driver 实例来讲,默认不存在 driver 的实例属性,因此返回 * 对应的属性
    val instConfig = metricsConfig.getInstance(instance)
    // 匹配正则表达式 ,获取所有度量源更细粒度的实例及属性
    val sourceConfigs = metricsConfig.subProperties(instConfig, MetricsSystem.SOURCE_REGEX)

    sourceConfigs.foreach { kv =>
      val classPath = kv._2.getProperty("class")
      try {
        // 使用实例的 class 属性,通过 Java 反射生成度量源的实例,并调用 registerSource 方法将此度量源注册到MetricRegistry
        val source = Utils.classForName(classPath).newInstance()
        // 从初始化完成的 MetricsConfig 中获取当前实例的度量输出属性,并将这些度量输出注册到sinks
        registerSource(source.asInstanceOf[Source])
      } catch {
        case e: Exception => logError("Source class " + classPath + " cannot be instantiated", e)
      }
    }
  }

  private def registerSinks() {
    // 获取当前实例的度量属性。根据 MetricsConfig 的 getlnstance 方法,当实例不存在时将返回默认实例的属性。以 driver 实例来讲,默认不存在 driver 的实例属性,因此返回 * 对应的属性
    val instConfig = metricsConfig.getInstance(instance)
    // 匹配正则表达式 ,获取所有度量源更细粒度的实例及属性
    val sinkConfigs = metricsConfig.subProperties(instConfig, MetricsSystem.SINK_REGEX)

    sinkConfigs.foreach { kv =>
      val classPath = kv._2.getProperty("class")
      if (null != classPath) {
        try {
          // 使用实例的 class 属性,通过 Java 反射生成度量输出的实例。如果当前实例是 servlet,则由 metricsServlet 持有此 servlet 的引用,否则将度量输出实例注册到数组缓冲 sinks 中
          val sink = Utils.classForName(classPath)
            .getConstructor(classOf[Properties], classOf[MetricRegistry], classOf[SecurityManager])
            .newInstance(kv._2, registry, securityMgr)
          // 启动 sinks 中的全部度量输出实例
          if (kv._1 == "servlet") {
            metricsServlet = Some(sink.asInstanceOf[MetricsServlet])
          } else {
            sinks += sink.asInstanceOf[Sink]
          }
        } catch {
          case e: Exception =>
            logError("Sink class " + classPath + " cannot be instantiated")
            throw e
        }
      }
    }
  }
}

输出提交协调器 简析

当 Spark 应用程序使用了 SparkSQL (包括 Hive)或者需要将任务的输出保存到 HDFS 时,就会用到输出提交协调器OutputCommitCoordinator,OutputCommitCoordinator 将决定任务是否可以提交输出到 HDFS

无论是 Driver 还是 Executor,在 SparkEnv 中都包含了子组件 OutputCommitCoordinator。在 Driver 上注册了OutputCommitCoordinatorEndpoint,所有 Executor 上的 OutputCommitCoordinator 都是通过 OutputCommitCoordinatorEndpoint 的RpcEndpointRef 来询问 Driver 上的 OutputCommitCoordinator,是否能够将输出提交到 HDFS

// 新建OutputCommitCoordinator实例    
val outputCommitCoordinator = mockOutputCommitCoordinator.getOrElse {
      new OutputCommitCoordinator(conf, isDriver)
    }
// 如果当前实例是 Driver,则创建 OutputCommitCoordinatorEndpoint,并且注册到 Dispatcher 中,注册名为OutputCommitCoordinator
// 如果当前应用程序是 Executor,则从远端 Driver 实例的 NettyRpcEnv的Dispatcher 中查找OutputCommitCoordinatorEndpoint 的引用

    val outputCommitCoordinatorRef = registerOrLookupEndpoint("OutputCommitCoordinator",
      new OutputCommitCoordinatorEndpoint(rpcEnv, outputCommitCoordinator))
// // 无论是 Driver 还是 Executor,最后都由 OutputCommitCoordinator 的属性 coordinatorRef 持有OutputCommitCoordinatorEndpoint 的引用
    outputCommitCoordinator.coordinatorRef = Some(outputCommitCoordinatorRef)
OutputCommitCoordinatorEndpoint 的实现简析
  private[spark] class OutputCommitCoordinatorEndpoint(
      override val rpcEnv: RpcEnv, outputCommitCoordinator: OutputCommitCoordinator)
    extends RpcEndpoint with Logging {

    logDebug("init") // force eager creation of logger

    override def receive: PartialFunction[Any, Unit] = {
      case StopCoordinator =>
        logInfo("OutputCommitCoordinator stopped!")
        stop()
    }

    override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
      case AskPermissionToCommitOutput(stage, stageAttempt, partition, attemptNumber) =>
        context.reply(
          outputCommitCoordinator.handleAskPermissionToCommit(stage, stageAttempt, partition,
            attemptNumber))
    }
  }

OutputCommitCoordinatorEndpoint 将接收两个消息

  • StopCoordinator : 此消息将停止 OutputCommitCoordinatorEndpoint
  • AskPermissionToCommitOutput : 此消息将通过 OutputCommitCoordinator 的 handleAskPermissionToCommit 方法处理,进而确认客户端是否有权限将输出提交到 HDFS
OutputCommitCoordinator 的实现简析

OutputCommitCoordinator 用于判定给定 Stage 的分区任务是否有权限将输出提交到 HDFS,并对同一分区任务的多次任务尝试(TaskAttempt) 进行协调

OutputCommitCoordinator 中有以下属性:

  • conf : 即SparkConf
  • isDriver : 当前节点是否是 Driver
  • coordinatorRef : 即 OutputCommitCoordinatorEndpoint的 NettyRpcEndpointRef 引用
  • NO_AUTHORIZED_COMMITTER : 值为-1的常量
  • authorizedCommittersByStage : 缓存 Stage 的各个分区的任务尝试。类型为 scala.collection.mutable.Map[Stageld,Array[TaskAttemptNumber]]

SparkEnv 构建

当SparkEnv内的所有组件都实例化完毕,将正式构造 SparkEnv

SparkEnv内部的成员属性:

  • isStopped : 当前 SparkEnv 是否停止的状态
  • pythonWorkers : 所有 python 实现的 Worker 的缓存
  • hadoopJobMetadata : HadoopRDD 进行任务切分时所需要的元数据的软引用。例如,HadoopFileRDD 将使用 hadoopJobMetadata缓存 JobConf 和 InputFormat
  • driverTmpDir : 如果当前 SparkEnv 处于 Driver 实例中,那么将创建 Driver 的临时目录
  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值