Spark2.1.0——广播管理器BroadcastManager

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

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

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

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

BroadcastManager在其初始化的过程中就会调用自身的initialize方法,当initialize执行完毕,BroadcastManager就正式生效。BroadcastManager的initialize方法的实现见代码清单1。

代码清单1       BroadcastManager的初始化

  private def initialize() {
    synchronized {
      if (!initialized) {
        broadcastFactory = new TorrentBroadcastFactory
        broadcastFactory.initialize(isDriver, conf, securityManager)
        initialized = true
      }
    }
  }

根据代码清单1,initialize方法首先判断BroadcastManager是否已经初始化,以保证BroadcastManager只被初始化一次。新建TorrentBroadcastFactory作为BroadcastManager的广播工厂实例。之后调用TorrentBroadcastFactory的initialize方法对TorrentBroadcastFactory进行初始化[1]。最后将BroadcastManager自身标记为初始化完成状态。


注意:TorrentBroadcastFactory实现了BroadcastFactory特质。在Spark 1.x.x版本中,BroadcastManager的initialize方法是使用Java反射生成广播工厂实例broadcastFactory的,还可以通过配置属性spark.broadcast.factory指定BroadcastFactory特质的实现类,默认为org.apache.spark. broadcast.TorrentBroadcastFactory。从Spark 2.0.0版本开始,不再提供此Spark属性,属性成员broadcastFactory也固定为TorrentBroadcastFactory。


BroadcastManager中提供了三个方法,见代码清单2。

代码清单2       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)
  }

从代码清单2可以看到BroadcastManager的三个方法都分别代理了TorrentBroadcastFactory的对应方法,TorrentBroadcastFactory中提供的三个方法的实现见代码清单3。

代码清单3      TorrentBroadcastFactory提供的方法

  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)
  }

代码清单3中TorrentBroadcastFactory提供的三个方法,由于stop是空实现,所以我们只关注newBroadcast和unbroadcast两个方法。

根据代码清单3,我们知道TorrentBroadcastFactory的newBroadcast方法用于生成TorrentBroadcast实例,其作用为广播TorrentBroadcast中的value。表面看只是利用构造器生成了TorrentBroadcast实例,但是其效果远不止此。TorrentBroadcast对象包括以下属性:

  • compressionCodec:用于广播对象的压缩编解码器。可以设置spark.broadcast.compress属性为true启用,默认是启用的。compressionCodec的类型为《Spark内核设计的艺术》一书5.4节详细介绍过的CompressionCodec,而且最终采用的压缩算法与SerializerManager中的CompressionCodec是一致的。
  • blockSize:每个块的的大小。是个只读属性,可以使用spark.broadcast.blockSize属性进行配置,默认为4MB。
  • broadcastId:广播Id。broadcastId实际是样例类BroadcastBlockId,BroadcastBlockId由的实现如下:
case class BroadcastBlockId(broadcastId: Long, field: String = "") extends BlockId {
  override def name: String = "broadcast_" + broadcastId + (if (field == "") "" else "_" + field)
}

其中broadcastId是由BroadcastManager的原子变量nextBroadcastId自增产生。

  • checksumEnabled:是否给广播块生成校验和。可以通过spark.broadcast.checksum属性进行配置,默认为true。
  • checksums:用于存储每个广播块的校验和的数组。
  • numBlocks:广播变量包含的块的数量。numBlocks通过调用writeBlocks方法获得。由于numBlocks是个val修饰的不可变属性,因此在构造TorrentBroadcast实例的时候就会调用writeBlocks方法将广播对象写入存储体系。
  • _value:从Executor或者Driver上读取的广播块的值。_value是通过调用readBroadcastBlock方法获得的广播对象。由于_value是个lazy及val修饰的属性,因此在构造TorrentBroadcast实例的时候不会调用readBroadcastBlock方法,而是等到明确需要使用_value的值时。

广播对象的写操作

刚才说到在构造TorrentBroadcast实例的时候就会调用writeBlocks方法,其实现见代码清单4。

