spark broadcast广播原理优缺点示例源码权威讲解

spark broadcast广播原理优缺点示例源码权威讲解

文章目录

  • spark broadcast广播原理优缺点示例源码权威讲解
  • 广播原理
  • 适用场景
  • 缺点
  • 示例
  • 源码
    • broadcast方法
    • 基础类Broadcast抽象类
    • 实现类TorrentBroadcast
      • 内部版本广播方法broadcastInternal
    • broadcastManager初始化和创建广播对象
    • 初始化
    • 创建广播变量
  • 源码拓展
    • BroadcastManager对象
    • BroadcastFactory接口
    • TorrentBroadcastFactory
    • BitTorrent 协议
      • BitTorrent 的工作原理
      • 块链技术
      • 安全性
      • 总结
  • 参考链接

广播原理

Spark广播(broadcast)的原理是通过将一个只读变量从驱动程序发送到集群上的所有工作节点,以便在运行任务时能够高效地访问这个变量。广播变量只会被发送一次,并且在工作节点上缓存,以供后续任务重用。

下面是Spark广播的实现方法:

  1. 驱动程序将要广播的变量划分为小块,并对每个块进行序列化。
  2. 驱动程序将序列化的块发送给各个工作节点。
  3. 每个工作节点接收到序列化的块后,将其反序列化并存储在内存中。
  4. 在执行任务时,每个工作节点可以直接访问已经缓存的广播变量,而不需要从驱动程序再次获取。

这种方式可以避免在任务执行期间多次传输相同的数据,从而提高性能和效率。

在Spark中,广播变量的实现主要依赖于DriverEndpoint和ExecutorEndpoint之间的通信机制。具体来说,当驱动程序将广播变量发送给工作节点时,它会使用BlockManager将序列化的块存储在内存中,并将块的元数据注册到BlockManagerMaster。然后,当工作节点执行任务时,它会向BlockManagerMaster请求获取广播变量的块,并从本地BlockManager中获取这些块的数据。这样,每个工作节点都可以在本地快速访问广播变量的数据。

总结起来,Spark广播的实现涉及驱动程序对广播变量进行序列化和发送,以及工作节点接收、反序列化和缓存广播变量的块。这种机制有效地将只读数据分发到集群上的所有工作节点,提高了任务执行的性能和效率。

适用场景

广播变量在以下场景中非常有用:

  1. 广播较大的只读数据集:当需要在多个任务中共享一个较大的只读数据集时,广播变量可以避免将该数据集复制到每个任务中。这样可以减少网络传输和内存消耗。

  2. 提高任务执行效率:如果一个任务需要频繁地使用相同的只读数据,通过广播变量可以将数据缓存在工作节点上,避免重复传输数据,从而提高任务的执行效率。

  3. 减少数据传输开销:广播变量将只读数据发送到工作节点一次,并在本地进行缓存,避免了多次传输相同的数据,减少了网络开销。

  4. 避免内存溢出:对于大规模的只读数据集,将其广播到工作节点并在本地缓存可以避免驱动程序的内存溢出问题。

  5. 共享全局配置信息:如果有全局的配置信息需要在不同任务之间共享,可以使用广播变量将其发送到工作节点,方便任务访问。

总之,广播变量适用于需要在多个任务之间共享只读数据,并且能够提供更高效的数据访问和减少网络传输开销的情况。通过使用广播变量,可以提高Spark应用程序的性能和效率。

缺点

虽然广播在分布式计算中有很多优点,但它也存在一些缺点:

  1. 内存消耗:广播变量需要将数据集复制到每个工作节点的内存中进行缓存。对于较大的数据集,这可能导致内存消耗较高,特别是当集群规模较大时。

  2. 延迟问题:由于广播变量需要将数据集发送到每个工作节点并进行缓存,所以在开始任务之前可能会有一定的延迟。这可能会对实时性要求较高的应用程序产生影响。

  3. 传输开销:广播变量的数据需要通过网络传输到工作节点,并且每个节点都需要接收和存储这些数据。对于大规模数据集,传输开销可能会比较大,特别是在网络带宽有限的情况下。

  4. 只读限制:广播变量是只读的,无法在任务执行过程中进行修改。如果需要对数据进行更新或变换,广播变量可能不适合。

  5. 需要额外管理:使用广播变量需要在驱动程序中显式创建和管理,包括序列化、发送和缓存。这增加了编码和维护的复杂性。

因此,在使用广播变量时需要考虑其局限性和适用场景。如果数据集较大,实时性要求高,或者需要频繁修改数据,可能需要考虑其他替代方案来避免广播的缺点。

示例

import org.apache.spark.{SparkConf, SparkContext}

