Spark广播变量底层的实现原理?
广播变量Executor端读取是push/put方式。。

大家好,我是老兵。

前面为大家介绍了一期Spark源码体系剖析,讲述了任务提交->Driver注册启动->SparkContext初始化->Executor启动->Task启动的全流程底层实现。

本期为spark源码系列第二讲:broadcast源码剖析。从一个面试题入手,作为后续序列展开的一道开胃菜。

1 引子

先看一段Spark程序代码,简单的读取数据并过滤:

val sc = new SparkContext(conf)
val list = List('hello')
val dataRDD = sc.textFile('./test.txt')

//读取变量
dataRDD.filter{x => x
     .contains(list)}.foreach{println}

在上一篇源码中我们知道Executor接收到TaskScheduler的taskset分发命令,根据rdd分区数ThreadPool中创建对应的Task线程,每个Task线程拉取并序列化代码,启动分布式计算。

spark ui 源码分析 spark源码解析_序列化

Spark在计算过程中的算子函数、变量都会由Driver分发到每台机器中,每个Task持有该变量的一个副本拷贝。可是这样会存在一个问题:

是否可以只在Executor中存放一次变量,所有Task共享?

Spark中提供了broadcast广播变量实现task共享功能。

2 Broadcast原理及作用

我们再来看下修改后的程序:

val sc = new SparkContext(conf)
val list = List('hello')

//定义broadcast变量
val broadcastVal = sc.broadcast(list)
val dataRDD = sc.textFile('./test.txt')

//broadcast变量读取
dataRDD.filter{x => x
     .contains(broadcastVal.value)}.foreach{println}

通过在Driver端使用broadcast()将一些大变量(List、Array)持久化,Executor根据broadcastid拉取本地缓存中的Broadcast对象,如果不存在,则尝试远程拉取Driver端持久化的那份Broadcast变量。

spark ui 源码分析 spark源码解析_spark_02

这样所有的Executor均存储了一份变量的备份,这个executor启动的task会共享这个变量,节省了通信的成本和服务器的资源。

注意不能广播RDD,因为RDD不存储数据;同时广播变量只能在Driver端定义和修改,Executor端只能读取。

3 Broadcast源码剖析

在了解broadcast广播变量基本原理后,我们来看看源码的实现逻辑。

3.1 TorrentBroadcast对象初始化

在程序中我们通过SparkContext调用broadcast()方法的时候,内部其实会直接尝试调用BroadcastFactory的newBroadcast()方法。

BroadcastFactory的默认实现是TorrentBroadcastFactory,在其newBroadcast()方法中,实际上就是new了一个TorrentBroadcast

这个TorrentBroadcast在初试化流程中,将会通过writeBlocks()方法准备将需要广播的变量写入到BlockManager中。

spark ui 源码分析 spark源码解析_序列化_03

BlockManager源码将会在后续剖析

3.2 Broadcast Driver端序列化

TorrentBroadcast初始化过程,主要做了两件事:广播变量Driver端备份Driver端序列化

3.2.1 Driver备份

通过调用BlockManager进行Driver端备份,广播变量会被写入到一个备份目录

在广播变量的持久化中,调用了BlockManager的putSingle()方法,广播变量被持久化到磁盘上,注意:这里是对广播对象在driver端的一个备份,并不是后续提供给executor

spark ui 源码分析 spark源码解析_大数据_04

3.2.2 Driver端序列化

Driver端在完成广播变量的备份后,需要本地再持久化一份数据。

通过调用BlockManager的putBytes奖对象切割成小文件,序列化并持久化到本地磁盘BlockManager上

Driver在写完全部广播对象后,会将其根据配置的单份文件的大小进行切割。在BlockManager的putBytes()方法内,底层调用DiskBlockManagergetFile()方法,将blockid进行hash的结果对应的输广播变量写入到子目录创建的文件中。

广播变量在driver端的过程,主要就是通过BlockManager将要广播的对象,切割成小文件持久化到了磁盘上。其中putBytes()putSingle()的区别在于,一个直接提供已经序列化好了的bytes流,一个直接提供对象来进行序列化。

3.3 Broadcast Executor端读取

3.3.1 Executor端拉取

广播变量在driver端完成了备份和序列化后,在executor将会通过广播变量操作类TorrentBroadcastvalue()方法获取具体的广播对象。

Executor在反序列化task的时候会反序列化broadcast,即调用value()拉取broadcast。

  • 1)在TorrentBroadcast的value()方法中,最后的实际调用实则是readBroadcastBlock()方法。
  • 2)在readBroadcastBlock()方法中,首先根据广播变量对应的broadcastid寻找本地是否已经存在这份广播变量,如果已经存在,会直接返回内存中对这份文件的缓存,否则,将会通过readBlocks()尝试远程拉取远程的广播变量。
  • 3)在readBlocks()中,TorrentBroadcast的实现,将会从各个持有小分片的远程BlockManager通过BlockTransferService进行远程广播数据的拉取。
  • 4) 在拉取之前,将会向driver端的BlockManagerMaster获取一个持有小文件的所有BlockManager,随机选择一个进行拉取(Driver/executor),保证文件分片后的高效拉取。

总结:
1.先从本地查找broadcastid,找不到则远程拉取
2.远程拉取可以从Driver/Executor中读取(第一次从Driver端拉取)
3. 拉取时连接Driver端的BlockStatus获取所有BlockManager位置,其中通过BlockTransferServices实现远程连接

3.3.2 Executor端持久化

Executor不断地从远程或本地拉取BlockManger中的广播变量数据,进行合并持久化操作。

通过TorrentBroadcast实现广播变量合并且将结果缓存到内存中,同时持久化并产生broadcastid,缓存目的是后续使用的时候可以高效返回。

随后Executor通知BlockMangerMaster并上报broadcastid以便其他executor访问。