背景
最近在做离线batch任务执行的中间件,目标将线上所有的批任务都接过来,以便Hive向Spark
迁移,对任务整个链路追踪(从开始预执行,到执行引擎选择,到执行日志收集,到执行完成后结果分析 是否倾斜等等)。
在做自适应选择执行引擎的时候,定义了一个proposer,里面写了一些规则,来决定使用Spark还是Hive。
现象
在大量并发提交任务时,会出先proposer不生效的情况,应该用Hive执行的SQL却用了Spark。
问题代码
masterSchedule.scheduleAtFixedRate(() -> {
while (scheduleQueue.size() > 0) { // 存放任务的优先阻塞队列中有任务
try {
JobHistory tmpJob = scheduleQueue.take(); // 拿个出队列
if (proposer.preHandler(tmpJob.getSqlAll()); != 0) { // 通过判断该任务的SQL,来确定用Hive还是Spark执行
// 用Hive执行
continue;
}
// 不用Hive就把任务放回去,准备用Spark执行
scheduleQueue.put(tmpJob);
// 先获取一个Spark ThrifServer连接,然后直接丢给执行线程池执行Spark任务
executeJobPool.execute(() -> {
// 用Spark执行
JobHistory jobHistory = null;
try {
// 任务出队列
jobHistory = scheduleQueue.take();
} catch (InterruptedException e) {
...
}
// 省略Spark执行逻辑
});
} catch (SQLException e) {
...
}
}
}, 5, 5, TimeUnit.SECONDS);
问题所在
用了2个线程池,一个是定时扫描任务队列的线程池,一个是Spark任务执行的线程池。
所有的用到的组件都是线程安全的,那么只可能是代码逻辑上非线程安全。
发现,在两个线程池调用之间,任务队列scheduleQueue被公用了,这就是问题的关键。
仔细一想:由于是优先阻塞队列,本来判断好应该用Spark执行的job,由于优先级put回队首后,还没被下面take出来,就已经被别的线程执行proposer时,把该job从队首take出来了,从而导致了Spark这边take出来的不是刚刚put到队首的那个任务了。
改进方法
之前逻辑是先判断用Hive还是Spark,如果是Spark,再去向ThriftServer连接池拿连接。
改进后先拿Spark连接,拿到了连接再去判断用Hive还是Spark,如果是Spark则直接执行,避免了put回去,如果是Hive则用Hive逻辑执行。但这样会导致Spark的负载成为了执行Hive任务的瓶颈,已经能满足目前业务需求,计划之后再进行优化。
总结
在存在多个线程池嵌套使用时,一定要多多关注只被一个线程池覆盖的代码是否线程安全或是否会引发多线程不一致的问题(本例子就是代码线程安全但逻辑非线程安全)。