object BroadcastExample {
  def main(args: Array[String]): Unit = {
    // 创建SparkConf对象
    val conf = new SparkConf().setAppName("Broadcast Example").setMaster("local[*]")

    // 创建SparkContext对象
    val sc = new SparkContext(conf)

    try {
      // 创建要广播的只读数据集
      val data = Map("A" -> 1, "B" -> 2, "C" -> 3)
      val broadcastData = sc.broadcast(data)

      // 创建RDD并在任务中访问广播变量
      val rdd = sc.parallelize(Seq("A", "B", "C"))
      val result = rdd.map(key => (key, broadcastData.value.getOrElse(key, -1)))

      // 打印结果
      result.foreach(println)
    } finally {
      // 关闭SparkContext对象
      sc.stop()
    }
  }
}

源码

broadcast方法

功能:将只读变量广播到集群,返回一个Broadcast对象以在分布式函数中进行读取变量将仅发送一次到每个执行器,同时调用了内部的方法broadcastInternal

/**
 * 将只读变量广播到集群,返回一个 [[org.apache.spark.broadcast.Broadcast]] 对象以在分布式函数中进行读取。
 * 变量将仅发送一次到每个执行器。
 *
 * @param value 要广播到 Spark 节点的值
 * @return `Broadcast` 对象,一个在每台机器上缓存的只读变量
 */
def broadcast[T: ClassTag](value: T): Broadcast[T] = {
  broadcastInternal(value, serializedOnly = false)
}

基础类Broadcast抽象类

Broadcast 是 Spark 中的一个广播变量类。广播变量允许程序员在每台机器上缓存一个只读的变量,而不是将它与任务一起传输。通过使用广播变量,可以以高效的方式为每个节点提供大型输入数据集的副本。

Broadcast 类的构造函数接收一个唯一标识符 id,用于标识广播变量。

Broadcast 类是一个抽象类,有以下几个主要方法:

  • value 方法:获取广播变量的值。
  • unpersist 方法:异步删除执行器上此广播变量的缓存副本。可以选择阻塞等待操作完成。
  • destroy 方法:销毁与此广播变量相关的所有数据和元数据。一旦广播变量被销毁,就不能再次使用它。也可以选择阻塞等待操作完成。

Broadcast 类还定义了一些受保护的方法,用于实际获取广播变量的值、取消持久化广播变量的值以及销毁广播变量的状态。

Broadcast 类还具有 _isValid_destroySite 两个私有变量,分别表示广播变量是否有效(即尚未销毁)以及销毁广播变量的位置信息。

总体来说,Broadcast 类提供了管理广播变量的功能,并确保广播变量的正确使用和销毁。

/**
 * 广播变量。广播变量允许程序员在每台机器上缓存一个只读变量,而不是将其与任务一起传输。它们可以用于以高效的方式为每个节点提供大型输入数据集的副本。
 * Spark 还尝试使用高效的广播算法分发广播变量,以减少通信成本。
 *
 * 广播变量是通过调用 [[org.apache.spark.SparkContext#broadcast]] 从变量 `v` 创建的。
 * 广播变量是对 `v` 的包装,可以通过调用 `value` 方法来访问其值。下面的解释器会话显示了这一点:
 *
 * {{{
 * scala> val broadcastVar = sc.broadcast(Array(1, 2, 3))
 * broadcastVar: org.apache.spark.broadcast.Broadcast[Array[Int]] = Broadcast(0)
 *
 * scala> broadcastVar.value
 * res0: Array[Int] = Array(1, 2, 3)
 * }}}
 *
 * 创建广播变量后,在集群上运行的任何函数中都应该使用广播变量,而不是值 `v`,以便 `v` 不会多次发送到节点。
 * 此外,在广播之后不应修改对象 `v`,以确保所有节点获得相同的广播变量的值(例如,如果稍后将变量发送到新节点)。
 *
 * @param id 广播变量的唯一标识符。
 * @tparam T 广播变量中包含的数据的类型。
 */
abstract class Broadcast[T: ClassTag](val id: Long) extends Serializable with Logging {

  /**
   * 表示广播变量是否有效(即尚未销毁)的标志。
   */
  @volatile private var _isValid = true

  private var _destroySite = ""

  /** 获取广播变量的值。 */
  def value: T = {
    assertValid()
    getValue()
  }

  /**
   * 异步删除执行器上此广播的缓存副本。
   * 如果在调用此方法后继续使用广播变量,则需要将其重新发送到每个执行器。
   */
  def unpersist(): Unit = {
    unpersist(blocking = false)
  }

  /**
   * 删除执行器上此广播的缓存副本。
   * 如果在调用此方法后继续使用广播变量,则需要将其重新发送到每个执行器。
   * @param blocking 是否阻塞,直到取消持久化完成
   */
  def unpersist(blocking: Boolean): Unit = {
    assertValid()
    doUnpersist(blocking)
  }


