字节跳动大数据面试题:讲讲Spark资源调度和任务的源码

其实 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)
    
  • 1
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值