其实 Spark 在某种层面上和 MR 是一样的,只不过 Spark 更适合于迭代计算的业务。那么二者的区别有哪些呢?我这里只列举几个大层面的东西,细小的东西我们会在下文继续分析:
- 当内存足够的情况下或者数据量相对较小的情况下,Spark RDD之间传输的数据不用上传 HDFS
- Spark 可以将 RDD 存储在内存,供下一个 task 使用
- Spark 适用于迭代计算,如果不是迭代相关的业务问题,Spark 于 MR 的性能差不多
这里对 Spark 从集群启动到最后的 task 执行完毕并返回之行结果的过程做了一个概述,下面首先从资源调度开始分析。
Spark 资源调度源码解析
一、Spark 集群启动:
首先启动 Spark 集群,查看 sbin 下的 start-all.sh 脚本,会先启动 Master:
# Start Master
"${SPARK_HOME}/sbin"/start-master.sh
# Start Workers
"${SPARK_HOME}/sbin"/start-slaves.sh
查看 sbin/start-master.sh 脚本,发现会去执行 org.apache.spark.deploy.master.Master 类,开始在源码中跟进Master,从 main 方法开始:
//主方法
def main(argStrings: Array[String]) {
Thread.setDefaultUncaughtExceptionHandler(new SparkUncaughtExceptionHandler(
exitOnUncaughtException = false))
Utils.initDaemon(log)
val conf = new SparkConf
val args = new MasterArguments(argStrings, conf)
/**
* 创建RPC 环境和Endpoint (RPC 远程过程调用),在Spark中 Driver, Master ,Worker角色都有各自的Endpoint,相当于各自的通信邮箱。
*
*/
val (rpcEnv, _, _) = startRpcEnvAndEndpoint(args.host, args.port, args.webUiPort, conf)
rpcEnv.awaitTermination()
}
在 main 方法中执行了 startRpcEnvAndEndpoint 方法,创建 RPC 环境和 Endpoint(RPC 远程过程调用),详细的说 RpcEnv 是用于接收消息和处理消息的远程通信调用环境,Master 向 RpcEnv 中去注册,不管是 Master,Driver,Worker,Executor 等都有自己的 Endpoint,相当于是邮箱,其他人想跟我通信先要找到我的邮箱才可以。Master启动时会将 Endpoint 注册在 RpcEnv 里面,用于接收,处理消息。跟进 startRpcEnvAndEndpoint 方法:
/**
* 创建RPC(Remote Procedure Call )环境 ,Remote Procedure Call
* 这里只是创建准备好Rpc的环境,后面会向RpcEnv中注册 角色【Driver,Master,Worker,Executor】
*/
val rpcEnv = RpcEnv.create(SYSTEM_NAME, host, port, conf, securityMgr)
/**
* 向RpcEnv 中 注册Master
*
* rpcEnv.setupEndpoint(name,new Master)
* 这里new Master 的Master 是一个伴生类,继承了 ThreadSafeRpcEndpoint,归根结底继承到了 Trait 接口 RpcEndpoint
* 什么是Endpoint?
* EndPoint中存在
* onstart() :启动当前Endpoint
* receive() :负责收消息
* receiveAndReply():接受消息并回复
* Endpoint 还有各自的引用,方便其他Endpoint发送消息,直接引用对方的EndpointRef 即可找到对方的Endpoint
* 以下 masterEndpoint 就是Master的Endpoint引用 RpcEndpointRef 。
* RpcEndpointRef中存在:
* send():发送消息
* ask() :请求消息,并等待回应。
*/
val masterEndpoint: RpcEndpointRef = rpcEnv.setupEndpoint(ENDPOINT_NAME,
new Master(rpcEnv, rpcEnv.address, webUiPort, securityMgr, conf))
val portsResponse = masterEndpoint.askSync[BoundPortsResponse](BoundPortsRequest)
(rpcEnv, portsResponse.webUIPort, portsResponse.restPort)
在该方法有两个功能:
- 创建 RpcEnv,一些细节已在代码中说明
- 向RpcEnv 中 注册Master,一些细节已在代码中说明
继续跟进 create 方法:
//创建RPC 环境
create(name, host, host, port, conf, securityManager, 0, clientMode)
继续跟进 create 方法:
//创建NettyRpc 环境
new NettyRpcEnvFactory().create(config)
在这里会创建一个 NettyRpcEnvFactory 的对象,并调用 create 方法,看一下在 实例化 NettyRpcEnvFactory 时有哪些操作:
/**
* 创建nettyRPC通信环境。
*/
val nettyEnv =
new NettyRpcEnv(sparkConf, javaSerializerInstance, config.advertiseAddress,
config.securityManager, config.numUsableCores)
该方法的作用是创建nettyRPC通信环境,并且在 new NettyRpcEnv 时会做一些初始化:
- Dispatcher:这个对象中有存放消息的队列和消息的转发
- TransportContext:可以创建NettyRpcHandler
private val dispatcher: Dispatcher = new Dispatcher(this, numUsableCores)
private val transportContext = new TransportContext(transportConf,
new NettyRpcHandler(dispatcher, this, streamManager))
Dispatcher 的作用是存放消息的队列和消息的转发,首先看 Dispatcher 实例化时执行的逻辑:
private val threadpool: ThreadPoolExecutor = {
val availableCores =
if (numUsableCores > 0) numUsableCores else Runtime.getRuntime.availableProcessors()
val numThreads = nettyEnv.conf.getInt("spark.rpc.netty.dispatcher.numThreads",
math.max(2, availableCores))
val pool = ThreadUtils.newDaemonFixedThreadPool(numThreads, "dispatcher-event-loop")
for (i <- 0 until numThreads) {
pool.execute(new MessageLoop)
}
pool
}
在 Dispatcher 实例化的过程中会创建一个 threadpool,在 threadpool 中会执行 MessageLoop:
private class MessageLoop extends Runnable {
override def run(): Unit = {
try {
while (true) {
try {
//take 出来消息一直处理
val data: EndpointData = receivers.take()
if (data == PoisonPill) {
// Put PoisonPill back so that other MessageLoops can see it.
receivers.offer(PoisonPill)
return
}
//调用process 方法处理消息
data.inbox.process(Dispatcher.this)
} catch {
case NonFatal(e) => logError(e.getMessage, e)
}
}
} catch {
case ie: InterruptedException => // exit
}
}
}
在 MessageLoop 中的 receivers.take() 会一直向 receivers消息队列中去数据,而 receivers 是在 Dispatcher 初始化时创建的,至此接收消息的程序已经启动起来:
private val receivers = new LinkedBlockingQueue[EndpointData]
其中会传入一个 EndpointData 对象,实例化时会实例化一个 Inbox 对象:
private class EndpointData(
val name: String,
val endpoint: RpcEndpoint,
val ref: NettyRpcEndpointRef) {
//将endpoint封装到Inbox中
val inbox = new Inbox(ref, endpoint)
}
实例化 Inbox 对象,当注册 endpoint 时都会调用一个异步方法,messages中放一个OnStart样例类(消息队列),所以早默认情况下都会调用 OnStart 的匹配方法:
inbox.synchronized {
messages.add(OnStart)
}
在实例化 MessageLoop 时还会调用 process 方法处理消息:
//调用process 方法处理消息
data.inbox.process(Dispatcher.this)
在 process 中就会找到与发送消息所匹配的 case 去执行逻辑,例如:
case OnStart =>
//调用Endpoint 的onStart方法
endpoint.onStart()
if (!endpoint.isInstanceOf[ThreadSafeRpcEndpoint]) {
inbox.synchronized {
if (!stopped) {
enableConcurrent = true
}
}
}
下面来分析 transportContext 对象的作用,在创建完该类的实例之后,会调用 transportContext.createServer 方法去启动 NettyRpc 的服务,在创建 Rpc 服务的过程中,会创建将处理消息的对象 createChannelHandler:
server = transportContext.createServer(bindAddress, port, bootstraps)
在 createServer 方法中会实例化 TransportServer 的对象,在 try 中会调用 init 方法进行初始化:
try {
//运行初始化init方法
init(hostToBind, portToBind);
}
在 init 方法中,Rpc 的远程通信对象 bootstrap 会调用 childHandler 方法,会初始化网络通信管道:
//初始化网络通信管道
context.initializePipeline(ch, rpcHandler);
在初始化网络通信管道的过程中,创建处理消息的 channelHandler 对象,该对象的作用是
- 创建并处理客户端的请求消息和服务消息
//创建处理消息的 channelHandler
TransportChannelHandler channelHandler = createChannelHandler(channel, channelRpcHandler);
private TransportChannelHandler createChannelHandler(Channel channel, RpcHandler rpcHandler) {
TransportResponseHandler responseHandler = new TransportResponseHandler(channel);
TransportClient client = new TransportClient(channel, responseHandler);
TransportRequestHandler requestHandler = new TransportRequestHandler(channel, client,
rpcHandler, conf.maxChunksBeingTransferred());
return new TransportChannelHandler(client, responseHandler, requestHandler,
conf.connectionTimeoutMs(), closeIdleConnections);
}
TransportChannelHandler 由以上 responseHandler,client,requestHandler 三个 handler 构建,并且这个对象中有 channelRead 方法,用于读取接收到的消息:
@Override
public void channelRead(ChannelHandlerContext ctx, Object request) throws Exception {
//判断当前消息是请求的消息还是回应的消息
if (request instanceof RequestMessage) {
requestHandler.handle((RequestMessage) request);
} else if (request instanceof ResponseMessage) {
responseHandler.handle((ResponseMessage) request);
} else {
ctx.fireChannelRead(request);
}
}
以 requestHandler 为例,调用 headle —> processRpcRequest((RpcRequest) request),会看到 rpcHandler.receive,此时调用的是 NettyRpcHandler 的 receive:
try {
/**
* rpcHandler 是一直传过来的 NettyRpcHandler
* 这里的receive 方法 是 NettyRpcHandler 中的方法
*/
rpcHandler.receive(reverseClient, req.body().nioByteBuffer(), new RpcResponseCallback() {
@Override
public void onSuccess(ByteBuffer response) {
respond(new RpcResponse(req.requestId, new NioManagedBuffer(response)));
}
@Override
public void onFailure(Throwable e) {
respond(new RpcFailure(req.requestId, Throwables.getStackTraceAsString(e)));
}
});
}
------------------------------------------------------------------------
override def receive(
client: TransportClient,
message: ByteBuffer,
callback: RpcResponseCallback): Unit = {
val messageToDispatch = internalReceive(client, message)
//dispatcher负责发送远程的消息,都最终调到postMessage 方法
dispatcher.postRemoteMessage(messageToDispatch, callback)
}
继续调用 dispatcher.postRemoteMessage 方法:
def postRemoteMessage(message: RequestMessage, callback: RpcResponseCallback): Unit = {
val rpcCallContext =
new RemoteNettyRpcCallContext(nettyEnv, callback, message.senderAddress)
val rpcMessage = RpcMessage(message.senderAddress, message.content, rpcCallContext)
postMessage(message.receiver.name, rpcMessage, (e) => callback.onFailure(e))
}
在 postRemoteMessage 中,无论是请求消息还是回应消息,都最终会执行到这个 postMessage:
private def postMessage(
endpointName: String,
message: InboxMessage,
callbackIfStopped: (Exception) => Unit): Unit = {
val error = synchronized {
//获取消息的通信邮箱名称
val data = endpoints.get(endpointName)