代码清单4       writeBlocks的实现

  private def writeBlocks(value: T): Int = {
    import StorageLevel._
    val blockManager = SparkEnv.get.blockManager
    if (!blockManager.putSingle(broadcastId, value, MEMORY_AND_DISK, tellMaster = false)) {
      throw new SparkException(s"Failed to store $broadcastId in BlockManager")
    }
    val blocks =
      TorrentBroadcast.blockifyObject(value, blockSize, SparkEnv.get.serializer, compressionCodec)
    if (checksumEnabled) {
      checksums = new Array[Int](blocks.length)
    }
    blocks.zipWithIndex.foreach { case (block, i) =>
      if (checksumEnabled) {
        checksums(i) = calcChecksum(block)
      }
      val pieceId = BroadcastBlockId(id, "piece" + i)
      val bytes = new ChunkedByteBuffer(block.duplicate())
      if (!blockManager.putBytes(pieceId, bytes, MEMORY_AND_DISK_SER, tellMaster = true)) {
        throw new SparkException(s"Failed to store $pieceId of $broadcastId in local BlockManager")
      }
    }
    blocks.length
  }

根据代码清单4,writeBlocks的执行步骤如下:

  1. 获取当前SparkEnv的BlockManager组件。
  2. 调用BlockManager的putSingle方法将广播对象写入本地的存储体系。当Spark以local模式运行时,则会将广播对象写入Driver本地的存储体系,以便于任务也可以在Driver上执行(由于MEMORY_AND_DISK对应的StorageLevel的_replication属性固定为1,因此此处只会将广播对象写入Driver或Executor本地的存储体系,有关BlockManager的putSingle方法及StorageLevel的内容将在第6章详细介绍)。
  3. 调用TorrentBroadcast的blockifyObject方法(实现很简单,感兴趣的读者可以自行查阅)将对象转换成一系列的块。每个块的大小由blockSize决定,使用当前SparkEnv中的JavaSerializer组件进行序列化,使用TorrentBroadcast自身的compressionCodec进行压缩。
  4. 如果需要给分片广播块生成校验和,则创建和第3步转换的块的数量一致的checksums数组。
  5. 对每个块进行如下处理:如果需要给分片广播块生成校验和,则给分片广播块生成校验和(calcChecksum方法的实现很简单,感兴趣的读者可以自行查阅)。给当前分片广播块生成分片的BroadcastBlockId,分片通过BroadcastBlockId的field属性区别,例如:piece0、piece1、…。调用BlockManager的putBytes方法将分片广播块以序列化方式写入Driver本地的存储体系(由于MEMORY_AND_DISK_SER对应的StorageLevel的_replication属性也固定为1,因此此处只会将分片广播块写入Driver或Executor本地本地的存储体系,有关BlockManager的putBytes方法及StorageLevel的内容将在第6章详细介绍)。
  6. 返回块的数量。

经过以上分析,最后用图1来更直观的表示广播对象的写入过程。

Spark2.1.0——广播管理器BroadcastManager_第1张图片 图1   广播对象的写入

广播对象的读操作

       前文提到,只有当TorrentBroadcast实例的_value属性值在需要的时候,才会调用readBroadcastBlock方法获取值,readBroadcastBlock的实现见代码清单5。

代码清单5       readBroadcastBlock的实现

  private def readBroadcastBlock(): T = Utils.tryOrIOException {
    TorrentBroadcast.synchronized {
      setConf(SparkEnv.get.conf)
      val blockManager = SparkEnv.get.blockManager
      blockManager.getLocalValues(broadcastId) match {
        case Some(blockResult) =>
          if (blockResult.data.hasNext) {
            val x = blockResult.data.next().asInstanceOf[T]
            releaseLock(broadcastId)
            x
          } else {
            throw new SparkException(s"Failed to get locally stored broadcast data: $broadcastId")
          }
        case None =>
          logInfo("Started reading broadcast variable " + id)
          val startTimeMs = System.currentTimeMillis()
          val blocks = readBlocks().flatMap(_.getChunks())
          logInfo("Reading broadcast variable " + id + " took" + Utils.getUsedTimeMs(startTimeMs))

          val obj = TorrentBroadcast.unBlockifyObject[T](
            blocks, SparkEnv.get.serializer, compressionCodec)
          val storageLevel = StorageLevel.MEMORY_AND_DISK
          if (!blockManager.putSingle(broadcastId, obj, storageLevel, tellMaster = false)) {
            throw new SparkException(s"Failed to store $broadcastId in BlockManager")
          }
          obj
      }
    }
  }