  /**
   * 销毁与此广播变量相关的所有数据和元数据。请谨慎使用此方法;一旦销毁了广播变量,就无法再次使用它。
   */
  def destroy(): Unit = {
    destroy(blocking = false)
  }

  /**
   * 销毁与此广播变量相关的所有数据和元数据。请谨慎使用此方法;一旦销毁了广播变量,就无法再次使用它。
   * @param blocking 是否阻塞,直到销毁完成
   */
  private[spark] def destroy(blocking: Boolean): Unit = {
    assertValid()
    _isValid = false
    _destroySite = Utils.getCallSite().shortForm
    logInfo("正在销毁 %s(来自 %s)".format(toString, _destroySite))
    doDestroy(blocking)
  }

  /**
   * 此广播变量是否可用。一旦从驱动程序中删除了持久状态,这个值应该为 false。
   */
  private[spark] def isValid: Boolean = {
    _isValid
  }

  /**
   * 实际获取广播的值。Broadcast 类的具体实现必须定义自己的获取值的方法。
   */
  protected def getValue(): T

  /**
   * 在执行器上异步取消持久化广播的值。Broadcast 类的具体实现必须定义自己的取消持久化逻辑。
   */
  protected def doUnpersist(blocking: Boolean): Unit

  /**
   * 实际销毁与此广播变量相关的所有数据和元数据。Broadcast 类的实现必须定义自己的销毁状态的逻辑。
   */
  protected def doDestroy(blocking: Boolean): Unit

  /** 检查此广播变量是否有效。如果无效,则抛出异常。 */
  protected def assertValid(): Unit = {
    if (!_isValid) {
      throw new SparkException(
        "在销毁后尝试使用 %s(%s)".format(toString, _destroySite))
    }
  }

  override def toString: String = "Broadcast(" + id + ")"
}

实现类TorrentBroadcast

TorrentBroadcast 是使用类似 BitTorrent 协议实现的 Broadcast 的具体实现(目前spark中只有一种实现)。它继承自 Broadcast 类,并提供以下功能:

  • 将对象分成多个块并将这些块存储在驱动程序的块管理器中
  • 在每个执行器上,首先尝试从其块管理器获取对象。如果不存在,则使用远程获取从驱动程序和/或其他执行器获取小块。获取到块后,将块放入自己的块管理器中,以便其他执行器可以获取。
  • 这样可以防止驱动程序成为发送多个副本的广播数据的瓶颈。
  • 当初始化时,TorrentBroadcast 从 SparkEnv 获取配置

TorrentBroadcast 包含以下主要成员变量和方法:

  • _value:在执行器上的广播对象的值。通过调用 readBroadcastBlock 方法从驱动程序和/或其他执行器读取块来重建该值。
  • compressionCodec:用于压缩的压缩编解码器。
  • blockSize:每个块的大小,默认为4MB
  • isLocalMaster:是否在本地模式下执行。
  • checksumEnabled:是否生成块的校验和。
  • writeBlocks(value: T): Int:将对象分成多个块并将这些块存储在块管理器中
  • readBlocks(): Array[BlockData]:从驱动程序和/或其他执行器获取 torrent 块
  • readBroadcastBlock(): T:读取广播块,重建广播对象的值。
  • unpersist(id: Long, removeFromDriver: Boolean, blocking: Boolean): Unit:从执行器中移除与指定 ID 相关的所有持久化块。

TorrentBroadcast 通过将广播数据分成小块并使用类似 BitTorrent 的协议进行分布式传输,以提高广播性能和可靠性。它允许在集群中高效地广播大量数据,并减少了驱动程序的负载

/**
 * 使用类似 BitTorrent 的协议实现的 [[org.apache.spark.broadcast.Broadcast]]。
 *
 * 具体机制如下:
 *
 * 驱动程序将序列化对象分成小块,并将这些块存储在驱动程序的块管理器中。
 *
 * 在每个执行器上,执行器首先尝试从自己的块管理器获取对象。如果不存在,则使用远程获取从驱动程序和/或其他执行器获取小块。获取到块后,将块放入自己的块管理器中,以便其他执行器可以获取。
 *
 * 这样可以防止驱动程序成为发送多个副本的广播数据(每个执行器一个)的瓶颈。
 *
 * 初始化时,TorrentBroadcast 对象会读取 SparkEnv.get.conf。
 *
 * @param obj 要广播的对象
 * @param id 广播变量的唯一标识符
 * @param serializedOnly 如果为 true,则不在驱动程序上缓存未序列化的值
 */
