def persist(newLevel: StorageLevel): this.type = { // StorageLevel不能随意更改 if (storageLevel != StorageLevel.NONE && newLevel != storageLevel) { throw new UnsupportedOperationException("Cannot change storage level of an RDD after it was already assigned a level") } sc.persistRDD(this) // Register the RDD with the ContextCleaner for automatic GC-based cleanup // 注册清理方法 sc.cleaner.foreach(_.registerRDDForCleanup(this)) storageLevel = newLevel this }
它调用SparkContext去缓存这个RDD,追杀下去。
private[spark] def persistRDD(rdd: RDD[_]) { persistentRdds(rdd.id) = rdd }
它居然是用一个HashMap来存的,具体看这个map的类型是TimeStampedWeakValueHashMap[Int, RDD[_]]类型。把存进去的值都隐式转换成WeakReference,然后加到一个内部的一个ConcurrentHashMap里面。这里貌似也没干啥,这是有个鸟蛋用。。大神莫喷,知道干啥用的人希望告诉我一下。
CacheManager
现在并没有保存,等到真正运行Task运行的时候才会去缓存起来。入口在Task的runTask方法里面,具体的我们可以看ResultTask,它调用了RDD的iterator方法。
final def iterator(split: Partition, context: TaskContext): Iterator[T] = { if (storageLevel != StorageLevel.NONE) { SparkEnv.get.cacheManager.getOrCompute(this, split, context, storageLevel) } else { computeOrReadCheckpoint(split, context) } }
一旦设置了StorageLevel,就要从SparkEnv的cacheManager取数据。
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
1、如果blockManager当中有,直接从blockManager当中取。
2、如果blockManager没有,就先用RDD的compute函数得到出来一个Iterable接口。
3、如果StorageLevel是只保存在硬盘的话,就把值存在blockManager当中,然后从blockManager当中取出一个Iterable接口,这样的好处是不会一次把数据全部加载进内存。
4、如果StorageLevel是需要使用内存的情况,就把结果添加到一个ArrayBuffer当中一次返回,另外在blockManager存上一份,下次直接从blockManager取。
对StorageLevel说明一下吧,贴一下它的源码。
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
大家注意看它那几个参数,useDisk_、useMemory_、useOffHeap_、deserialized_、replication_ 在具体的类型的时候是传的什么值。
下面我们的目标要放到blockManager。
BlockManager
BlockManager这个类比较大,我们从两方面开始看吧,putBytes和get方法。先从putBytes说起,之前说过Task运行结束之后,结果超过10M的话,会用BlockManager缓存起来。
env.blockManager.putBytes(blockId, serializedDirectResult, StorageLevel.MEMORY_AND_DISK_SER)
putBytes内部又掉了另外一个方法doPut,方法很大呀,先折叠起来。
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
从上面的的来看:
1、存储的时候按照不同的存储级别分了3种情况来处理:存在内存当中(包括MEMORY字样的),存在tachyon上(OFF_HEAP),只存在硬盘上(DISK_ONLY)。
2、存储完成之后会根据存储级别决定是否发送到别的节点,在名字上最后带2字的都是这种,2表示一个block会在两个节点上保存。
3、存储完毕之后,会向BlockManagerMaster汇报block的情况。
4、这里面的序列化其实是先压缩后序列化,默认使用的是LZF压缩,可以通过spark.io.compression.codec设定为snappy或者lzo,序列化方式通过spark.serializer设置,默认是JavaSerializer。
接下来我们再看get的情况。
val local = getLocal(blockId) if (local.isDefined) return local val remote = getRemote(blockId) if (remote.isDefined) return remote None
先从本地取,本地没有再去别的节点取,都没有,返回None。从本地取就不说了,怎么进怎么出。讲一下怎么从别的节点去,它们是一个什么样子的关系?
我们先看getRemote方法
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
这个方法包括两个步骤:
1、用blockId通过master的getLocations方法找到它的位置。
2、通过BlockManagerWorker.syncGetBlock到指定的节点获取数据。
ok,下面就重点讲BlockManager和BlockManagerMaster之间的关系,以及BlockManager之间是如何相互传输数据。
BlockManager与BlockManagerMaster的关系
BlockManager我们使用的时候是从SparkEnv.get获得的,我们观察了一下SparkEnv,发现它包含了我们运行时候常用的那些东东。那它创建是怎么创建的呢,我们找到SparkEnv里面的create方法,右键FindUsages,就会找到两个地方调用了,一个是SparkContext,另一个是Executor。在SparkEnv的create方法里面会实例化一个BlockManager和BlockManagerMaster。这里我们需要注意看BlockManagerMaster的实例化方法,里面调用了registerOrLookup方法。
def registerOrLookup(name: String, newActor: => Actor): ActorRef = { if (isDriver) { actorSystem.actorOf(Props(newActor), name = name) } else { val driverHost: String = conf.get("spark.driver.host", "localhost") val driverPort: Int = conf.getInt("spark.driver.port", 7077) Utils.checkHost(driverHost, "Expected hostname") val url = s"akka.tcp://spark@$driverHost:$driverPort/user/$name" val timeout = AkkaUtils.lookupTimeout(conf) Await.result(actorSystem.actorSelection(url).resolveOne(timeout), timeout) } }
所以从这里可以看出来,除了Driver之后的actor都是,都是持有的Driver的引用ActorRef。梳理一下,我们可以得出以下结论:
1、SparkContext持有一个BlockManager和BlockManagerMaster。
2、每一个Executor都持有一个BlockManager和BlockManagerMaster。
3、Executor和SparkContext的BlockManagerMaster通过BlockManagerMasterActor来通信。
接下来,我们看看BlockManagerMasterActor里的三组映射关系。
// 1、BlockManagerId和BlockManagerInfo的映射关系 private val blockManagerInfo = new mutable.HashMap[BlockManagerId, BlockManagerInfo] // 2、Executor ID 和 Block manager ID的映射关系 private val blockManagerIdByExecutor = new mutable.HashMap[String, BlockManagerId] // 3、BlockId和保存它的BlockManagerId的映射关系 private val blockLocations = new JHashMap[BlockId, mutable.HashSet[BlockManagerId]]
看到这三组关系,前面的getLocations方法不用看它的实现,我们都应该知道是怎么找了。
BlockManager相互传输数据
BlockManager之间发送数据和接受数据是通过BlockManagerWorker的syncPutBlock和syncGetBlock方法来实现。看BlockManagerWorker的注释,说是BlockManager的网络接口,采用的是事件驱动模型。
再仔细看这两个方法,它传输的数据包装成BlockMessage之后,通过ConnectionManager的sendMessageReliablySync方法来传输。
接下来的故事就是nio之间的发送和接收了,就简单说几点吧:
1、ConnectionManager内部实例化一个selectorThread线程来接收消息,具体请看run方法。
2、Connection发送数据的时候,是一次把消息队列的message全部发送,不是一个一个message发送,具体看SendConnection的write方法,与之对应的接收看ReceivingConnection的read方法。
3、read完了之后,调用回调函数ConnectionManager的receiveMessage方法,它又调用了handleMessage方法,handleMessage又调用了BlockManagerWorker的onBlockMessageReceive方法。传说中的事件驱动又出现了。
def processBlockMessage(blockMessage: BlockMessage): Option[BlockMessage] = { blockMessage.getType match { case BlockMessage.TYPE_PUT_BLOCK => { val pB = PutBlock(blockMessage.getId, blockMessage.getData, blockMessage.getLevel) putBlock(pB.id, pB.data, pB.level) None } case BlockMessage.TYPE_GET_BLOCK => { val gB = new GetBlock(blockMessage.getId) val buffer = getBlock(gB.id) Some(BlockMessage.fromGotBlock(GotBlock(gB.id, buffer))) } case _ => None } }
根据BlockMessage的类型进行处理,put类型就保存数据,get类型就从本地把block读出来返回给它。
注:BlockManagerMasterActor是存在于BlockManagerMaster内部,画在外面只是因为它在通信的时候起了关键的作用的,Executor上持有的BlockManagerMasterActor均是Driver节点的Actor的引用。
广播变量
先回顾一下怎么使用广播变量:
scala> val broadcastVar = sc.broadcast(Array(1, 2, 3)) broadcastVar: spark.Broadcast[Array[Int]] = spark.Broadcast(b5c40191-a864-4c7d-b9bf-d87e1a4e787c) scala> broadcastVar.value res0: Array[Int] = Array(1, 2, 3)
看了一下实现调用的是broadcastFactory的newBroadcast方法。
def newBroadcast[T: ClassTag](value_ : T, isLocal: Boolean) = {
broadcastFactory.newBroadcast[T](value_, isLocal, nextBroadcastId.getAndIncrement())
}
默认的broadcastFactory是HttpBroadcastFactory,内部还有另外一个实现TorrentBroadcastFactory,先说HttpBroadcastFactory的newBroadcast方法。
它直接new了一个HttpBroadcast。
HttpBroadcast.synchronized { SparkEnv.get.blockManager.putSingle(blockId, value_, StorageLevel.MEMORY_AND_DISK, tellMaster = false) } if (!isLocal) { HttpBroadcast.write(id, value_) }
它的内部做了两个操作,把数据保存到driver端的BlockManager并且写入到硬盘。
TorrentBroadcast和HttpBroadcast都把数据存进了BlockManager做备份,但是TorrentBroadcast接着并没有把数据写入文件,而是采用了下面这种方式:
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
1、把数据序列化之后,每4M切分一下。
2、切分完了之后,把所有分片写入BlockManager。
但是找不到它们是怎么传播的??只是写入到BlockManager,但是tellMaster为false的话,就相当于存在本地了,别的BlockManager是没法获取到的。
这时候我注意到它内部有两个方法,readObject和writeObject,会不会和这两个方法有关呢?它们做的操作就是给value赋值。
为了检验这个想法,我亲自调试了一下,在反序列化任务的时候,readObject这个方法是被ObjectInputStream调用了。这块的知识大家可以百度下ObjectInputStream和ObjectOutputStream。
具体操作如下:
1、打开BroadcastSuite这个类,找到下面这段代码,图中的地方原来是512, 被我改成256了,之前一直运行不起来。
test("Accessing TorrentBroadcast variables in a local cluster") { val numSlaves = 4 sc = new SparkContext("local-cluster[%d, 1, 256]".format(numSlaves), "test", torrentConf) val list = List[Int](1, 2, 3, 4) val broadcast = sc.broadcast(list) val results = sc.parallelize(1 to numSlaves).map(x => (x, broadcast.value.sum)) assert(results.collect().toSet === (1 to numSlaves).map(x => (x, 10)).toSet) }
2、找到TorrentBroadcast,在readObject方法上打上断点。
3、开始调试吧。
之前讲过,Task是被序列化之后包装在消息里面发送给Worker去运行的,所以在运行之前必须把Task进行反序列化,具体在TaskRunner的run方法里面:
task = ser.deserialize[Task[Any]](taskBytes, Thread.currentThread.getContextClassLoader)
Ok,告诉大家入口了,剩下的大家去尝试吧。前面介绍了怎么切分的,到TorrentBroadcast的readObject里面就很容易理解了。
1、先通过MetaId从BlockManager里面取出来Meta信息。
2、通过Meta信息,构造分片id,去BlockManager里面取。
3、获得分片之后,把分片写入到本地的BlockManager当中。
4、全部取完之后,通过下面的方法反向赋值。
if (receiveBroadcast()) { value_ = TorrentBroadcast.unBlockifyObject[T](arrayOfBlocks, totalBytes, totalBlocks) SparkEnv.get.blockManager.putSingle(broadcastId, value_, StorageLevel.MEMORY_AND_DISK, tellMaster = false) }
5、把value_又顺手写入到BlockManager当中。(这里相当于写了两份进去,大家要注意了哈,内存消耗还是大大地。幸好是MEMORY_AND_DISK的)
这么做是有好处的,这是一种类似BT的做法,把数据切分成一小块一小块,容易传播,从不同的机器上获取一小块一小块的数据,最后组装成完整的。
把完整的value写入BlockManager是为了使用的时候方便,不需要再次组装。
相关参数
// BlockManager的最大内存 spark.storage.memoryFraction 默认值0.6 // 文件保存的位置 spark.local.dir 默认是系统变量java.io.tmpdir的值 // tachyon保存的地址 spark.tachyonStore.url 默认值tachyon://localhost:19998 // 默认不启用netty来传输shuffle的数据 spark.shuffle.use.netty 默认值是false spark.shuffle.sender.port 默认值是0 // 一个reduce抓取map中间结果的最大的同时抓取数量大小(to avoid over-allocating memory for receiving shuffle outputs) spark.reducer.maxMbInFlight 默认值是48*1024*1024 // TorrentBroadcast切分数据块的分片大小 spark.broadcast.blockSize 默认是4096 // 广播变量的工厂类 spark.broadcast.factory 默认是org.apache.spark.broadcast.HttpBroadcastFactory,也可以设置为org.apache.spark.broadcast.TorrentBroadcastFactory // 压缩格式 spark.io.compression.codec 默认是LZF,可以设置成Snappy或者Lzo