Hive on Spark源码分析(一)—— SparkTaskHive on Spark源码分析(二)—— SparkSession与HiveSparkClientHive on Spark源码分析(三)—— SparkClilent与SparkClientImpl(上)Hive on Spark源码分析(四)—— SparkClilent与SparkClientImpl(下)Hive on Spark源码分析(五)—— RemoteDriverHive on Spark源码分析(六)—— RemoteSparkJobMonitor与JobHandle
之所以首先分析SparkTask的源码的原因,是根据Hive on Spark的运行模式和任务提交流程的出来的。首先我们看一下Hive on Spark运行模式:
Hive on Spark(HOS)目前支持两种运行模式:本地(local)和远程(remote)。当用户把Spark Master URL设置为local时,采用本地模式;其余情况则采用远程模式。本地模式下,SparkContext与客户端运行在同一个JVM中;远程模式下,SparkContext运行在一个独立的JVM中。本地模式通常仅用于调试。因此我们主要分析一下远程模式(Remote SparkContext,RSC)。下图展示了RSC的工作原理。
图1
用户的每个Session会创建一个SparkClient,SparkClient会启动RemoteDriver进程,并由RemoteDriver创建SparkContext。SparkTask执行时,通过Session提交任务,任务的主体就是相应的SparkWork。SparkClient将任务提交给RemoteDriver,并返回一个SparkJobRef,通过该SparkJobRef,客户端可以监控任务执行进度,进行错误处理,以及采集统计信息等。由于最终的RDD计算没有返回结果,因此客户端只需要监控执行进度而不需要处理返回值。RemoteDriver通过SparkListener收集任务级别的统计数据,通过Accumulator收集Operator级别的统计数据(Accumulator被包装为SparkCounter),并在任务结束时返回给SparkClient。
SparkClient与RemoteDriver之间通过基于Netty的RPC进行通信。除了提交任务,SparkClient还提供了诸如添加Jar包、获取集群信息等接口。如果客户端需要使用更一般的SparkContext的功能,可以自定义一个任务并通过SparkClient发送到RemoteDriver上执行。
因此在接下里的几篇文章里,我将会针对上面提到的Session、SparkClient、RemoteDriver,以及与job监控相关的JobMonitor进行分析,并且主要针对远程模式的相关实现类。
首先根据上面的运行模式介绍可知,Session是HOS提交任务的起点。而在HOS的代码中,Session是在SparkTask中创建的,然后一步一步将Task中包含的SparkWork进行提交。SparkTask继承了Hive中各种任务类型统一的父类Task。SparkTask的核心是execute方法,该方法负责session的创建,以及SparkWork的提交。
@Override public int execute(DriverContext driverContext) { //返回码初始为0 int rc = 0; //创建session,以及用来管理多个session的sessionManager SparkSession sparkSession = null; SparkSessionManager sparkSessionManager = null;
try { //打印一些提示能够控制sparkWork的reducer数目的参数的信息 printConfigInfo(); sparkSessionManager = SparkSessionManagerImpl.getInstance(); sparkSession = SparkUtilities.getSparkSession(conf, sparkSessionManager);
其实在getSparkSession的过程中,经过一系列调用,最终会创建一个RpcServer实例,这个RpcServer是与sparkSession和SparkClient在同一个线程中,用来与RemoteDriver端的clientRpc进行通信,提交任务,处理返回信息。
SparkSessionManagerImpl.setup => SparkClientFactory.initialize => new RpcServer。
new RpcServer中通过以下代码创建一个ServerBootstrap
public RpcServer(Map<String, String> mapConf) throws IOException, InterruptedException { this.config = new RpcConfiguration(mapConf); this.group = new NioEventLoopGroup( this.config.getRpcThreadCount(), new ThreadFactoryBuilder() .setNameFormat("RPC-Handler-%d") .setDaemon(true) .build()); this.channel = new ServerBootstrap() .group(group) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throws Exception { SaslServerHandler saslHandler = new SaslServerHandler(config); final Rpc newRpc = Rpc.createServer(saslHandler, config, ch, group); saslHandler.rpc = newRpc; Runnable cancelTask = new Runnable() { @Override public void run() { LOG.warn("Timed out waiting for hello from client."); newRpc.close(); } }; saslHandler.cancelTask = group.schedule(cancelTask, RpcServer.this.config.getServerConnectTimeoutMs(), TimeUnit.MILLISECONDS); } }) .option(ChannelOption.SO_BACKLOG, 1) .option(ChannelOption.SO_REUSEADDR, true) .childOption(ChannelOption.SO_KEEPALIVE, true) .bind(0) .sync() .channel(); this.port = ((InetSocketAddress) channel.localAddress()).getPort(); this.pendingClients = Maps.newConcurrentMap(); this.address = this.config.getServerAddress(); }
这个RpcServer,以及后面会在RemoteDriver中创建的Rpc,就是图1中HiveClient与RemoteDriver通信的基础。
下面继续回到execute方法中。上面创建好session后,接下来获取sparkWork,通过sparkSession.submit方法想SparkClient提交任务。提交任务具体的调用过程是:sparkSession.submit => RemoteHiveSparkClient.execute => RemoteHiveSparkClient.submit =>
jobRef是一个spark job的引用,包括jobId,jobStatus和jobHandle等信息,后面会用来监控任务执行状态
SparkWork sparkWork = getWork(); sparkWork.setRequiredCounterPrefix(getOperatorCounters()); perfLogger.PerfLogBegin(CLASS_NAME, PerfLogger.SPARK_SUBMIT_JOB); SparkJobRef jobRef = sparkSession.submit(driverContext, sparkWork);
调用monitorJob监控提交的job。monitorJob通过最终调用的是RemoteSparkJobMonitor的startMonitor,循环获取job的执行状态,并根据不同状态修改rc并返回
rc = jobRef.monitorJob();
以delegate的方式,通过jobHandle的相关方法返回具体信息
SparkJobStatus sparkJobStatus = jobRef.getSparkJobStatus();
下面根据rc的值进行不同的处理。当rc==0时,表示任务执行成功,这里则记录任务执行情况的统计数据
if (rc == 0) { SparkStatistics sparkStatistics = sparkJobStatus.getSparkStatistics(); if (LOG.isInfoEnabled() && sparkStatistics != null) { LOG.info(String.format("=====Spark Job[%s] statistics=====", jobRef.getJobId())); logSparkStatistic(sparkStatistics); } LOG.info("Execution completed successfully");
当rc==2时,表示任务提交超时,此时取消任务
} else if (rc == 2) { jobRef.cancelJob(); } sparkJobStatus.cleanup();
在remoteSparkJobMonitor.startMonitor中,只有抛出异常时会将rc置为1,因此这里不需要单独判断,直接在catch中处理
} catch (Exception e) { String msg = "Failed to execute spark task, with exception \'" + Utilities.getNameMessage(e) + "\'"; // Has to use full name to make sure it does not conflict with // org.apache.commons.lang.StringUtils console.printError(msg, "\\n" + org.apache.hadoop.util.StringUtils.stringifyException(e)); LOG.error(msg, e); rc = 1;
最后通过Utilities.clearWork清楚work的相关信息,删除任务目录
} finally { Utilities.clearWork(conf); if (sparkSession != null && sparkSessionManager != null) { rc = close(rc); try { sparkSessionManager.returnSession(sparkSession); } catch (HiveException ex) { LOG.error("Failed to return the session to SessionManager", ex); } } } return rc; }
简单总结:spark session将SparkTask中的SparkWork进行异步提交(具体提交任务链会在后面的文章中一步一步都分析到),并获得一个所提交任务的引用jobRef,通过这个引用可以对job进行监控,在一定时间阈值内循环获取异步任务的执行状态,并相应的修改返回码为不同的值,最终根据返回码的不同值进行不同的处理,对任务结果做出标识。
SparkTask中还有如上面提到的printConfigInfo、addToHistory,以及其他一些方法,实现都比较简单,而且不涉及到任务提交流程,所以感兴趣的同学可以自己阅读一些相关源码。
参考文献:
[1]. I ntel李锐:Hive on Spark解析