private[spark] class TorrentBroadcast[T: ClassTag](obj: T, id: Long, serializedOnly: Boolean)
  extends Broadcast[T](id) with Logging with Serializable {

  /**
   * 执行器上的广播对象的值。这是通过 [[readBroadcastBlock]] 方法从驱动程序和/或其他执行器读取块来重建的值。
   *
   * 在驱动程序上,如果需要该值,则会从块管理器中进行延迟读取。我们使用软引用来进行持有,以便在需要时可以进行垃圾回收,因为我们始终可以在将来重建。对于 `serializedOnly = true` 的内部广播变量,我们使用弱引用来更积极地回收值。
   */
  @transient private var _value: Reference[T] = _

  /** 压缩编解码器的选择,如果禁用压缩则为 None */
  @transient private var compressionCodec: Option[CompressionCodec] = _
  /** 每个块的大小,默认值为4MB。只有广播器会读取这个值。 */
  @transient private var blockSize: Int = _
  /** 是否处于本地模式 */
  @transient private var isLocalMaster: Boolean = _

  /** 是否生成块的校验和 */
  private var checksumEnabled: Boolean = false

  private def setConf(conf: SparkConf): Unit = {
    compressionCodec = if (conf.get(config.BROADCAST_COMPRESS)) {
      Some(CompressionCodec.createCodec(conf))
    } else {
      None
    }
    // 注意:使用 getSizeAsKb(而不是 bytes)来保持兼容性(如果未提供单位)
    blockSize = conf.get(config.BROADCAST_BLOCKSIZE).toInt * 1024
    checksumEnabled = conf.get(config.BROADCAST_CHECKSUM)
    isLocalMaster = Utils.isLocalMaster(conf)
  }
  setConf(SparkEnv.get.conf)

  private val broadcastId = BroadcastBlockId(id)

  /** 广播变量包含的块总数 */
  private val numBlocks: Int = writeBlocks(obj)

  /** 所有块的校验和 */
  private var checksums: Array[Int] = _

  override protected def getValue() = synchronized {
    val memoized: T = if (_value == null) null.asInstanceOf[T] else _value.get
    if (memoized != null) {
      memoized
    } else {
      val newlyRead = readBroadcastBlock()
      _value = if (serializedOnly) {
        new WeakReference[T](newlyRead)
      } else {
        new SoftReference[T](newlyRead)
      }
      newlyRead
    }
  }

  private def calcChecksum(block: ByteBuffer): Int = {
    val adler = new Adler32()
    if (block.hasArray) {
      adler.update(block.array, block.arrayOffset + block.position(), block.limit()
        - block.position())
    } else {
      val bytes = new Array[Byte](block.remaining())
      block.duplicate.get(bytes)
      adler.update(bytes)
    }
    adler.getValue.toInt
  }

  /**
   * 将对象分成多个块并将这些块放入块管理器中。
   *
   * @param value 要分割的对象
   * @return 广播变量被分割成的块数
   */
  private def writeBlocks(value: T): Int = {
    import StorageLevel._
    val blockManager = SparkEnv.get.blockManager
    if (serializedOnly && !isLocalMaster) {
      // SPARK-39983:在创建内部广播变量(如哈希关系广播)时,不要将广播值存储在驱动程序的块管理器中:
      // 我们不期望在驱动程序上读取内部广播变量的值,因此跳过存储可以减少驱动程序的内存压力,
      // 因为我们不会添加一个长期存在的对广播对象的引用。但是,这个优化不能应用于本地模式(因为任务可能在驱动程序上运行)。
      // 为了防止在驱动程序上访问内部广播变量时性能退化,我们使用弱引用来保存广播值:
      _value = new WeakReference[T](value)
    } else {
      // 在驱动程序中存储广播变量的副本,以便在驱动程序上运行的任务不会创建广播变量值的重复副本。
      if (!blockManager.putSingle(broadcastId, value, MEMORY_AND_DISK, tellMaster = false)) {
        throw new SparkException(s"Failed to store $broadcastId in BlockManager")
      }
    }
    try {
      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 " +
            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
    }
  }

  /** 从驱动程序和/或其他执行器获取 torrent 块。 */
  private def readBlocks(): Array[BlockData] = {
    // 获取数据块的片段。注意,所有这些片段都存储在块管理器中并报告给驱动程序,
    // 因此其他执行器也可以从该执行器中提取这些片段。
    val blocks = new Array[BlockData](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")
      // 首先尝试使用 getLocalBytes,因为先前获取广播块的尝试可能已经获取了一些块。
      // 在这种情况下,一些块会在本地(在该执行器上)可用。
      bm.getLocalBytes(pieceId) match {
        case Some(block) =>
          blocks(pid) = block
          releaseBlockManagerLock(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)}")
                }
              }
              // 从远程执行器/驱动程序的块管理器中找到了块,因此将该块放入此执行器的块管理器中。
              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) = new ByteBufferBlockData(b, true)
            case None =>
              throw new SparkException(s"Failed to get $pieceId of $broadcastId")
          }
      }
    }
    blocks
  }

  /**
   * 在执行器上移除与此 Torrent 广播相关联的所有持久状态。
   */
  override protected def doUnpersist(blocking: Boolean): Unit = {
    TorrentBroadcast.unpersist(id, removeFromDriver = false, blocking)
  }

  /**
   * 在执行器和驱动程序上移除与此 Torrent 广播相关联的所有持久状态。
   */
  override protected def doDestroy(blocking: Boolean): Unit = {
    TorrentBroadcast.unpersist(id, removeFromDriver = true, blocking)
  }

  /** JVM 在序列化此对象时使用。 */
  private def writeObject(out: ObjectOutputStream): Unit = Utils.tryOrIOException {
    assertValid()
    out.defaultWriteObject()
  }

  private def readBroadcastBlock(): T = Utils.tryOrIOException {
    TorrentBroadcast.torrentBroadcastLock.withLock(broadcastId) {
      // 因为我们只是基于 `broadcastId` 锁定,所以在使用 `broadcastCache` 时,
      // 我们应该只涉及 `broadcastId`。
      val broadcastCache = SparkEnv.get.broadcastManager.cachedValues

      Option(broadcastCache.get(broadcastId)).map(_.asInstanceOf[T]).getOrElse {
        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]
              releaseBlockManagerLock(broadcastId)

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

              x
            } else {
              throw new SparkException(s"Failed to get locally stored broadcast data: $broadcastId")
            }
          case None =>
            val estimatedTotalSize = Utils.bytesToString(numBlocks.toLong * blockSize)
            logInfo(s"Started reading broadcast variable $id with $numBlocks pieces " +
              s"(estimated total size $estimatedTotalSize)")
            val startTimeNs = System.nanoTime()
            val blocks = readBlocks()
            logInfo(s"Reading broadcast variable $id took ${Utils.getUsedTimeNs(startTimeNs)}")

            try {
              val obj = TorrentBroadcast.unBlockifyObject[T](
                blocks.map(_.toInputStream()), SparkEnv.get.serializer, compressionCodec)

              if (!serializedOnly || isLocalMaster || Utils.isInRunningSparkTask) {
                // 将合并后的副本存储在块管理器中,以便此执行器上的其他任务无需重新获取它。
                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 releaseBlockManagerLock(blockId: BlockId): Unit = {
    val blockManager = SparkEnv.get.blockManager
    Option(TaskContext.get()) match {
      case Some(taskContext) =>
        taskContext.addTaskCompletionListener[Unit](_ => blockManager.releaseLock(blockId))
      case None =>
        // 这只会发生在驱动程序上,在驱动程序上可能会在没有运行任务的情况下访问广播变量
        // (例如在计算 rdd.partitions() 时)。为了允许广播变量进行垃圾回收,我们需要在这里释放引用,
        // 这略微存在一些风险,但从技术上来说是可以的,因为广播变量不存储在堆外内存中。
        blockManager.releaseLock(blockId)
    }
  }

  // 是否缓存了未序列化的值。用于测试。
  private[spark] def hasCachedValue: Boolean = {
    TorrentBroadcast.torrentBroadcastLock.withLock(broadcastId) {
      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]
          releaseBlockManagerLock(broadcastId)
          x != null
        case _ => false
      }
    }
  }
}


