Hive on Spark源码分析(四)—— SparkClilent与SparkClientImpl(下)

Hive on Spark源码分析(一)—— SparkTask
Hive on Spark源码分析(二)—— SparkSession与HiveSparkClient
Hive on Spark源码分析(三)—— SparkClilent与SparkClientImpl(上)
Hive on Spark源码分析(四)—— SparkClilent与SparkClientImpl(下)
Hive on Spark源码分析(五)—— RemoteDriver
Hive on Spark源码分析(六)—— RemoteSparkJobMonitor与JobHandle 


SparkClientImpl提交任务的方法是submit方法,在HiveSparkClient中被调用,提交SparkSession需要提交的任务,其内部其实是调用了内部类ClientProtocol的submit方法:

  
  
  1. @Override
  2. public <T extends Serializable> JobHandle<T> submit(Job<T> job) {
  3. return protocol.submit(job);
  4. }

我们通过ClientProtocol的继承关系来看一下它到底是什么:




从图上可知,ClientProtocol是继承自io.netty.channel.SimpleChannelInboundHandler,它是负责底层的rpc通信与异步任务执行的,其中定义了submit、run、cancel、endSession这些job相关的方法,以及多个签名不同的handle方法,用以处理针对不同消息类型的rpc请求。


下面首先先看一下submit方法的实现,该方法负责提交SparkClient需要提交的任务,其中JobHandle可以认为是一个job的句柄,用来监控和控制一个正在运行的远程任务,会在后面的文章对JobHandle进行详细解析。

  
  
  1. <T extends Serializable> JobHandleImpl<T> submit(Job<T> job) {
  2. //利用java.utils.UUID产生一个jobId
  3. final String jobId = UUID.randomUUID().toString();
  4. //promise由EventLoopGroup分配线程进行执行,并封装在JobHandleImpl中,代理很多方法的实现.
  5. final Promise<T> promise = driverRpc.createPromise();
  6. //构造JobHandle
  7. final JobHandleImpl<T> handle = new JobHandleImpl<T>(SparkClientImpl.this, promise, jobId);
  8. jobs.put(jobId, handle);
  9. //将jobId和job封装成JobRequest类型的消息,交给driverRpc来发送,最终返回的也是一个promise对象保存异步执行结果
  10. final io.netty.util.concurrent.Future<Void> rpc = driverRpc.call(new JobRequest(jobId, job));
  11. LOG.debug("Send JobRequest[{}].", jobId);

这里我们先看一下driverRpc.call方法的实现,其实现实际是调用Rpc类中另一个call方法实现的。首先是检查msg是否为空以及channel是否建立好了
   
   
  1. public <T> Future<T> call(Object msg, Class<T> retType) {
  2. Preconditions.checkArgument(msg != null);
  3. Preconditions.checkState(channel.isActive(), "RPC channel is closed.");

然后为创建一个promise监控当前任务的执行,并产生一个id,通过RpcDispatcher的registerRpc方法进行注册。注册过程就是讲id和promise封装成OutstandingRpc对象添加到rpcs集合中,至于这个集合的作用会在以后分析
    
    
  1. try {
  2. final long id = rpcId.getAndIncrement();
  3. final Promise<T> promise = createPromise();
  4. //注册client
  5. dispatcher.registerRpc(id, promise, msg.getClass().getName());

最后一步是将消息msg和消息头MessageHeader写入channel后flush,并未写入操作和flush操作注册监听器。最后返回promise。需要注意的一点是,channel flush后代表消息成功发送到了RemoteDriver端,由RemoteDriver端的driverProtocol处理(在第五篇文章中会讲到),只有 当clientProtocol收到driverProtocol返回的REPLY消息时,rpc(promise)才会被标记为success
    
    
  1. synchronized (channelLock) {
  2. //write和writeAndFlush都会返回ChannelFuture,如果
  3. //id和message的类型封装为消息头MessageHeader,这里消息的类型为CALL
  4. channel.write(new MessageHeader(id, Rpc.MessageType.CALL)).addListener(listener);
  5. channel.writeAndFlush(msg).addListener(listener);
  6. }
  7. return promise;
  8. } catch (Exception e) {
  9. throw Throwables.propagate(e);
  10. }
  11. }

其中监听器的定义如下:
     
     
  1. ChannelFutureListener listener = new ChannelFutureListener() {
  2. @Override
  3. public void operationComplete(ChannelFuture cf) {
  4. //isDone:任务因为正常终止,异常或者取消都会返回true(即只要是非运行状态)
  5. if (!cf.isSuccess() && !promise.isDone()) {
  6. LOG.warn("Failed to send RPC, closing connection.", cf.cause());
  7. promise.setFailure(cf.cause());
  8. dispatcher.discardRpc(id);
  9. close();
  10. }
  11. }
  12. };

下面回到SparkClientImpl。接下来先给rpc对象(Promise类型)添加监听器,覆盖operationComplete方法,当rpc执行结束时,如果状态为success,则将job的state变为QUEUED;否则标记rpc执行失败。
   
   
  1. rpc.addListener(new GenericFutureListener<io.netty.util.concurrent.Future<Void>>() {
  2. @Override
  3. public void operationComplete(io.netty.util.concurrent.Future<Void> f) {
  4. //如果rpc sent成功,jobHandle的状态从sent改为queued
  5. if (f.isSuccess()) {
  6. handle.changeState(JobHandle.State.QUEUED);
  7. } else if (!promise.isDone()) {
  8. promise.setFailure(f.cause());
  9. }
  10. }
  11. });

再给createPromise返回的promise对象添加监听器,覆盖 operationComplete方法,当promise执行结束时,从job列表中移除相应的job;如果promise被取消,但rpc还没有执行结束,则取消rpc的执行。最后返回job handle
    
    
  1. promise.addListener(new GenericFutureListener<Promise<T>>() {
  2. @Override
  3. public void operationComplete(Promise<T> p) {
  4. if (jobId != null) {
  5. //从任务列表里移除这个job
  6. jobs.remove(jobId);
  7. }
  8. if (p.isCancelled() && !rpc.isDone()) {
  9. rpc.cancel(true);
  10. }
  11. }
  12. });
  13. return handle;

从上面实现我们可以看出,所谓提交任务最终其实就是基于rpc向远程的rpc发送一个消息。同样地,run、cancel和endSession这几个方法,也是通过调用driverRpc来发送消息,只不过消息类型不同,具体如下:
    
    
  1. <T extends Serializable> Future<T> run(Job<T> job) {
  2. @SuppressWarnings("unchecked")
  3. final io.netty.util.concurrent.Future<T> rpc = (io.netty.util.concurrent.Future<T>)
  4. //将job封装成SyncJobRequest发送
  5. driverRpc.call(new SyncJobRequest(job), Serializable.class);
  6. return rpc;
  7. }
  8. void cancel(String jobId) {
  9. //将jobId封装成CancelJob发送
  10. driverRpc.call(new CancelJob(jobId));
  11. }
  12. Future<?> endSession() {
  13.     //直接发送EndSession消息
  14. return driverRpc.call(new EndSession());
  15. }

这些消息类型定义在ClientProtocol的直接基类BaseProtocol中:
    
    
  1. //在RpcDispatcher基础上定义了一系列内部类,代表不同的消息类型(或不同的事件 请求)
  2. abstract class BaseProtocol extends RpcDispatcher {
  3. protected static class CancelJob implements Serializable {...}
  4. protected static class EndSession implements Serializable {...}
  5. protected static class Error implements Serializable {...}
  6. protected static class JobMetrics implements Serializable {...}
  7. protected static class JobRequest<T extends Serializable> implements Serializable {...}
  8. protected static class JobResult<T extends Serializable> implements Serializable {...}
  9. protected static class JobStarted implements Serializable {..}
  10. /**
  11. * Inform the client that a new spark job has been submitted for the client job.
  12. */
  13. protected static class JobSubmitted implements Serializable {...}
  14. protected static class SyncJobRequest<T extends Serializable> implements Serializable {
  15. final Job<T> job;
  16. SyncJobRequest(Job<T> job) {
  17. this.job = job;
  18. }
  19. SyncJobRequest() {
  20. this(null);
  21. }
  22. }
其中:
CancelJob只封装了jobId,因为只需要根据一个jobId就可以将相应的jobcancel掉
EndSession为空信息
Error中封装了异常信息cause
JobMetrics中封装了jobId,SparkJobId,stageId,taskId和metrics
JobResult中封装了结果id,结果result,error以及sparkCounter
JobStarted只封装了一个id
JobSubmitted封装了clientJobId和sparkJobId
SyncJobRequest、JobRequest都是对Job的封装,只不过 JobRequest添加了jobId,两种消息在使用时用来封装不同类型的job。
SyncJobRequest仅用来封装SparkClientImpl中定义的几种可以快速执行的任务,具体如下:
     
     
  1. //添加jar包
  2. private static class AddJarJob implements Job<Serializable> {...}
  3. //添加文件
  4. private static class AddFileJob implements Job<Serializable> {...}
  5. //获得spark executor数
  6. private static class GetExecutorCountJob implements Job<Integer> {...}
  7. //获得spark.default.parallelism
  8. private static class GetDefaultParallelismJob implements Job<Integer> {...}

最后,SparkClientImpl中还定义了多个签名不同的handle方法,形式如下:
     
     
  1. private void handle(ChannelHandlerContext ctx, Error msg) {...}
  2. private void handle(ChannelHandlerContext ctx, JobMetrics msg) {...}
  3. private void handle(ChannelHandlerContext ctx, JobResult msg) {...}
  4. private void handle(ChannelHandlerContext ctx, JobStarted msg) {...}
  5. private void handle(ChannelHandlerContext ctx, JobSubmitted msg) {...}

那么这些方法在什么时候被调用呢?根据前面的继承关系图我们知道,ClientProtocol实际是一个netty中的handler角色,在父类RpcDispatcher的定义中覆盖了SimpleChannelInboundHandler中的channelread0方法。该方法是在收到消息时自动被调用(或者pipeline中前面的handler处理完了,将结果传递到当前handler),我们来看一下 RpcDispatcher中 channelread0方法的具体实现。

该方法的主要内容就是根据收到的消息的消息头(MessageHeader)来判断消息类型,根据不同类型使用不同的方法来处理:
     
     
  1. @Override
  2. protected final void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
  3. if (lastHeader == null) {
  4. if (!(msg instanceof Rpc.MessageHeader)) {
  5. LOG.warn("[{}] Expected RPC header, got {} instead.", name(),
  6. msg != null ? msg.getClass().getName() : null);
  7. throw new IllegalArgumentException();
  8. }
  9. lastHeader = (Rpc.MessageHeader) msg;
  10. } else {
  11. //log debug enable is needed
  12. LOG.debug("[{}] Received RPC message: type={} id={} payload={}", name(),
  13. lastHeader.type, lastHeader.id, msg != null ? msg.getClass().getName() : null);
  14. try {
  15. switch (lastHeader.type) {
  16. case CALL:
  17. handleCall(ctx, msg);
  18. break;
  19. case REPLY:
  20. handleReply(ctx, msg, findRpc(lastHeader.id));
  21. break;
  22. case ERROR:
  23. handleError(ctx, msg, findRpc(lastHeader.id));
  24. break;
  25. default:
  26. throw new IllegalArgumentException("Unknown RPC message type: " + lastHeader.type);
  27. }
  28. } finally {
  29. lastHeader = null;
  30. }
  31. }
  32. }

如果messageHeader的类型为REPLY,则根据header中的id找到对应的future,标记为成功并返回,从列表删除future;如果类型为ERROR,则根据header中的id找到相应的future,标记为失败并返回,从列表删除future。

这里重点看一下header类型为CALL的消息的处理。该类型消息由handleCall方法处理,首先查看handlers列表中是否有处理msg类型消息的handler,如果有直接获得;如果没有,则通过反射机制,找到对应类型的已声明的方法,添加到handler列表
     
     
  1. private void handleCall(ChannelHandlerContext ctx, Object msg) throws Exception {
  2. //得到相应的handle方法
  3. Method handler = handlers.get(msg.getClass());
  4. if (handler == null) {
  5. handler = getClass().getDeclaredMethod("handle", ChannelHandlerContext.class,
  6. msg.getClass());
  7. handler.setAccessible(true);
  8. handlers.put(msg.getClass(), handler);
  9. }

然后通过反射机制调用handler方法,如果返回结果不为null,则将返回消息类型置为REPLY(另一端收到后则会调用handleReply方法处理),否则为NullMessage,然后将结果header id 和reply type封装成MessageHeader写入channel,再写入返回结果后flush
      
      
  1. Rpc.MessageType replyType;
  2. Object replyPayload;
  3. try {
  4. replyPayload = handler.invoke(this, ctx, msg);
  5. if (replyPayload == null) {
  6. replyPayload = new Rpc.NullMessage();
  7. }
  8. replyType = Rpc.MessageType.REPLY;
  9. } catch (InvocationTargetException ite) {
  10. LOG.debug(String.format("[%s] Error in RPC handler.", name()), ite.getCause());
  11. replyPayload = Throwables.getStackTraceAsString(ite.getCause());
  12. replyType = Rpc.MessageType.ERROR;
  13. }
  14. ctx.channel().write(new Rpc.MessageHeader(lastHeader.id, replyType));
  15. ctx.channel().writeAndFlush(replyPayload);
  16. }
如果msg类型为JobResult,那么调用的handler方法则为handle(ChannelHandlerContext ctx, JobResult msg)。

这样我们就知道不同类型的消息是怎样得到不同的handle方法处理的了。至于几个handle方法的具体实现,这里仅以ClientProtocol中的 handle(ChannelHandlerContext ctx, JobStarted msg)为例进行分析。
首先根据msg的id查看jobs列表张是否注册了相应jobHandle,如果有的话,则修改相应job的状态为started,否则输出warn信息
      
      
  1. private void handle(ChannelHandlerContext ctx, JobStarted msg) {
  2. JobHandleImpl<?> handle = jobs.get(msg.id);
  3. if (handle != null) {
  4. handle.changeState(JobHandle.State.STARTED);
  5. } else {
  6. LOG.warn("Received event for unknown job {}", msg.id);
  7. }
  8. }

简单总结SparkClient提交任务的过程,就是通过内部类ClientProtocol的submit方法,根据job的类型,封装成不同类型的消息(JobRequest、SyncJobRequest等),然后服务端rpc收到不同类型的消息后,会根据签名通过反射机制调用相应的handle函数来做不同的处理。



  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值