SerializerManager 序列化管理器 简析
Spark 中很多对象在通过网络传输或者写入存储体系时,都需要序列化,SparkEnv 中有两个序列化的组件,分别是 SerializerManager 和 closureSerializer
def instantiateClassFromConf[T](propertyName: String, defaultClassName: String): T = {
instantiateClass[T](conf.get(propertyName, defaultClassName))
}
// 这里创建的 serializer 默认 org.apache.spark.serializer.JavaSerializer,用户可以通过 spark.serializer 属性配置其他的序列化实现,如 org.apache.spark.serializer.KryoSerializer
val serializer = instantiateClassFromConf[Serializer](
"spark.serializer", "org.apache.spark.serializer.JavaSerializer")
logDebug(s"Using serializer: ${serializer.getClass}")
val serializerManager = new SerializerManager(serializer, conf, ioEncryptionKey)
// closureSerializer 的实际类型固定为 org.apache.spark.serializer.JavaSerializer,用户不能够自己指定。JavaSerializer 采用 Java 语言自带的序列化API实现
val closureSerializer = new JavaSerializer(conf)
SerializerManager 的属性
SerializerManager 给各种 Spark 组件提供序列化、压缩及加密的服务,下面是这个对象的属性
- defaultSerializer : 默认的序列化器。此 defaultSerializer 即为上面代码中实例化的 serializer,也就是说defaultSerializer 的类型是JavaSerializer
- conf : 即SparkConf
- encryptionKey : 加密使用的密钥
- kryoSerialize : Spark提供的另一种序列化器。kryoSerializer 的实际类型是 KryoSerializer,其采用 Google 提供的 Kryo 序列化库实现
- stringClassTag : 字符申类型标记,即 ClassTag[String]。
- primitiveAndPrimitiveArrayClassTags : 原生类型及原生类型数组的类型标记的集合,包括 : Boolean、Array[boolean]、Int、Array[int]、Long、Array[long]、Byte、Array[byte]、Null、Array[scala.runtime.Null$]、Char、Array[char]、Double、Array[double]、Float、Array[float]、Short、Array[short]等
- compressBroadcast : 是否对广播对象进行压缩,可以通过 spark.broadcast.compress 属性配置,默认为true
- compressShuffle : 是否对 Shuffle 输出数据压缩,可以通过 spark.shuffle.compress 属性配置,默认为true
- compressRdds : 是否对RDD压缩,可以通过 spark.rdd.compress 属性配置,默认false
- compressShuffleSpill : 是否对溢出到磁盘的 Shuffle 数据压缩,可以通过 spark.shuffle.spill.compress 属性配置,默认为true
- compressionCodec : SerializerManager 使用的压缩编解码器。compressionCodec 的类型是CompressionCodec。在Spark1.x.x版本中,compresionCodec 是 BlockManager 的成员之一,现在把compressionCodec 和序列化、加密等功能都集中到 SerializerManager 中
CompressionCodec 的创建简析
为了节省磁盘存储空间,有些情况下需要对数据进行压缩
// 可以看到 compressionCodec 通过 lazy 关键字修饰为延迟初始化,即等到真正使用时才对其初始化
private lazy val compressionCodec: CompressionCodec = CompressionCodec.createCodec(conf)
private val shortCompressionCodecNames = Map(
"lz4" -> classOf[LZ4CompressionCodec].getName,
"lzf" -> classOf[LZFCompressionCodec].getName,
"snappy" -> classOf[SnappyCompressionCodec].getName,
"zstd" -> classOf[ZStdCompressionCodec].getName)
// val DEFAULT_COMPRESSION_CODEC = "lz4"
// 如果没有指定spark.io. compression.codec,默认是lz4压缩编解码器
def getCodecName(conf: SparkConf): String = {
conf.get(configKey, DEFAULT_COMPRESSION_CODEC)
}
def createCodec(conf: SparkConf): CompressionCodec = {
createCodec(conf, getCodecName(conf))
}
def createCodec(conf: SparkConf, codecName: String): CompressionCodec = {
// 从 shortCompressionCodecNames 缓存中获取编解码器名称对应的编解码器的类名
val codecClass =
shortCompressionCodecNames.getOrElse(codecName.toLowerCase(Locale.ROOT), codecName)
val codec = try {
// 通过 Java 反射生成编解码器的实例
val ctor = Utils.classForName(codecClass).getConstructor(classOf[SparkConf])
Some(ctor.newInstance(conf).asInstanceOf[CompressionCodec])
} catch {
case _: ClassNotFoundException | _: IllegalArgumentException => None
}
// 如果发现无法使用配置的编解码器 那么建议使用 val FALLBACK_COMPRESSION_CODEC = "snappy"
codec.getOrElse(throw new IllegalArgumentException(s"Codec [$codecName] is not available. " +
s"Consider setting $configKey=$FALLBACK_COMPRESSION_CODEC"))
}
在 Spark 1.x.x 版本中,默认的压缩算法是 snappy,2.x.x 是 lz4
SerializerManager 的方法简析
- encryptionEnabled : 当前 SerializerManager 是否支持加密。SerializerManager 要想支持加密,必须在构造 SerializerManager 的时候就传入 encryptionKey。可以通过 spark.io.encryption.enabled( 允许加密)、spark.io.encryption.keySizeBits (密钥长度,有128、192、256三种长度)、spark.io.encryption.keygen.algorithm (加密算法,默认为HmacSHA1 )等属性进行具体的配置
- canUseKryo(ct:ClassTag[]):对于指定的类型标记 ct,是否能使用 kryoSerializer 进行序列化。当类型标记ct 属于 primitiveAndPrimitiveArrayClassTags 或者 stringClassTag 时,canUseKryo 方法才返回真
- getSerializer(ct:ClassTag[],autoPick:Boolean) : 获取序列化器。如果 autoPick 为 true(即 Blockid 不为StreamBlockId 时)并且调用 canUseKryo 的结果为 true 时选择 kryoSerializer,否则选择 defaultSerializer
- getSerializer(keyClassTag:ClassTag[],valueClassTag:ClassTag[]) : 获取序列化器。如果对于 keyClassTag 和 valueClassTag,调用 canUseKryo 的结果都 true 时选择 kryoSerializer,否则选择defaultSerializer
- wrapStream(blockId:BlockId,s:InputStream) : 对 Block的输人流进行压缩与加密
- wrapStream(blockId:BlockId,s:OutputStream ) : 对Block的输出流进行压缩与加密
- wrapForEncryption(s:InputStream) : 对输入流进行加密
- wrapForEncryption(s:OutputStream) : 对输出流进行加密
- dataSerializeStreamT:ClassTag (blockId:BlockId,outputStream:OutputStream,values:Iterator[T]) : 对 Block 的输出流序列化。
- dataSerialize[T:ClassTag] (blockId:BlockId,values:Iterator[T],allowEncryption:Boolean=true) : 序列化成分块字节缓冲区(ChunkedByteBuffer)。
- dataSerializeWithExplicitClassTag(blockId:BlockId,values:Iterator[],classTag:ClassTag[],allowEncryption:Bolean=true) : 使用明确的类型标记,序列化成分块字节缓冲区(ChunkedByteBuffer)
- dataDeserializeStream[T](blockId:BlockId,inputStream:InputStream,maybeEncrypted:Boolean=true)(classTag:ClassTag[T]) : 将输入流反序列化为值的迭代器 Iterator[T]
BroadcastManager 广播管理器简析
BroadcastManager 用于将配置信息和序列化后的 RDD、Job 及 ShuffleDependency 等信息在本地存储,如果为了容灾,也会复制到其他节点上
val broadcastManager = new BroadcastManager(isDriver, conf, securityManager)
BroadcastManager除了构造器定义的三个成员属性外,内部还有三个成员
- initialized : 表示 BroadcastManager 是否初始化完成的状态
- broadcastFactory : 广播工厂实例
- nextBroadcastId : 下一个广播对象的广播 ID,类型为 AtomicLong
BroadcastManager 在其初始化的过程中就会调用自身的 initialize 方法,当 initialize 执行完毕,BroadcastManager 就正式生效
private def initialize() {
synchronized {
// 首先判断 BroadcastManager 是否已经初始化,以保证 BroadcastManager 只被初始化一次
if (!initialized) {
// 新建 TorrentBroadcastFactory 作为 BroadcastManager 的广播工厂实例
broadcastFactory = new TorrentBroadcastFactory
// 之后调用 TorrentBroadcastFactory 的 initialize 方法对 TorentBroadcastFactory进行初始化
broadcastFactory.initialize(isDriver, conf, securityManager)
// 最后将 BroadcastManager 自身标记为初始化完成状态
initialized = true
}
}
}
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)
}
对应的在 TorrentBroadcastFactory 中也提供了这三个方法的实现
// 用于生成 TorrentBroadcast 实例,其作用为广播 TorrentBroadcast 中的 value,表面看只是生成了 TorrentBroadcast 但其实不然
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)
}
TorrentBroadcastFactory 的属性
- compressionCodec : 用于广播对象的压缩编解码器。可以设置 spark.broadcast.compress 属性为 true 启用,默认是启用的。compressionCodec的类型前面介绍过的CompressionCodec,而且最终采用的压缩算法与 SerializerManager 中的 CompressionCodec 是一致的
- blockSize : 每个块的大小。它是个只读属性,可以使用 spark.broadcast.blockSize 属性进行配置,默认为4MB
- broadcastId : 广播Id。broadcastId 实际是样例类 BroadcastBlockId
- checksumEnabled : 是否给广播块生成校验和。可以通过 spark.broadcast.checksum 属性进行配置,默认为true
- checksums : 用于存储每个广播块的校验和的数组
- numBlocks : 广播变量包含的块的数量。numBlocks 通过调用 writeBlocks 方法获得。由于 numBlocks 是个 val 修饰的不可变属性,因此在构造 TorrentBroadcast 实例的时候就会调用 writeBlocks 方法将广播对象写入存储体系
- _value : 从 Executor 或者 Driver 上读取的广播块的值。value 是通过调用 readBroadcastBlock 方法获得的广播对象。由于 value 是个 lazy 及 val 修饰的属性,因此在构造 TorrentBroadcast 实例的时候不会调用 readBroadcastBlock 方法,而是等到明确需要使用 value 的值时
广播对象的写操作简析
private def writeBlocks(value: T): Int = {
import StorageLevel._
// 获取当前 SparkEnv 的 BlockManager 组件
val blockManager = SparkEnv.get.blockManager
// 调用 BlockManager 的 putSingle 方法将广播对象写入本地的存储体系。当 Spark 以 local 模式运行时,则会将广播对象写人 Driver 本地的存储体系,以便于任务也可以在 Driver 上执行。由于MEMORY_AND_DISK 对应的 StorageLevel 的 _replication 属性固定为 1,因此此处只会将广播对象写入 Driver 或 Executor 本地的存储体系
if (!blockManager.putSingle(broadcastId, value, MEMORY_AND_DISK, tellMaster = false)) {
throw new SparkException(s"Failed to store $broadcastId in BlockManager")
}
try {
// 调用 TorrentBroadcast 的 blockifyObject 方法,将对象转换成一系列的块。每个块的大小由 blockSize 决定,使用当前 SparkEnv 中的 JavaSerializer 组件进行序列化,使用TorrentBroadcast 自身的 compressionCodec 进行压缩
val blocks =
TorrentBroadcast.blockifyObject(value, blockSize, SparkEnv.get.serializer, compressionCodec)
// 如果需要给分片广播块生成校验和,则创建和上一步转换的块的数量一致的 checksums 数组
if (checksumEnabled) {
checksums = new Array[Int](blocks.length)
}
// 对每个块进行如下处理
blocks.zipWithIndex.foreach { case (block, i) =>
if (checksumEnabled) {
// 如果需要给分片广播块生成校验和,则给分片广播块生成校验和
checksums(i) = calcChecksum(block)
}
// 给当前分片广播块生成分片的 BroadcastBlockId,分片通过 BroadcastBlockld 的 field 属性区别,例如,pieceO、piece1...
val pieceId = BroadcastBlockId(id, "piece" + i)
val bytes = new ChunkedByteBuffer(block.duplicate())
// 调用 BlockManager 的 putBytes 方法将分片广播块以序列化方式写人 Driver 本地的存储体系。由于 MEMORY_AND_DISK_SER 对应的 StorageLeve l的 _replication 属性也固定为1,因此此处只会将分片广播块写人 Driver 或 Executor 本地的存储体系
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
}
}
广播对象的读操作简析
只有当 TorrentBroadcast 实例的 _value 属性值在需要的时候,才会调用 readBroadcastBlock方法获取值
private def readBroadcastBlock(): T = Utils.tryOrIOException {
TorrentBroadcast.synchronized {
val broadcastCache = SparkEnv.get.broadcastManager.cachedValues
Option(broadcastCache.get(broadcastId)).map(_.asInstanceOf[T]).getOrElse {
setConf(SparkEnv.get.conf)
// 获取当前 SparkEnv 的 BlockManager 组件
val blockManager = SparkEnv.get.blockManager
// 调用 BlockManager 的 getLocalValues 方法从本地的存储系统中获取广播对象,即通过 BlockManager 的 putSingle 方法写人存储体系的广播对象
blockManager.getLocalValues(broadcastId) match {
case Some(blockResult) =>
if (blockResult.data.hasNext) {
// // 如果从本地的存储体系中可以获取广播对象,则调用 releaseLock 方法(这个锁保证当块被一个运行中的任务使用时,不能被其他任务再次使用,但是当任务运行完成时,则应该释放这个锁),释放当前块的锁并返回此广播对象
val x = blockResult.data.next().asInstanceOf[T]
releaseLock(broadcastId)
if (x != null) {
broadcastCache.put(broadcastId, x)
}
x
} else {
throw new SparkException(s"Failed to get locally stored broadcast data: $broadcastId")
}
case None =>
// 如果从本地的存储体系中没有获取到广播对象,那么说明数据是通过 BlockManager 的 putBytes 方法以序列化方式写人存储体系的。此时首先调用 readBlocks 方法从 Driver 或 Executor 的存储体系中获取广播块
logInfo("Started reading broadcast variable " + id)
val startTimeMs = System.currentTimeMillis()
val blocks = readBlocks()
logInfo("Reading broadcast variable " + id + " took" + Utils.getUsedTimeMs(startTimeMs))
try {
// 然后调用 TorrentBroadcast 的 unBlockifyObject 方法,将一系列的分片广播块转换回原来的广播对象
val obj = TorrentBroadcast.unBlockifyObject[T](
blocks.map(_.toInputStream()), SparkEnv.get.serializer, compressionCodec)
// 最后再次调用 BlockManager 的 putSingle 方法将广播对象写人本地的存储体系,以便于当前Executor 的其他任务不用再次获取广播对象
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 readBlocks(): Array[BlockData] = {
// 新建用于存储每个分片广播块的数组 blocks
val blocks = new Array[BlockData](numBlocks)
// 获取当前 SparkEnv 的 BlockManager 组件
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")
// 调用 BlockManager 的 getLocalBytes 方法从本地的存储体系中获取序列化的分片广播块,
bm.getLocalBytes(pieceId) match {
// 如果本地可以获取到,则将分片广播块放入 blocks,并且调用 releaseLock 方法释放此分片广播块的锁
case Some(block) =>
blocks(pid) = block
releaseLock(pieceId)
// 如果本地没有,则调用 BlockManager 的 getRemoteBytes 方法从远端的存储体系中获取分片广播块
case None =>
bm.getRemoteBytes(pieceId) match {
case Some(b) =>
if (checksumEnabled) {
// 对于获取的分片广播块再次调用 calcChecksum 方法计算校验和
val sum = calcChecksum(b.chunks(0))
// 将此校验和与调用 writeBlocks 方法时存入 checksums 数组的校验和进行比较
// 校验和不相同,说明块的数据有损坏,此时抛出异常
if (sum != checksums(pid)) {
throw new SparkException(s"corrupt remote block $pieceId of $broadcastId:" +s" $sum != ${checksums(pid)}")
}
}
// 如果校验和相同,则调用 BlockManager 的 putBytes 方法将分片广播块写入本地存储体系,以便于当前 Executor 的其他任务不用再次获取分片广播块
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
blocks(pid) = new ByteBufferBlockData(b, true)
case None =>
throw new SparkException(s"Failed to get $pieceId of $broadcastId")
}
}
}
// 返回 blocks 中的所有分片广播块
blocks
}
广播对象的去持久化简析
// TorrentBroadcastFactory 的 unbroadcast 方法实际调用了 TorrentBroadcast 的 unpersist 方法对由 id标记的广播对象去持久化
def unpersist(id: Long, removeFromDriver: Boolean, blocking: Boolean): Unit = {
logDebug(s"Unpersisting TorrentBroadcast $id")
// 可以看到 TorrentBroadcast 的 unpersist 方法实际调用了 BlockManager的子组件BlockManagerMaster 的 removeBroadcast 方法来实现对广播对象去持久化
SparkEnv.get.blockManager.master.removeBroadcast(id, removeFromDriver, blocking)
}
map任务输出跟踪器 简析
mapOutputTracker 用于跟踪 map 任务的输出状态,此状态便于 reduce 任务定位 map 输出结果所在的节点地址,进而获取中间输出结果
每个 map 任务或者 reduce 任务都会有其唯一标识,分别为 mapId 和 reduceId,每个 reduce 任务的输入可能是多个 map 任务的输出,reduce 会到各个 map任 务所在的节点上拉取 Block,这一过程叫做Shuffle,每次Shuffle都有唯一的标识shuffleId
需要先对 SparkEnv 中用于注册 RpcEndpoint 或者查找 RpcEndpoint 的方法 registerOrLookupEndpoint 进行简析
def registerOrLookupEndpoint(
name: String, endpointCreator: => RpcEndpoint):
RpcEndpointRef = {
if (isDriver) {
logInfo("Registering " + name)
// 如果当前实例是 Driver,则调用 setupEndpoint 方法向 Dispatcher 注册 Endpoint
rpcEnv.setupEndpoint(name, endpointCreator)
} else {
// 如果是 Executor,则调用工具类 RpcUtils 的 makeDriverRef 方法向远端的 NettyRpcEnv 询问获取相关 RpcEndpoint 的 RpcEndpointRef
RpcUtils.makeDriverRef(name, conf, rpcEnv)
}
}
val mapOutputTracker =
if (isDriver) {
// 如果当前应用程序是 Driver,则创建 MapOutputTrackerMaster,然后创建 MapOutputTrackerMasterEndpoint,并且注册到 Dispatcher 中,注册名为 MapOutputTracker
new MapOutputTrackerMaster(conf, broadcastManager, isLocal)
} else {
// 如果当前应用程序是 Executor,则创建 MapOutputTrackerWorker,并从远端 Driver 实例的 NettyRpcEnv 的 Dispatcher 中查找 MapOutputTrackerMasterEndpoint 的引用
new MapOutputTrackerWorker(conf)
}
// 无论是Driver还是Executor,最后都由 mapOutputTracker 的属性 trackerEndpoint 持有 MapOutputTrackerMasterEndpoint 的引用
mapOutputTracker.trackerEndpoint = registerOrLookupEndpoint(MapOutputTracker.ENDPOINT_NAME,
new MapOutputTrackerMasterEndpoint(
rpcEnv, mapOutputTracker.asInstanceOf[MapOutputTrackerMaster], conf))
MapOutputTracker 的实现简析
无论是 MapOutputTrackerMaster 还是 MapOutputTrackerWorker,它们都继承自抽象类 MapOutputTracker
MapOutputTracker 内部定义了任务输出跟踪器的规范
MapOutputTracker 的属性
- trackerEndpoint : 用于持有 Driver 上 MapOutputTrackerMasterEndpoint 的 RpcEndpointRef
- epoch : 用于 Executor 故障转移的同步标记。每个 Executor 在运行的时候会更新 epoch,潜在的附加动作将清空缓存。当 Executor 丢失后增加 epoch
- epochLock : 用于保证 epoch 变量的线程安全性
MapOutputTracker 的方法
- askTraker方法
protected def askTracker[T: ClassTag](message: Any): T = {
try {
// 调用 RpcEndpointRef 的 askSync 方法
trackerEndpoint.askSync[T](message)
} catch {
case e: Exception =>
logError("Error communicating with MapOutputTracker", e)
throw new SparkException("Error communicating with MapOutputTracker", e)
}
}
- sendTracker 方法
protected def sendTracker(message: Any) {
// 用于向 MapOutputTrackerMasterEndpoint 发送消息,并期望在超时时间之内获得的返回值为 true
val response = askTracker[Boolean](message)
if (response != true) {
throw new SparkException(
"Error reply received from MapOutputTracker. Expecting true, got " + response.toString)
}
}
MapOutputTrackerMaster 的实现简析
通常而言,我们所说的 mapOutputTracker 都是指 MapOutputTrackerMaster ,而不是 MapOutputTrackerWorker
MapOutputTrackerWorker 将 map 任务的跟踪信息,通过 MapOutputTrackerMasterEndpoint 的 RpcEndpointRef 发送给MapOutputTrackerMaster ,由 MapOutputTrackerMaster 负责整理和维护所有的 map 任务的输出跟踪信息
MapOutputTrackerMasterEndpoint 位于 MapOutputTrackerMaster 内部,二者只存在于 Driver 上
MapOutputTrackerMaster 的属性
- minSizeForBroadcast : 用于广播的最小大小。可以使用 spark.shuffle.mapOutput.minSizeForBroadcast 属性配置,默认为512KB。minSizeForBroadcast 必须小于 maxRpcMessageSize
- shuffleLocalityEnabled : 是否为 reduce 任务计算本地性的偏好。可以使用 spark.shuffle.reduceLocality.enabled 属性进行配置,默认为 true
- shuffleStatuses : 用于存储 shuffleld 与 Array[ShuffleStatus] 的映射关系。 ShuffleStatus 作为一个辅助类维护了一些属性如下
- mapStatus : 存储了类型为 MapStatus ,长度为 map 分区数量的数组,其中 MapStatus 维护了 map 输出 Block 的地址 BlockManagerId,所以 reduce 任务知道从何处获取 map 任务的中间输出
- cachedSerializedStatuses : 用于存储 shuffleld 与序列化后的状态的映射关系。其中 key 对应 shuffleId, value为对 MapStatus 序列化后的字节数组
- cachedSerializedBroadcast : 用于缓存序列化的广播变量,保持与 cachedSerializedStatuses 的同步。当需要移除 shuffleld 在 cachedSerializedStatuses 中的状态数据时,此缓存中的数据也会被移除
- maxRpcMessageSize : 最大的 Rpc 消息的大小。此属性可以通过 spark.rpc.message.maxSize 属性进行配置,默认为 128,单位是 MB。minSizeForBroadcast 必须小于 maxRpcMessageSize。maxRpcMessageSize 实际通过调用RpcUtils 的 maxMesageSizeBytes 方法获得
- mapOutputRequests : 使用阻塞队列来缓存 GetMapOutputMessage (获取 map 任务输出)的请求。
- threadpool : 用于获取 map 输出的固定大小的线程池。此线程池提交的线程都以后台线程运行,且线程名以 map-output-dispatcher 为前缀,线程池大小可以使用 spark.shuffle.mapOutput.dispatcher.numThreads 属性配置,默认大小为8
MapOutputTrackerMaster 的运行原理
在创建 MapOutputTrackerMaster 的最后,会创建对 map 输出请求进行处理的线程池 threadpool
private val threadpool: ThreadPoolExecutor = {
// 获取此线程池的大小numThreads。此线程池的大小默认为8,也可以使用spark.shuffle.mapOutput.dispatcher.numThreads 属性配置
val numThreads = conf.getInt("spark.shuffle.mapOutput.dispatcher.numThreads", 8)
// 创建线程池。此线程池是固定大小的线程池,并且启动的线程都以后台线程方式运行,且线程名以map-output-dispatcher为前缀
val pool = ThreadUtils.newDaemonFixedThreadPool(numThreads, "map-output-dispatcher")
for (i <- 0 until numThreads) {
// 启动与线程池大小相同数量的线程,每个线程执行的任务都是 MessageLoop
pool.execute(new MessageLoop)
}
// 返回此线程池的引用
pool
}
private class MessageLoop extends Runnable {
override def run(): Unit = {
try {
while (true) {
try {
// 从 mapOutputRequests 中获取 GetMapOutputMessage。
// 由于 mapOutputRequests 是个阻塞队列,所以当 mapOutputRequests 中没有 GetMapOutputMessage 时,MessageLoop 线程会被阻塞。
// GetMapOutputMessage 是个样例类,包含了 shuffleId 和 RpcCallContext 两个属性,RpcCallContext 用于服务端回复客户端
val data = mapOutputRequests.take()
// 如果取到的 GetMapOutputMessage 是 PoisonPill(“毒药”),那么此 MessageLoop 线程将退出(通过 return 语句)。这里有个动作,就是将 PoisonPill 重新放人到 mapOutputRequests 中,这是因为 threadpool 线程池极有可能不止一个MessageLoop 线程,为了让大家都“毒发身亡”,还需要把“毒药”放回到 receivers 中,这样其他“活着”的线程就会再次误食“毒药”,达到所有 MessageLoop 线程都结束的效果
if (data == PoisonPill) {
mapOutputRequests.offer(PoisonPill)
return
}
val context = data.context
val shuffleId = data.shuffleId
val hostPort = context.senderAddress.hostPort
logDebug("Handling request to send map output locations for shuffle " + shuffleId +
" to " + hostPort)
// 如果取到的 GetMapOutputMessage 不是“毒药”,那么通过 GetMapOutputMessage 获取 shuffleId,然后再获取到 shuffleStatus
val shuffleStatus = shuffleStatuses.get(shuffleId).head
// 调用 RpcCallContext 的回调方法 reply,将序列化的 map 任务状态信息返回给客户端(即其他节点的 Executor)
context.reply(
// 调用 serializedMapStatus 方法获取 shuffleStatus 携带的 cachedSerializedMapStatus 进行回应
shuffleStatus.serializedMapStatus(broadcastManager, isLocal, minSizeForBroadcast))
} catch {
case NonFatal(e) => logError(e.getMessage, e)
}
}
} catch {
case ie: InterruptedException => // exit
}
}
}
MapOutputTrackerMaster 中的 MessageLoop 任务在不断地消费着阻塞队列 mapOutputRequests 中的 GetMapOutputMessage
在MapOutputTrackerMaster 中要将 GetMapOutputMessage 放入 mapOutputRequests,主要通过调用 MapOutputTrackerMaster的post方法
def post(message: GetMapOutputMessage): Unit = {
mapOutputRequests.offer(message)
}
MapOutputTrackerMasterEndpoint 组件用于接收获取 map 中间状态和停止对 map 中间状态进行跟踪的请求,它实现了特质RpcEndpoint 并重写了 receiveAndReply 方法
private[spark] class MapOutputTrackerMasterEndpoint(
override val rpcEnv: RpcEnv, tracker: MapOutputTrackerMaster, conf: SparkConf)
extends RpcEndpoint with Logging {
logDebug("init") // force eager creation of logger
// 可以看到MapOutputTrackerMasterBndpoint将接收两种类型的请求
override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
// 获取 map 中间输出状态。当接收到 GetMapOutputStatuses 消息后,将调用 MapOutputTrackerMaster 的 post 方法投递GetMapOutputMessage 类型的消息
case GetMapOutputStatuses(shuffleId: Int) =>
val hostPort = context.senderAddress.hostPort
logInfo("Asked to send map output locations for shuffle " + shuffleId + " to " + hostPort)
val mapOutputStatuses = tracker.post(new GetMapOutputMessage(shuffleId, context))
// 停止 map 中间输出的跟踪。首先回调 RpcCallContext 的 reply 方法向客户端返回 true,然后调用MapOutputTrackerMaster 的 stop 方法停止 MapOutputTrackerMaster
case StopMapOutputTracker =>
logInfo("MapOutputTrackerMasterEndpoint stopped!")
context.reply(true)
stop()
}
}
override def stop() {
// 向 mapOutputRequests 投递 PoisonPil(“毒药”)。此操作会向 mapOutputRequests 队列中添加 PoisonPil,进而停止MapOutputTrackerMaster 中的所有 MessageLoop 线程
mapOutputRequests.offer(PoisonPill)
// 关闭 threadpool。由于 MapOutputTrackerMaster 中的所有 MessageLoop 线程都已经停止,因而可以平滑关闭threadpool
threadpool.shutdown()
// 调用 sendTracker 向 MapOutputTrackerMasterEndpoint 发送 StopMapOutputTracker 消息
sendTracker(StopMapOutputTracker)
// 回收 MapOutputTrackerMasterEndpoint
trackerEndpoint = null
// 清空 shuffleStatuses
shuffleStatuses.clear()
}
MapOutputTrackerWorker 的实现简析
MapOutputTrackerWorker 的属性
- mapStatuses : 用于存储 shuffleId 与 Array[MapStatus] 的映射关系。由于 MapStatus 维护了 map 输出 Block 的地址BlockManagerId,所以 reduce 任务知道从何处获取 map 任务的中间输出
- fetching : shuffle 获取集合。数据类型为 HashSet[Int],用来记录当前 Executor 正在从哪些 map 输出的位置拉取数据
MapOutputTrackerWorker 的实现
- getStatuses 方法
private def getStatuses(shuffleId: Int): Array[MapStatus] = {
// 从当前 MapOutputTrackerWorker 的 mapStatuses 缓存中获取 MapStatus 数组
val statuses = mapStatuses.get(shuffleId).orNull
// 如果缓存中没有 MapStatus
if (statuses == null) {
logInfo("Don't have map outputs for shuffle " + shuffleId + ", fetching them")
val startTime = System.currentTimeMillis
var fetchedStatuses: Array[MapStatus] = null
// 如果 shuffle 获取集合(即 fetching )中已经存在要取的 shuffleId (这说明已经有其他线程对此 shuffleId 的数据进行远程拉取了),那么就等待其他线程获取。等待会一直持续
fetching.synchronized {
while (fetching.contains(shuffleId)) {
try {
fetching.wait()
} catch {
case e: InterruptedException =>
}
}
// 直到 fetching 中不存在要取的 shuffleId (这说明其他线程对此shuffleId的数据进行远程拉取的操作已经结束),并再次从 mapStatuses 缓存中获取 MapStatus 数组
fetchedStatuses = mapStatuses.get(shuffleId).orNull
// 如果 fetching 中不存在要取的 shuffleId,那么当前线程需要将 shuffleId 加入 fetching,以表示已经有线程对此shuffleId 的数据进行远程拉取了
if (fetchedStatuses == null) {
fetching += shuffleId
}
}
if (fetchedStatuses == null) {
logInfo("Doing the fetch; tracker endpoint = " + trackerEndpoint)
try {
// 调用 askTracker 方法向 MapOutputTrackerMasterEndpoint 发送 GetMapOutputStatuses 消息,以获取 map任务的状态信息,MapOutputTrackerMasterEndpoint 接收到 GetMapOutputStatuses 消息后,将 GetMapOutputStatuses 消息转换为 GetMapOutputMessage 消息,异步线程MessageLoop从队列中取出GetMapOutputMessage,将请求的map任务状态信息序列化后返回给请求方
val fetchedBytes = askTracker[Array[Byte]](GetMapOutputStatuses(shuffleId))
// 请求方接收到 map 任务状态信息后,调用 MapOutputTracker 的 deserializeMapStatuses 方法对 map 任务状态进行反序列化操作,然后放入本地的 mapStatuses 缓存中
fetchedStatuses = MapOutputTracker.deserializeMapStatuses(fetchedBytes)
logInfo("Got the output locations")
// 放人 mapOutputRequests 队列
mapStatuses.put(shuffleId, fetchedStatuses)
} catch {
case e: SparkException =>
throw new MetadataFetchFailedException(shuffleId, -1,
s"Unable to deserialize broadcasted map statuses for shuffle $shuffleId: " +
e.getCause)
} finally {
// 拉取结束后无论如何需要将 shuffleId 从 fetching 中移除,并唤醒那些在 fetching 的锁上等待的线程,以便这些线程能够获取自己需要的 MapStatus 数组
fetching.synchronized {
fetching -= shuffleId
fetching.notifyAll()
}
}
}
logDebug(s"Fetching map output statuses for shuffle $shuffleId took " +
s"${System.currentTimeMillis - startTime} ms")
if (fetchedStatuses != null) {
fetchedStatuses
} else {
logError("Missing all output locations for shuffle " + shuffleId)
throw new MetadataFetchFailedException(
shuffleId, -1, "Missing all output locations for shuffle " + shuffleId)
}
} else {
// 如果获取到 MapStatus 数组,返回得到的MapStatus数组
statuses
}
}
- getMapSizesByExecutorId 方法
// 通过 shuffleId 和 reduceId 获取存储了 reduce 所需的 map 中间输出结果的 BlockManager 的 BlockManagerId,以及map 中间输出结果每个 Block 块的 BlockId 与大小
MapOutputTracker的convertMapStatuses方法用于将Array[MapStatus]转换为Seql(BlockManagerld,Seq[(Blockid,Long)])]
override def getMapSizesByExecutorId(shuffleId: Int, startPartition: Int, endPartition: Int)
: Iterator[(BlockManagerId, Seq[(BlockId, Long)])] = {
logDebug(s"Fetching outputs for shuffle $shuffleId, partitions $startPartition-$endPartition")
val statuses = getStatuses(shuffleId)
try {
MapOutputTracker.convertMapStatuses(shuffleId, startPartition, endPartition, statuses)
} catch {
case e: MetadataFetchFailedException =>
// We experienced a fetch failure so our mapStatuses cache is outdated; clear it:
mapStatuses.clear()
throw e
}
}
- updateEpoch 方法
// 当 Executor 运行出现故障时,Master 会再分配其他 Executor 运行任务,此时会调用 updateEpoch 方法更新纪元,并且清空mapStatuses
def updateEpoch(newEpoch: Long): Unit = {
epochLock.synchronized {
if (newEpoch > epoch) {
logInfo("Updating epoch to " + newEpoch + " and clearing cache")
epoch = newEpoch
mapStatuses.clear()
}
}
}
- unregisterShuffle 方法
// unregisterShuffle 方法用于 ContextCleaner 清除 shuffleId 对应 MapStatus 的信息
def unregisterShuffle(shuffleId: Int): Unit = {
mapStatuses.remove(shuffleId)
}
Shuffle 的注册
DAGScheduler 在创建 ShuffleMapStage 的时候,将调用 MapOutputTrackerMaster 的containsShuffle 方法,查看是否已经存在 shuffleId 对应的 ShuffleStatus。如果 MapOutputTrackerMaster 中未注册此 shuffleld,那么调用 MapOutputTrackerMaster 的registerShuffle 方法注册 shuffle
containsShuffle 方法用于查找 MapOutputTrackerMaster 的 shuffleStatuses 是否已经注册了 shuffleId
def containsShuffle(shuffleId: Int): Boolean = shuffleStatuses.contains(shuffleId)
registerShuffle 方法用于向 MapOutputTrackerMaster 的 shuffleStatuses 注册 shuffleId 与对应的 ShuffleStatus 的关系
def registerShuffle(shuffleId: Int, numMaps: Int) {
if (shuffleStatuses.put(shuffleId, new ShuffleStatus(numMaps)).isDefined) {
throw new IllegalArgumentException("Shuffle ID " + shuffleId + " registered twice")
}
}
当 ShuffleMapStage 内的所有 ShuffleMapTask 运行成功后,将调用 MapOutputTrackerMaster 的 registerMapOutputs 方法
def registerMapOutput(shuffleId: Int, mapId: Int, status: MapStatus) {
shuffleStatuses(shuffleId).addMapOutput(mapId, status)
}
registerMapOutputs 方法将把 ShuffleMapStage 中每个 ShuffleMapTask 的 MapStatus 保存到 shuffleld 在 ShuffleStatus 中对应的数组中
存储体系的构建 简析
BroadcastManager 的底层都依赖于 SparkEnv 的存储体系。存储体系中最重要的组件包括 Shuffle 管理器 ShuffleManager、内存管理器MemoryManager、块传输服务 BlockTransferService、对所有 BlockManager 进行管理的 BlockManagerMaster、磁盘块管理器DiskBlockManager、块锁管理器 BlockInfoManager 及块管理器 BlockManager。这里简单介绍SparkEnv中存储体系各个组件的实例化
val shortShuffleMgrNames = Map(
"sort" -> classOf[org.apache.spark.shuffle.sort.SortShuffleManager].getName,
"tungsten-sort" -> classOf[org.apache.spark.shuffle.sort.SortShuffleManager].getName)
val shuffleMgrName = conf.get("spark.shuffle.manager", "sort")
val shuffleMgrClass =
shortShuffleMgrNames.getOrElse(shuffleMgrName.toLowerCase(Locale.ROOT), shuffleMgrName)
// 根据 spark.shuffle.manager 属性,实例化 ShuffleManager。Spark2.x.x版本提供了 sort 和 tungsten-sort 两种ShuffleManager 的实现。无论是 sort 还是 tungsten-sort,我们看到其实现类都是 SortShuffleManager
val shuffleManager = instantiateClass[ShuffleManager](shuffleMgrClass)
val useLegacyMemoryManager = conf.getBoolean("spark.memory.useLegacyMode", false)
// MemoryManager 的主要实现有 StaticMemoryManager 和 UnifiedMemoryManager。StaticMemoryManager 是 Spark 早期版本遗留下来的内存管理器实现,可以配置 spark.memory.useLegacyMode 属性来指定,该属性默认为 false,因此默认的内存管理器是 UnifiedMemoryManager
val memoryManager: MemoryManager =
if (useLegacyMemoryManager) {
new StaticMemoryManager(conf, numUsableCores)
} else {
UnifiedMemoryManager(conf, numUsableCores)
}
// 获取当前 SparkEnv 的 块传输服务 BlockTransferService 对外提供的端口号。如果当前实例是 Driver,则从 SparkConf 中获取由常量 DRIVER_BLOCK_MANAGER_PORT 指定的端口。如果当前实例是 Executor,则从 SparkConf 中获取由常量BLOCK_MANAGER_PORT指定的端口
// 可以通过指定 spark.driver.blockManager.port 属性或者 spark.blockManager.port 属性对 BlockTransferService 的端口进行配置
val blockManagerPort = if (isDriver) {
conf.get(DRIVER_BLOCK_MANAGER_PORT)
} else {
conf.get(BLOCK_MANAGER_PORT)
}
// 创建块传输服务 BlockTransferService。这里使用的是 BlockTransferService 的子类NettyBlockTransferService,NettyBlockTransferService 将提供对外的块传输服务。也正是因为 MapOutputTracker 与NettyBlockTransferService 的配合,才实现了Spark 的 Shuffle。 因此:
val blockTransferService =
new NettyBlockTransferService(conf, securityManager, bindAddress, advertiseAddress,
blockManagerPort, numUsableCores)
// 查找或注册BlockManagerMasterBndpoint。这里的registerOrLookupEndpoint方法会根据不同应用程序来进行注册
// 1 当前应用程序是 Driver,则创建 BlockManagerMasterEndpoint,并且注册到 Dispatcher 中,注册名为BlockManagerMaster
// 2 当前应用程序是 Executor,则从远端 Driver 实例的 NettyRpcEnv的Dispatcher 中查找 BlockManagerMasterEndpoint 的引用
// 无论是 Driver 还是 Executor,最后都由 BlockManagerMaster 的属性 driverEndpoint 持有BlockManagerMasterEndpoint 的引用
// 创建 BlockManagerMaster
val blockManagerMaster = new BlockManagerMaster(registerOrLookupEndpoint(
BlockManagerMaster.DRIVER_ENDPOINT_NAME,
new BlockManagerMasterEndpoint(rpcEnv, isLocal, conf, listenerBus)),
conf, isDriver)
// 创建 BlockManager。此处只创建了 BlockManager,只有其 init 方法被调用后,BlockManager 才能正常工作
val blockManager = new BlockManager(executorId, rpcEnv, blockManagerMaster,
serializerManager, conf, memoryManager, mapOutputTracker, shuffleManager,
blockTransferService, securityManager, numUsableCores)
度量系统的创建 简析
val metricsSystem = if (isDriver) {
// 当前实例为 Driver : 创建度量系统,并且制定度量系统的实例名为 driver。此时虽然创建了,但是并未启动,目的是等待SparkContext 中的任务调度器 TaskScheculer 告诉度量系统应用程序 ID 后再启动
MetricsSystem.createMetricsSystem("driver", conf, securityManager)
} else {
// 当前实例为 Executor : 设置 spark.executor.id 属性为当前 Executor 的 ID,然后创建并启动度量系统
conf.set("spark.executor.id", executorId)
val ms = MetricsSystem.createMetricsSystem("executor", conf, securityManager)
ms.start()
ms
}
// 创建度量系统使用了伴生对象 MetricsSystem 的 createMetricsSystem 方法
def createMetricsSystem(
instance: String, conf: SparkConf, securityMgr: SecurityManager): MetricsSystem = {
new MetricsSystem(instance, conf, securityMgr)
}
度量系统 MetricsSystem 的属性 简析
- instance : 度量系统的实例名。例如,Master、Worker、Application、Driver 及Executor 等
- metricsConfig : 度量配置,metricsConfig 的类型为 MetricsConfig,主要提供对度量配置的设置、加载、转换等功能,MetricsConfig 中的度量配置包括了 Sink 和 Source,因此 MetricsSystem 将根据 MetricsConfig 构建度量系统的所有 Sink 和 Source
- sinks : Sink的数组,sinks 用于缓存所有注册到 MetricsSystem 的度量输出
- sources : Source的数组,sources 用于缓存所有注册到 MetricsSystem 的 Source
- registry : 度量注册点 MetricRegistry。Source 和 Sink 实际都是通过 MetricRegistry 注册到 Metrics 的度量仓库中的。Metrics 是codahale 提供的第三方度量仓库,这里的 MetricRegistry 是 Metrics 提供的API
- running : 用于标记当前 MetricsSystem 是否正在运行
- metricsServlet : metricsServlet 将在添加 ServletContextHandler 后通过 WebUI 展示。metricsServlet 的类型是Option[MetricsServlet]
MetricsConfig 简析
MetricsConfig 的属性 简析
- properties : 度量的属性信息。类型为 Properties
- perlnstanceSubProperties : 每个实例的子属性。缓存每个实例与其属性的映射关系,类型为 HashMap[String,Properties]
MetricsConfig 的内部方法 简析
// 用于给 MetricsConfig 的 properties 中添加默认的度量属性,这里有四个默认的度量属性
private def setDefaultProperties(prop: Properties) {
prop.setProperty("*.sink.servlet.class", "org.apache.spark.metrics.sink.MetricsServlet")
prop.setProperty("*.sink.servlet.path", "/metrics/json")
prop.setProperty("master.sink.servlet.path", "/metrics/master/json")
prop.setProperty("applications.sink.servlet.path", "/metrics/applications/json")
}
// 从指定的文件为 MetricsConfig 的 properties 加载度量属性
// 我们知道如果没有指定度量属性文件或者此文件不存在,那么将从类路径下的属性文件 metrics.properties(即常量DEFAULT_METRICS_CONF_FILENAME 的值)中加载度量属性。读取类路径下的文件时使用了工具类 Utils 的 getSparkClassLoader 方法
private[this] def loadPropertiesFromFile(path: Option[String]): Unit = {
var is: InputStream = null
try {
is = path match {
case Some(f) => new FileInputStream(f)
case None => Utils.getSparkClassLoader.getResourceAsStream(DEFAULT_METRICS_CONF_FILENAME)
}
if (is != null) {
properties.load(is)
}
} catch {
case e: Exception =>
val file = path.getOrElse(DEFAULT_METRICS_CONF_FILENAME)
logError(s"Error loading configuration file $file", e)
} finally {
if (is != null) {
is.close()
}
}
}
// 对于 properties 中的每个属性 kv,通过正则表达式匹配找出 kv 的 key 的前缀和后缀,以前缀为实例,后缀作为新的属性 kv2 的key,kv 的 value 作为新的属性 kv2 的 value,最后将属性 kv2 添加到实例对应的属性集合中作为实例的属性
def subProperties(prop: Properties, regex: Regex): mutable.HashMap[String, Properties] = {
val subProperties = new mutable.HashMap[String, Properties]
prop.asScala.foreach { kv =>
if (regex.findPrefixOf(kv._1.toString).isDefined) {
val regex(prefix, suffix) = kv._1.toString
subProperties.getOrElseUpdate(prefix, new Properties).setProperty(suffix, kv._2.toString)
}
}
subProperties
}
这里以KV对".sink.servlet.class"->“org.apache.spark.metrics.sink.MetricsServlet” 和 ".sink.servlet.path"->"/metrics/json"为例
首先使用正则表达式 ^(*|[a-zA-Z]+).(.+) 匹配出两个元组,那么第一个KV对的两个元组分别为 * 和 sink.servlet.class 。第二个KV对的两个元组分别为 * 和 sink.servlet.path 。然后以第一个元组作为前缀,第二个元组作为后缀。
以前缀为实例,后缀为新属性的 key,KV 对的 value 作为新属性的 value。最后将新属性添加到实例对应的 Properties 中,将实例与 Properties 之间的映射关系存入 Map。结果如下:
Мар(
*->{
sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet,
sink.servlet.path=/metrics/json
}
)
根据这个例子,4个默认度量属性提取出实例的属性后为 : 即实例 applications、master、* 与它们对应的属性集合
Map(
applications->{
sink.servlet.path=/metrics/applications/json
}.
master->{
sink.servlet.path=/metrics/master/json
},
*->{
sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet,
sink.servlet.path=/metrics/json
}
)
// MetricsConfig的初始化过程
def initialize() {
// 设置默认属性
setDefaultProperties(properties)
// 从文件中加载度量属性。可以通过 spark.metrics.conf 属性指定度量属性文件
loadPropertiesFromFile(conf.getOption("spark.metrics.conf"))
val prefix = "spark.metrics.conf."
conf.getAll.foreach {
// 从 SparkConf 中查找以 spark.metrics.conf. 为前缀的配置属性,并且截取 key 的前缀后的部分作为度量属性的key/value 不变
case (k, v) if k.startsWith(prefix) =>
properties.setProperty(k.substring(prefix.length()), v)
case _ =>
}
// 提取实例的属性。这里指定了正则表达式 ^(*I[a-zA-Z]+).(.+) 找出 properties 中各个实例的属性
perInstanceSubProperties = subProperties(properties, INSTANCE_REGEX)
if (perInstanceSubProperties.contains(DEFAULT_PREFIX)) {
val defaultSubProperties = perInstanceSubProperties(DEFAULT_PREFIX).asScala
// 向子属性中添加缺失的默认子属性(所谓默认子属性,即在 Map 中以 * 为 key 的属性)
for ((instance, prop) <- perInstanceSubProperties if (instance != DEFAULT_PREFIX);
(k, v) <- defaultSubProperties if (prop.get(k) == null)) {
prop.put(k, v)
}
}
}
为了理解更加简单,这里以上面说明中得到的Map为例
默认子属性中的 sink.servlet.class 属性是其他两个(applications 和 master)子属性中不具有的属性,因此需要将此属性添加到以 applications 和 master 为 key 的子属性中。因此得到的结果为:
Map(applications->{
sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet,
sink.servlet.path=/metrics/applications/json
},
master->{
sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet,
sink.servlet.path=/metrics/master/json
},
*->{
sink.servlet.class=org.apache.spark.metrics.sink.MetricsServlet,
sink.servlet.path=/metrics/json
}
)
经过以上处理,每个度量实例与其属性集合的信息都存储在 perlnstanceSubProperties 了
在默认配置下,Spark 度量系统的属性配置只有 Sink,却没有 Source,没有了度量源,光有输出能有什么用?度量系统中的 Sink 都通过 Spark 属性或者从文件中加载,而 Source 除了与 Sink 相同的两种方式外,Spark 的实现中,很多地方都是调用 MetricsSystem 的 registerSource 方法注册各种 Source 的。虽然如此,有些 Source 依然需要你在 Spark 属性或者指定的文件中明确配置,就像这样: master.source.jvm.class=org.apache.spark.metrics.source.JvmSource
// getlnstance 方法优先从 perinstanceSubProperties 中获取指定实例的属性,如果 perInstanceSubProperties 中没有存储对应的实例,则返回默认实例(即*)的属性
def getInstance(inst: String): Properties = {
perInstanceSubProperties.get(inst) match {
case Some(s) => s
case None => perInstanceSubProperties.getOrElse(DEFAULT_PREFIX, new Properties)
}
}
MetricsSystem 的常用方法 简析
buildRegistryName 方法
// 用于给Source生成向MetricRegistry中注册的注册
private[spark] def buildRegistryName(source: Source): String = {
// metricsNamespace : 度量命名空间。可以通过 spark.metrics.namespace 属性进行配置,默认读取spark.app.id属性的值
val metricsNamespace = conf.get(METRICS_NAMESPACE).orElse(conf.getOption("spark.app.id"))
// executorld :当前 Executor 的身份标识,通过读取 spark.executor.id 属性获得
val executorId = conf.getOption("spark.executor.id")
val defaultName = MetricRegistry.name(source.sourceName)
// defaultName : 调用 MetricRegistry 的 name 方法生成的默认注册名
if (instance == "driver" || instance == "executor") {
if (metricsNamespace.isDefined && executorId.isDefined) {
// 如果定义了 metricsNamespace 和 executorId,那么调用 MetricRegistry 的 name 方法生成${metricsNamespace}.${executorld}.${defaultName} 格式的注册名
MetricRegistry.name(metricsNamespace.get, executorId.get, source.sourceName)
} else {
if (metricsNamespace.isEmpty) {
logWarning(s"Using default name $defaultName for source because neither " +
s"${METRICS_NAMESPACE.key} nor spark.app.id is set.")
}
if (executorId.isEmpty) {
logWarning(s"Using default name $defaultName for source because spark.executor.id is " +
s"not set.")
}
// 如果metricsNamespace或executorld没有定义,那么采用defaultName为注册名
defaultName
}
} else { defaultName }
}
registerSource 方法
// 用于向MetricsSystem中注册度量源
def registerSource(source: Source) {
sources += source
try {
val regName = buildRegistryName(source)
registry.register(regName, source.metricRegistry)
} catch {
case e: IllegalArgumentException => logInfo("Metrics already registered", e)
}
}
getServletHandlers 方法
// 为了将度量系统和 SparkU I结合起来使用,即将SparkUI的Web展现也作度量系统的Sink之一,需要能将度量输出到 SparkUI 的页面。MetricsServlet 是特质 Sink 的实现之一,但是它还不是一个 ServletContextHandler,因此需有一个转换
def getServletHandlers: Array[ServletContextHandler] = {
require(running, "Can only call getServletHandlers on a running MetricsSystem")
// 调用了 MetricsServlet 的 getHandlers 方法来实现转换
metricsServlet.map(_.getHandlers(conf)).getOrElse(Array())
}
// getHandlers 也调用了 JettyUtils 的 createServletHandler 方法创建 ServletContextHandler
def getHandlers(conf: SparkConf): Array[ServletContextHandler] = {
Array[ServletContextHandler](
createServletHandler(servletPath,
new ServletParams(request => getMetricsSnapshot(request), "text/json"), securityMgr, conf)
)
}
MetricsSystem 的启动 简析
def start(registerStaticSources: Boolean = true) {
require(!running, "Attempting to start a MetricsSystem that is already running")
// 将当前 MetricsSystem 的 running 字段置为 true,即表示 MetricsSystem 已经处于运行状态
running = true
if (registerStaticSources) {
// 将静态的度量来源 CodegenMetrics 和 HiveCatalogMetrics 注册到 MetricRegistry
StaticSources.allSources.foreach(registerSource)
// // 从初始化完成的 MetricsConfig 中获取当前实例的度量来源属性,并调用 registerSources 方法,将这些度量来源注册到MetricRegistry
registerSources()
}
registerSinks()
sinks.foreach(_.start)
}
private def registerSources() {
// 获取当前实例的度量属性。根据 MetricsConfig 的 getlnstance 方法的逻辑,当实例不存在时将返回默认实例的属性。以 driver 实例来讲,默认不存在 driver 的实例属性,因此返回 * 对应的属性
val instConfig = metricsConfig.getInstance(instance)
// 匹配正则表达式 ,获取所有度量源更细粒度的实例及属性
val sourceConfigs = metricsConfig.subProperties(instConfig, MetricsSystem.SOURCE_REGEX)
sourceConfigs.foreach { kv =>
val classPath = kv._2.getProperty("class")
try {
// 使用实例的 class 属性,通过 Java 反射生成度量源的实例,并调用 registerSource 方法将此度量源注册到MetricRegistry
val source = Utils.classForName(classPath).newInstance()
// 从初始化完成的 MetricsConfig 中获取当前实例的度量输出属性,并将这些度量输出注册到sinks
registerSource(source.asInstanceOf[Source])
} catch {
case e: Exception => logError("Source class " + classPath + " cannot be instantiated", e)
}
}
}
private def registerSinks() {
// 获取当前实例的度量属性。根据 MetricsConfig 的 getlnstance 方法,当实例不存在时将返回默认实例的属性。以 driver 实例来讲,默认不存在 driver 的实例属性,因此返回 * 对应的属性
val instConfig = metricsConfig.getInstance(instance)
// 匹配正则表达式 ,获取所有度量源更细粒度的实例及属性
val sinkConfigs = metricsConfig.subProperties(instConfig, MetricsSystem.SINK_REGEX)
sinkConfigs.foreach { kv =>
val classPath = kv._2.getProperty("class")
if (null != classPath) {
try {
// 使用实例的 class 属性,通过 Java 反射生成度量输出的实例。如果当前实例是 servlet,则由 metricsServlet 持有此 servlet 的引用,否则将度量输出实例注册到数组缓冲 sinks 中
val sink = Utils.classForName(classPath)
.getConstructor(classOf[Properties], classOf[MetricRegistry], classOf[SecurityManager])
.newInstance(kv._2, registry, securityMgr)
// 启动 sinks 中的全部度量输出实例
if (kv._1 == "servlet") {
metricsServlet = Some(sink.asInstanceOf[MetricsServlet])
} else {
sinks += sink.asInstanceOf[Sink]
}
} catch {
case e: Exception =>
logError("Sink class " + classPath + " cannot be instantiated")
throw e
}
}
}
}
}
输出提交协调器 简析
当 Spark 应用程序使用了 SparkSQL (包括 Hive)或者需要将任务的输出保存到 HDFS 时,就会用到输出提交协调器OutputCommitCoordinator,OutputCommitCoordinator 将决定任务是否可以提交输出到 HDFS
无论是 Driver 还是 Executor,在 SparkEnv 中都包含了子组件 OutputCommitCoordinator。在 Driver 上注册了OutputCommitCoordinatorEndpoint,所有 Executor 上的 OutputCommitCoordinator 都是通过 OutputCommitCoordinatorEndpoint 的RpcEndpointRef 来询问 Driver 上的 OutputCommitCoordinator,是否能够将输出提交到 HDFS
// 新建OutputCommitCoordinator实例
val outputCommitCoordinator = mockOutputCommitCoordinator.getOrElse {
new OutputCommitCoordinator(conf, isDriver)
}
// 如果当前实例是 Driver,则创建 OutputCommitCoordinatorEndpoint,并且注册到 Dispatcher 中,注册名为OutputCommitCoordinator
// 如果当前应用程序是 Executor,则从远端 Driver 实例的 NettyRpcEnv的Dispatcher 中查找OutputCommitCoordinatorEndpoint 的引用
val outputCommitCoordinatorRef = registerOrLookupEndpoint("OutputCommitCoordinator",
new OutputCommitCoordinatorEndpoint(rpcEnv, outputCommitCoordinator))
// // 无论是 Driver 还是 Executor,最后都由 OutputCommitCoordinator 的属性 coordinatorRef 持有OutputCommitCoordinatorEndpoint 的引用
outputCommitCoordinator.coordinatorRef = Some(outputCommitCoordinatorRef)
OutputCommitCoordinatorEndpoint 的实现简析
private[spark] class OutputCommitCoordinatorEndpoint(
override val rpcEnv: RpcEnv, outputCommitCoordinator: OutputCommitCoordinator)
extends RpcEndpoint with Logging {
logDebug("init") // force eager creation of logger
override def receive: PartialFunction[Any, Unit] = {
case StopCoordinator =>
logInfo("OutputCommitCoordinator stopped!")
stop()
}
override def receiveAndReply(context: RpcCallContext): PartialFunction[Any, Unit] = {
case AskPermissionToCommitOutput(stage, stageAttempt, partition, attemptNumber) =>
context.reply(
outputCommitCoordinator.handleAskPermissionToCommit(stage, stageAttempt, partition,
attemptNumber))
}
}
OutputCommitCoordinatorEndpoint 将接收两个消息
- StopCoordinator : 此消息将停止 OutputCommitCoordinatorEndpoint
- AskPermissionToCommitOutput : 此消息将通过 OutputCommitCoordinator 的 handleAskPermissionToCommit 方法处理,进而确认客户端是否有权限将输出提交到 HDFS
OutputCommitCoordinator 的实现简析
OutputCommitCoordinator 用于判定给定 Stage 的分区任务是否有权限将输出提交到 HDFS,并对同一分区任务的多次任务尝试(TaskAttempt) 进行协调
OutputCommitCoordinator 中有以下属性:
- conf : 即SparkConf
- isDriver : 当前节点是否是 Driver
- coordinatorRef : 即 OutputCommitCoordinatorEndpoint的 NettyRpcEndpointRef 引用
- NO_AUTHORIZED_COMMITTER : 值为-1的常量
- authorizedCommittersByStage : 缓存 Stage 的各个分区的任务尝试。类型为 scala.collection.mutable.Map[Stageld,Array[TaskAttemptNumber]]
SparkEnv 构建
当SparkEnv内的所有组件都实例化完毕,将正式构造 SparkEnv
SparkEnv内部的成员属性:
- isStopped : 当前 SparkEnv 是否停止的状态
- pythonWorkers : 所有 python 实现的 Worker 的缓存
- hadoopJobMetadata : HadoopRDD 进行任务切分时所需要的元数据的软引用。例如,HadoopFileRDD 将使用 hadoopJobMetadata缓存 JobConf 和 InputFormat
- driverTmpDir : 如果当前 SparkEnv 处于 Driver 实例中,那么将创建 Driver 的临时目录