private object TorrentBroadcast extends Logging {

  /**
   * 一个 [[KeyLock]],其键是 [[BroadcastBlockId]],以确保只有一个线程获取相同的 [[TorrentBroadcast]] 块。
   */
  private val torrentBroadcastLock = new KeyLock[BroadcastBlockId]

  def blockifyObject[T: ClassTag](
      obj: T,
      blockSize: Int,
      serializer: Serializer,
      compressionCodec: Option[CompressionCodec]): Array[ByteBuffer] = {
    val cbbos = new ChunkedByteBufferOutputStream(blockSize, ByteBuffer.allocate)
    val out = compressionCodec.map(c => c.compressedOutputStream(cbbos)).getOrElse(cbbos)
    val ser = serializer.newInstance()
    val serOut = ser.serializeStream(out)
    Utils.tryWithSafeFinally {
      serOut.writeObject[T](obj)
    } {
      serOut.close()
    }
    cbbos.toChunkedByteBuffer.getChunks()
  }

  def unBlockifyObject[T: ClassTag](
      blocks: Array[InputStream],
      serializer: Serializer,
      compressionCodec: Option[CompressionCodec]): T = {
    require(blocks.nonEmpty, "Cannot unblockify an empty array of blocks")
    val is = new SequenceInputStream(blocks.iterator.asJavaEnumeration)
    val in: InputStream = compressionCodec.map(c => c.compressedInputStream(is)).getOrElse(is)
    val ser = serializer.newInstance()
    val serIn = ser.deserializeStream(in)
    val obj = Utils.tryWithSafeFinally {
      serIn.readObject[T]()
    } {
      serIn.close()
    }
    obj
  }

