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之类的中间件,利用其心跳机制,进行各个执行侧健康状态的确认和保活。但又增加了架构成本,真累,不弄了

相关推荐
YDS82939 分钟前
DeepSeek RAG&MCP + Agent智能体项目 —— 集成ELK日志管理系统和Prometheus监控系统
java·elk·ai·springboot·agent·prometheus·deepseek
骄马之死8 小时前
SpringMVC + SpringBoot 核心知识点总结
java·spring boot·后端
GoGeekBaird9 小时前
Anthropic技能"(Skills)的经验分享
后端
王码码20359 小时前
多台服务器怎么统一看状态?Beszel 轻量监控,搭起来不费事
运维·服务器·后端·安全·阿里云·接口·web
郑洁文9 小时前
基于Spring Boot的流浪动物救助网站
java·spring boot·后端·毕设·流浪动物救助
螺丝钉code10 小时前
JAVA项目 Claude code CLAUDE.md 到底应该怎么写
java·人工智能·claude code
指令集梦境11 小时前
Cursor + Spring Boot实战:从零写一个RESTful API
spring boot·后端·restful
摇滚侠11 小时前
Maven 入门+高深 单一架构案例 54-59
java·架构·maven·intellij-idea
VidDown11 小时前
Webhook 调试器:让第三方回调“原形毕露”
java·开发语言·javascript·编辑器·postman
码云之上11 小时前
聊聊如何设计一个高效、稳定的 Node.js 接入层
前端·后端·node.js