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在计算过程中的算子函数、变量都会由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变量。
这样所有的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
中。
BlockManager源码将会在后续剖析
3.2 Broadcast Driver端序列化
TorrentBroadcast初始化过程,主要做了两件事:广播变量Driver端备份
和Driver端序列化
。
3.2.1 Driver备份
通过调用
BlockManager
进行Driver端备份,广播变量会被写入到一个备份目录
在广播变量的持久化中,调用了BlockManager的putSingle()
方法,广播变量被持久化到磁盘上,注意:这里是对广播对象在driver端的一个备份,并不是后续提供给executor
。
3.2.2 Driver端序列化
Driver端在完成广播变量的备份后,需要本地再持久化
一份数据。
通过调用BlockManager的
putBytes
奖对象切割成小文件,序列化并持久化到本地磁盘BlockManager上
Driver在写完全部广播对象后,会将其根据配置的单份文件的大小进行切割。在BlockManager的putBytes()方法内,底层调用DiskBlockManager
的getFile()
方法,将blockid
进行hash
的结果对应的输广播变量写入到子目录创建的文件中。
广播变量在driver端的过程,主要就是通过BlockManager
将要广播的对象,切割成小文件持久化到了磁盘上。其中putBytes()
与putSingle()
的区别在于,一个直接提供已经序列化好了的bytes流,一个直接提供对象来进行序列化。
3.3 Broadcast Executor端读取
3.3.1 Executor端拉取
广播变量在driver端完成了备份和序列化后,在executor将会通过广播变量操作类TorrentBroadcast
的value()
方法获取具体的广播对象。
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访问。