  /**
   * 移除与此 torrent 广播相关联的所有持久化块。
   * 如果 removeFromDriver 为 true,则还会在驱动程序上移除这些持久化块。
   */
  def unpersist(id: Long, removeFromDriver: Boolean, blocking: Boolean): Unit = {
    logDebug(s"Unpersisting TorrentBroadcast $id")
    SparkEnv.get.blockManager.master.removeBroadcast(id, removeFromDriver, blocking)
  }
}

内部版本广播方法broadcastInternal

该方法是spark内部版本的广播 - 将只读变量广播到集群,变量将仅发送一次到每个执行器。该方法中使用了broadcastManager对象中的newBroadcast创建广播变量

/**
 * 内部版本的广播 - 将只读变量广播到集群,返回一个 [[org.apache.spark.broadcast.Broadcast]] 对象以在分布式函数中进行读取。
 * 变量将仅发送一次到每个执行器。
 *
 * @param value 要广播到 Spark 节点的值
 * @param serializedOnly 如果为 true,则不会在驱动程序上缓存未序列化的值
 * @return `Broadcast` 对象,一个在每台机器上缓存的只读变量
 */
private[spark] def broadcastInternal[T: ClassTag](
    value: T,
    serializedOnly: Boolean): Broadcast[T] = {
  assertNotStopped()
  require(!classOf[RDD[_]].isAssignableFrom(classTag[T].runtimeClass),
    "Can not directly broadcast RDDs; instead, call collect() and broadcast the result.")
  val bc = env.broadcastManager.newBroadcast[T](value, isLocal, serializedOnly)
  val callSite = getCallSite
  logInfo("Created broadcast " + bc.id + " from " + callSite.shortForm)
  cleaner.foreach(_.registerBroadcastForCleanup(bc))
  bc
}

broadcastManager初始化和创建广播对象

private[spark] class BroadcastManager(
    val isDriver: Boolean, conf: SparkConf) extends Logging {

  private var initialized = false
  private var broadcastFactory: BroadcastFactory = null

  initialize()

  // 在使用 Broadcast 之前由 SparkContext 或 Executor 调用
  private def initialize(): Unit = {
    synchronized {
      if (!initialized) {
        broadcastFactory = new TorrentBroadcastFactory
        broadcastFactory.initialize(isDriver, conf)
        initialized = true
      }
    }
  }   

  /**
   * 创建新的广播变量。
   *
   * @param value_ 要广播的值
   * @param isLocal 如果为 true,则广播将被限制在本地节点上
   * @param serializedOnly 如果为 true,则不会在驱动程序上缓存未序列化的值
   * @return 广播变量对象
   */
  def newBroadcast[T: ClassTag](
      value_ : T,
      isLocal: Boolean,
      serializedOnly: Boolean = false): Broadcast[T] = {
    val bid = nextBroadcastId.getAndIncrement()
    value_ match {
      case pb: PythonBroadcast =>
        // SPARK-28486: 将新广播变量的 id 附加到 PythonBroadcast 上,
        // 以便 PythonBroadcast 的底层数据文件可以根据此 id 映射到 BroadcastBlockId。
        // 请参阅 PythonBroadcast.readObject() 中对 id 的具体使用方式。
        pb.setBroadcastId(bid)

      case _ => // do nothing
    }
    broadcastFactory.newBroadcast[T](value_, isLocal, bid, serializedOnly)
  }   
}

初始化

**BroadcastManager构造函数会调用自身的initialize方法,创建一个TorrentBroadcastFactory实例.**对象在实例化时,会自动调用自身的writeBlocks,把数据写入blockManager:

使用了实现了BroadcastFactory接口的TorrentBroadcastFactory工厂方法。TorrentBroadcastFactory 是一个使用类似 BitTorrent 的协议来进行广播数据分布式传输的广播工厂。

创建广播变量

TorrentBroadcastFactory实例通过调用 newBroadcast() 方法创建新的 TorrentBroadcast对象即广播变量。 可以参考上文实现类

源码拓展

BroadcastManager对象

