Shell任务调度实战

Shell任务调度实战

需求

平台侧主动调度shell脚本的执行,结合业务进行各种操作。

机器配置:

shell执行侧:多台16G,16核虚机

平台侧:64G物理机,120核

挑战:
  1. shell脚本执行是进程级,开销较大
  2. shell脚本执行时间很长,需要连接Hive数仓
  3. 队列所具备的连接数有限
  4. 此次任务无法使用消息队列(增加架构成本)
思考
  1. 由于平台侧的资源较丰富,对于任务的调度策略,分配策略应当放在平台侧。shell执行侧不应有太大的负担。
  2. 每台shell执行测的机器的并发度需要合理控制,同一时间内,不应有过多的进程并发执行。
  3. 队列等参数应当由用户提供,各部门使用对应自己的队列名。
  4. 需要给任务分配的线程池参数合理分配,和其它业务的线程池进行资源隔离。
  5. shell执行器应当对执行日志进行采集,每次任务执行并行的开一个线程进行日志写入,防止JVM缓存区溢出
  6. 平台侧需要记录任务的执行状态。
  7. 平台侧和shell调度侧第一版用HTTP的方式进行通信。

实现

shell执行侧核心代码

java 复制代码
    private final ThreadPoolExecutor logExecutor = new ThreadPoolExecutor(10, 20,
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable>(50)); 
​
@PostMapping("/execute")
    public Boolean execute(@RequestBody List<String> paramMap) {
        StringBuilder command = new StringBuilder();
        if(StringUtils.isEmpty(SHELL_PATH)){
            command = new StringBuilder("sh " + path);
        }else {
            command = new StringBuilder("sh " + SHELL_PATH);
        }
        
        for (String s : paramMap) {
            command.append(" ").append(s);
        }
        
        Process process = null;
        try {
            // 执行命令, 返回一个子进程对象(命令在子进程中执行)
            logger.info("command ,{}",command);
            process = Runtime.getRuntime().exec(command.toString());
            
            // 获取命令执行结果, 有两个结果: 正常的输出 和 错误的输出(PS: 子进程的输出就是主进程的输入)
            Process finalProcess = process;
            logExecutor.execute(()->{
                String line;
                BufferedReader bufrIn = new BufferedReader(new InputStreamReader(finalProcess.getInputStream()));
                try {
                    while ((line = bufrIn.readLine()) != null) {
                        logger.info("log % {}",line);
                    }
                }catch (Exception e){
                    logger.error(e.getMessage());
                }finally {
                    try {
                        bufrIn.close();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            logExecutor.execute(()->{
                String line;
                BufferedReader bufrError = new BufferedReader(new InputStreamReader(finalProcess.getErrorStream()));
                try {
                    while ((line = bufrError.readLine()) != null) {
                        logger.info("log % {}",line);
                    }
                }catch (Exception e){
                    logger.error(e.getMessage());
                }finally {
                    try {
                        bufrError.close();
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            // 方法阻塞, 等待命令执行完成(成功会返回0)
            process.waitFor();
            int exitVal = process.waitFor();
            if (0 != exitVal) {
                logger.error("执行脚本失败");
            }
            logger.info("执行脚本成功");
        }catch (Exception e){
            logger.error(e.getMessage());
        }
        return true;
    }

平台侧代码

用户提交任务

less 复制代码
taskCache.addTask(new ShellMonitorTask(byId.getCloud(),bizDay,configId,UserCache.getUserInfo()));
arduino 复制代码
public class ShellMonitorTask implements Runnable{
    
    
    private static final Logger logger = LoggerFactory.getLogger(ShellMonitorTask.class);
    
    
    private final String cloud;
    
    
    private String up;
    
    
    private final String bizDay;
    
    
    private final String configId;
    
    
    private final HiveComplementClient client = SpringContextUtils.getBean(
            HiveComplementClient.class);
    
    
    private final HiveTableComplementLogMapper mapper = SpringContextUtils.getBean(
            HiveTableComplementLogMapper.class);
    
    
    private final ShellExecutionTaskCache taskCache = SpringContextUtils.getBean(
            ShellExecutionTaskCache.class);
    
    
    public ShellMonitorTask(String cloud, String bizDay, String configId,String up) {
        this.cloud = cloud;
        this.bizDay = bizDay;
        this.configId = configId;
        this.up = up;
    }
    
    @Override
    public void run() {
        logger.info("=== 开始执行补数任务,param:{},{},{},{}",configId,bizDay,cloud,up);
        HiveTableComplementLog log = addLog();
        //尝试获取执行凭证,否则阻塞
        taskCache.acquire();
        //执行shell脚本
        executeShellScript(log);
        //释放凭证
        taskCache.release();
        //更新执行日志,确定状态
        updateLog(log);
        logger.info("=== 补数任务结束");
    }
    
    private void executeShellScript(HiveTableComplementLog log){
        String res = "true";
        //根据注册表,其它传入字段进行HTTP/rpc 阻塞请求
    }
    
    private HiveTableComplementLog addLog(){
        HiveTableComplementLog log = new HiveTableComplementLog();
        log.setStartTime(new Date());
        log.setConfigId(configId);
        log.setBizDay(bizDay);
        log.setCp(up);
        log.setStatus(HiveTableComplementLogStatus.RUNNING.getValue());
        mapper.add(log);
        return log;
    }
    
    private void updateLog(HiveTableComplementLog log){
        log.setFinishTime(new Date());
        mapper.update(log);
    }
}

单线程循环取任务,这部分代码在Nacos发送注册请求 时,和Netty统一处理连接时的思想类似

java 复制代码
@Component
public class ShellExecutionTaskCache {
    
 
    private static final Logger logger = LoggerFactory.getLogger(ShellExecutionTaskCache.class);
    
    //任务容器
    private static final BlockingDeque<ShellMonitorTask> SHELL_TASKS = new LinkedBlockingDeque<>();
    
    //许可证,控制shell执行吞吐,目前并发度是7
    private static final Semaphore RUNNING_VOUCHER = new Semaphore(7);
    
    //利用信号量,如果获取不到凭证就直接阻塞
    public void acquire(){
        try {
            RUNNING_VOUCHER.acquire(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    //利用信号量释放凭证
    public void release(){
        RUNNING_VOUCHER.release(1);
    }
    
    public void addTask(ShellMonitorTask task)  {
        try {
            SHELL_TASKS.put(task);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    
    @PostConstruct
    public void init() {
       start();
    }
    
    //单线程循环控制任务执行顺序
    private void start(){
        Thread t = new Thread(() -> {
            for (; ; ) {
                ShellMonitorTask pop = null;
                try {
                    pop = SHELL_TASKS.take();
                } catch (InterruptedException e) {
                    logger.error("shell 执行器异常{}",e.getMessage());
                }
                assert pop != null;
                //单例模式获取 Shell执行的连接池
                ShellExecetionPool.executor().execute(pop);
            }
        });
        //设置守护线程
        t.setDaemon(true);
        t.start();
    }
}
​

有待改善

虽然我们能控制每台机器的并发量,正常情况执行侧不会出现问题。但很多异常是我们意想不到的,如果后续需要改进,可能会借助Zookeeper,Nacos之类的中间件,利用其心跳机制,进行各个执行侧健康状态的确认和保活。但又增加了架构成本,真累,不弄了

相关推荐
Theodore_10224 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
冰帝海岸5 小时前
01-spring security认证笔记
java·笔记·spring
世间万物皆对象5 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
没书读了5 小时前
ssm框架-spring-spring声明式事务
java·数据库·spring
小二·6 小时前
java基础面试题笔记(基础篇)
java·笔记·python
开心工作室_kaic6 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
懒洋洋大魔王6 小时前
RocketMQ的使⽤
java·rocketmq·java-rocketmq
武子康6 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
qq_17448285756 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
转世成为计算机大神7 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式