根据代码清单5,readBroadcastBlock的执行步骤如下:

  1. 获取当前SparkEnv的BlockManager组件。
  2. 调用BlockManager的getLocalValues方法从本地的存储系统中获取广播对象(即通过BlockManager的putSingle方法写入存储体系的广播对象),BlockManager的getLocalValues方法将在《Spark内核设计的艺术》一书的6.7.2节详细介绍。
  3. 如果从本地的存储体系中可以获取广播对象,则调用releaseLock方法(这个锁保证当块被一个运行中的任务使用时,不能被其他任务再次使用,但是当任务运行完成时,则应该释放这个锁)释放当前块的锁并返回此广播对象。
  4. 如果从本地的存储体系中没有获取到广播对象,那么说明数据是通过BlockManager的putBytes方法以序列化方式写入存储体系的。此时首先调用readBlocks方法从Driver或Executor的存储体系中获取广播块,然后调用TorrentBroadcast的unBlockifyObject方法(实现很简单,感兴趣的读者可以自行查阅)将一系列的分片广播块转换回原来的广播对象,最后再次调用BlockManager的putSingle方法将广播对象写入本地的存储体系以便于当前Executor的其他任务不用再次获取广播对象。

上文谈到调用readBlocks方法可以从Driver、Executor的存储体系中获取块,其实现见代码清单6。

代码清单6       readBlocks的实现

  private def readBlocks(): Array[ChunkedByteBuffer] = {
    val blocks = new Array[ChunkedByteBuffer](numBlocks)
    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")
      bm.getLocalBytes(pieceId) match {
        case Some(block) =>
          blocks(pid) = block
          releaseLock(pieceId)
        case None =>
          bm.getRemoteBytes(pieceId) match {
            case Some(b) =>
              if (checksumEnabled) {
                val sum = calcChecksum(b.chunks(0))
                if (sum != checksums(pid)) {
                  throw new SparkException(s"corrupt remote block $pieceId of $broadcastId:" +
                    s" $sum != ${checksums(pid)}")
                }
              }
              // We found the block from remote executors/driver's BlockManager, so put the block
              // in this executor's BlockManager.
              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(pid) = b
            case None =>
              throw new SparkException(s"Failed to get $pieceId of $broadcastId")
          }
      }
    }
    blocks
  }

根据代码清单6,readBlocks方法的执行步骤如下:

  1. 新建用于存储每个分片广播块的数组blocks,并获取当前SparkEnv的BlockManager组件。
  2. 对各个广播分片进行随机洗牌,避免对广播块的获取出现“热点”,提升性能。对洗牌后的各个广播分片依次执行3)至5)步的操作。
  3. 调用BlockManager的getLocalBytes方法从本地的存储体系中获取序列化的分片广播块,如果本地可以获取到,则将分片广播块放入blocks,并且调用releaseLock方法释放此分片广播块的锁。
  4. 如果本地没有,则调用BlockManager的getRemoteBytes方法从远端的存储体系中获取分片广播块。对于获取的分片广播块再次调用calcChecksum方法计算校验和,并将此校验和和调用writeBlocks方法时存入checksums数组的校验和进行比较。如果校验和不相同,说明块的数据有损坏,此时抛出异常。
  5. 如果校验和相同,则调用BlockManager的putBytes方法将分片广播块写入本地存储体系,以便于当前Executor的其他任务不用再次获取分片广播块。最后将分片广播块放入blocks。
  6. 返回blocks中的所有分片广播块。

经过以上的分析,最后用图2来更直观的表示广播对象的读取过程。

Spark2.1.0——广播管理器BroadcastManager_第2张图片 图2   广播对象的读取

广播对象的去持久化

根据代码清单3,我们知道TorrentBroadcastFactory的unbroadcast方法实际调用了TorrentBroadcast的unpersist方法对由id标记的广播对象去持久化。TorrentBroadcast的unpersist方法的实现,见代码清单7。

代码清单7       对广播对象去持久化

  def unpersist(id: Long, removeFromDriver: Boolean, blocking: Boolean): Unit = {
    logDebug(s"Unpersisting TorrentBroadcast $id")
    SparkEnv.get.blockManager.master.removeBroadcast(id, removeFromDriver, blocking)
  }

根据代码清单7,可以看到TorrentBroadcast的unpersist方法实际调用了BlockManager的子组件BlockManagerMaster的removeBroadcast方法来实现对广播对象去持久化,有关BlockManagerMaster的具体介绍请参阅《Spark内核设计的艺术》一书的6.8节。


[1] 笔者查阅TorrentBroadcastFactory的源码后,发现TorrentBroadcastFactory的Initialize方法实际是一个空实现,所以这里不作介绍。

你可能感兴趣的:(大数据,Scala,Spark,深入理解Spark)