BroadcastManager 是 Spark 中负责管理广播变量的类。它包含以下主要功能:

  • 初始化广播工厂在第一次使用广播变量之前,会调用 initialize() 方法初始化广播工厂
  • 停止广播管理器:通过调用 stop() 方法停止广播管理器,释放相关资源。
  • 创建新的广播变量:通过调用 newBroadcast() 方法创建新的广播变量。该方法接受要广播的值、是否限制在本地节点上以及是否只序列化等参数,并返回一个广播变量对象。
  • 解除广播变量的广播:通过调用 unbroadcast() 方法解除已广播的广播变量。该方法接受广播变量的 ID、是否从驱动程序中删除以及是否阻塞等参数,并执行相应操作。

此外,BroadcastManager 还包含了一些内部变量,如下:

  • initialized:指示广播管理器是否已初始化的标志。
  • broadcastFactory:广播工厂对象,负责实际的广播操作。
  • nextBroadcastId:用于生成下一个广播变量的 ID 的原子长整型。
  • cachedValues:用于缓存已广播的值的映射。

总而言之,BroadcastManager 提供了广播变量的管理和操作功能,确保广播变量能够在集群中高效地分发和访问

private[spark] class BroadcastManager(
    val isDriver: Boolean, conf: SparkConf) extends Logging {

  private var initialized = false
  private var broadcastFactory: BroadcastFactory = null

  initialize()

  // 在使用 Broadcast 之前由 SparkContext 或 Executor 调用
  private def initialize(): Unit = {
    synchronized {
      if (!initialized) {
        broadcastFactory = new TorrentBroadcastFactory
        broadcastFactory.initialize(isDriver, conf)
        initialized = true
      }
    }
  }

  /**
   * 停止 BroadcastManager,释放资源。
   */
  def stop(): Unit = {
    broadcastFactory.stop()
  }

  private val nextBroadcastId = new AtomicLong(0)

  /**
   * 缓存已广播的值。
   */
  private[broadcast] val cachedValues =
    Collections.synchronizedMap(
      new ReferenceMap(ReferenceStrength.HARD, ReferenceStrength.WEAK)
        .asInstanceOf[java.util.Map[Any, Any]]
    )

  /**
   * 创建新的广播变量。
   *
   * @param value_ 要广播的值
   * @param isLocal 如果为 true,则广播将被限制在本地节点上
   * @param serializedOnly 如果为 true,则不会在驱动程序上缓存未序列化的值
   * @return 广播变量对象
   */
  def newBroadcast[T: ClassTag](
      value_ : T,
      isLocal: Boolean,
      serializedOnly: Boolean = false): Broadcast[T] = {
    val bid = nextBroadcastId.getAndIncrement()
    value_ match {
      case pb: PythonBroadcast =>
        // SPARK-28486: 将新广播变量的 id 附加到 PythonBroadcast 上,
        // 以便 PythonBroadcast 的底层数据文件可以根据此 id 映射到 BroadcastBlockId。
        // 请参阅 PythonBroadcast.readObject() 中对 id 的具体使用方式。
        pb.setBroadcastId(bid)

      case _ => // do nothing
    }
    broadcastFactory.newBroadcast[T](value_, isLocal, bid, serializedOnly)
  }

  /**
   * 解除广播变量的广播。
   *
   * @param id 广播变量的 id
   * @param removeFromDriver 如果为 true,则从驱动程序中删除广播变量
   * @param blocking 如果为 true,则阻塞直到解除广播完成
   */
  def unbroadcast(id: Long, removeFromDriver: Boolean, blocking: Boolean): Unit = {
    broadcastFactory.unbroadcast(id, removeFromDriver, blocking)
  }
}

BroadcastFactory接口

BroadcastFactory 是 Spark 中所有广播实现的接口,用于允许多个广播实现。它定义了以下方法:

  • initialize(isDriver: Boolean, conf: SparkConf): Unit:初始化广播工厂。
  • newBroadcast[T: ClassTag](value: T, isLocal: Boolean, id: Long, serializedOnly: Boolean = false): Broadcast[T]:创建新的广播变量。
  • unbroadcast(id: Long, removeFromDriver: Boolean, blocking: Boolean): Unit:解除广播变量的广播。
  • stop(): Unit:停止广播工厂,释放资源。

通过实现 BroadcastFactory 接口,可以自定义广播实现,并在 SparkContext 中使用相应的广播工厂来实例化广播变量。

/**
 * Spark 中所有广播实现的接口(允许多个广播实现)。
 * SparkContext 使用 BroadcastFactory 实现来为整个 Spark 作业实例化特定的广播。
 */
private[spark] trait BroadcastFactory {

  /**
   * 初始化广播工厂。
   *
   * @param isDriver 是否为驱动程序节点
   * @param conf Spark 配置
   */
  def initialize(isDriver: Boolean, conf: SparkConf): Unit

  /**
   * 创建新的广播变量。
   *
   * @param value 要广播的值
   * @param isLocal 是否处于本地模式(单个 JVM 进程)
   * @param id 表示此广播变量的唯一 ID
   * @param serializedOnly 如果为 true,则不会在驱动程序上缓存未序列化的值
   * @return `Broadcast` 对象,一个在每台机器上缓存的只读变量
   */
  def newBroadcast[T: ClassTag](
      value: T,
      isLocal: Boolean,
      id: Long,
      serializedOnly: Boolean = false): Broadcast[T]

  /**
   * 解除广播变量的广播。
   *
   * @param id 广播变量的 ID
   * @param removeFromDriver 如果为 true,则从驱动程序中删除广播变量
   * @param blocking 如果为 true,则阻塞直到解除广播完成
   */
  def unbroadcast(id: Long, removeFromDriver: Boolean, blocking: Boolean): Unit

  /**
   * 停止广播工厂,释放资源。
   */
  def stop(): Unit
}

TorrentBroadcastFactory

TorrentBroadcastFactory 是一个使用类似 BitTorrent 的协议来进行广播数据分布式传输的广播工厂。它实现了 BroadcastFactory 接口,并提供以下功能:

  • 创建新的 TorrentBroadcast 广播变量:通过调用 newBroadcast() 方法创建新的 TorrentBroadcast 广播变量。
  • 停止广播工厂:通过调用 stop() 方法停止广播工厂,释放相关资源。
  • 解除广播变量的广播:通过调用 unbroadcast() 方法解除已广播的广播变量。

TorrentBroadcastFactory 主要用于支持使用 BitTorrent-like 协议进行分布式传输的广播操作,以提高广播数据在集群中的传输效率和可靠性。

/**
 * 使用类似 BitTorrent 的协议来进行广播数据的分布式传输的 [[org.apache.spark.broadcast.Broadcast]] 实现。
 * 有关详细信息,请参见 [[org.apache.spark.broadcast.TorrentBroadcast]]。
 */
private[spark] class TorrentBroadcastFactory extends BroadcastFactory {

  override def initialize(isDriver: Boolean, conf: SparkConf): Unit = { }

  /**
   * 创建新的 TorrentBroadcast 广播变量。
   *
   * @param value_ 要广播的值
   * @param isLocal 是否处于本地模式(单个 JVM 进程)
   * @param id 表示此广播变量的唯一 ID
   * @param serializedOnly 如果为 true,则不会在驱动程序上缓存未序列化的值
   * @return TorrentBroadcast 对象,一个在每台机器上缓存的只读变量
   */
  override def newBroadcast[T: ClassTag](
      value_ : T,
      isLocal: Boolean,
      id: Long,
      serializedOnly: Boolean = false): Broadcast[T] = {
    new TorrentBroadcast[T](value_, id, serializedOnly)
  }

  override def stop(): Unit = { }

  /**
   * 移除与给定 ID 的 torrent 广播相关联的所有持久状态。
   * @param removeFromDriver 是否从驱动程序中删除状态
   * @param blocking 是否阻塞直到解除广播完成
   */
  override def unbroadcast(id: Long, removeFromDriver: Boolean, blocking: Boolean): Unit = {
    TorrentBroadcast.unpersist(id, removeFromDriver, blocking)
  }
}

BitTorrent 协议

BitTorrent 是一种流行的文件分享协议,它使用了一种名为 “块链” 的技术。块链技术通常用于比特币等加密货币,但在 BitTorrent 中,它用于分发大型文件。

BitTorrent 的工作原理

初始化: 当一个用户想要下载一个文件时,他首先创建一个 “种子” 文件,这个文件包含该文件的所有块的哈希列表。
查找: 下载者使用 BitTorrent 客户端软件查找其他下载者,并请求他们分享文件块。
交换: 下载者与其他下载者交换文件块。每个下载者不仅下载文件,还同时通过上传已下载的块来帮助其他下载者。
完整性: 每个块都有一个哈希值,用于验证块的完整性。如果某个块的哈希值不匹配,则该块被认为是无效的,需要重新下载。

块链技术

BitTorrent 使用块链来确保每个块的完整性。每个块都包含前一个块的哈希值,这使得整个文件的所有块形成了一个链。如果某个块被修改或损坏,它的哈希值将不再匹配,BitTorrent 客户端将自动从其他下载者那里请求一个新的块。

安全性

BitTorrent 协议不使用加密,这意味着在交换文件块时,你的数据可能被第三方监听。为了提高安全性,你可以使用一个加密的 BitTorrent 客户端,如 BitTorrent Secure。

总结

BitTorrent 协议是一种高效的文件分享协议,它使用块链技术来保证文件块的完整性和安全性。然而,由于其不加密的特点,它可能不适合传输敏感信息。

参考链接

你可能感兴趣的:(大数据,spark,spark,大数据)