目录
在笔者的职业生涯中,Hera 调度系统是使用过的所有开源调度系统中最符合用户操作习惯、最贴近业务实际需求的一款产品 —— 没有之一。若论产品成熟度与用户体验,或许只有部分大厂自研的调度平台才能与之比肩。
与 DolphinScheduler 等主流开源调度系统相比,Hera 的设计理念更加贴近“开箱即用”与“灵活配置”的双重需求。无论是任务调度配置逻辑、全局工作流结构,还是任务依赖与执行引擎解耦的策略,都体现出 Hera 对调度系统“易用性”与“强功能”的精准把握。这种优势在实际使用中尤为明显,尤其是在中小团队缺乏大量运维成本投入时,Hera 的轻量与高效特性就显得尤为珍贵。
虽然笔者已多年未在项目中直接使用 Hera,但至今依旧对其架构设计与产品理念保持高度认可。为便于更多从事数据开发、任务调度、DevOps 的同学了解和借鉴 Hera 的实现方式,笔者抽业余时间系统梳理了一版 Hera 调度系统运行时架构与源码分析,希望能为大家在调度系统选型与研发落地中提供实用参考。
本文以笔者熟悉版本梳理,下图是hera类图:
一、Hera启动过程
Hera采用Master-Worker架构,启动时通过分布式锁选举Master节点:
1. 分布式锁初始化
@PostConstruct public void init() { workClient.workSchedule.scheduleAtFixedRate(() -> { try { checkLock(); } catch (Exception e) { e.printStackTrace(); } }, 10, 60, TimeUnit.SECONDS); }
-
每60秒执行
checkLock()
进行Master选举 -
首次启动时创建锁记录:
HeraLock heraLock = HeraLock.builder() .id(1) .host(WorkContext.host) .serverUpdate(new Date()) .subgroup(ON_LINE) .gmtCreate(new Date()) .gmtModified(new Date()) .build(); heraLockService.insert(heraLock);
2. Master/Worker启动逻辑
if (WorkContext.host.equals(heraLock.getHost().trim())) { heraSchedule.startup(); // 启动Master } else { if (/*抢占条件满足*/) { heraLockService.changeLock(WorkContext.host, new Date(), date, heraLock.getHost()); heraSchedule.startup(); // 切换Master } else { heraSchedule.shutdown(); // 关闭Master } } workClient.init(); // 启动Worker workClient.connect(heraLock.getHost().trim()); // 连接Master
二、Master节点启动流程
1. Master核心初始化
public void init() { // 1. 启动Quartz调度服务 this.getQuartzSchedulerService().start(); // 2. 创建任务分发器 dispatcher = new Dispatcher(); // 3. 启动Netty服务 handler = new MasterHandler(this); masterServer = new MasterServer(handler); masterServer.start(HeraGlobalEnvironment.getConnectPort()); // 4. 初始化Master master.init(this); }
2. Netty服务端实现
public MasterServer(final ChannelHandler handler) { serverBootstrap.group(bossGroup, workGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) { ch.pipeline() .addLast("frameDecoder", new ProtobufVarint32FrameDecoder()) .addLast("decoder", new ProtobufDecoder(RpcSocketMessage.SocketMessage.getDefaultInstance())) .addLast("frameEncoder", new ProtobufVarint32LengthFieldPrepender()) .addLast("encoder", new ProtobufEncoder()) .addLast("handler", handler); } }); }
3. Master任务调度核心
public void init(MasterContext masterContext) { // 初始化线程池 executeJobPool = new ThreadPoolExecutor(/*参数*/); // 启动关键守护线程 batchActionCheck(); // 版本生成 waitingQueueCheck(); // 任务扫描 heartCheck(); // 心跳检查 lostJobCheck(); // 漏跑检测 }
任务扫描核心逻辑:
public boolean scan() throws InterruptedException { // 处理调度队列 if (!masterContext.getScheduleQueue().isEmpty()) { JobElement jobElement = masterContext.getScheduleQueue().take(); MasterWorkHolder selectWork = getRunnableWork(jobElement); // 负载均衡 if (selectWork != null) { runScheduleJob(selectWork, jobElement.getJobId()); } } // 类似处理手动队列和调试队列 }
三、Worker节点启动流程
1. Worker初始化
public void init() { // 1. 初始化Netty客户端 bootstrap = new Bootstrap() .group(eventLoopGroup) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new WorkHandler(workContext)); } }); // 2. 启动心跳服务 workSchedule.schedule(new Runnable() { public void run() { workerHandlerHeartBeat.send(workContext); // 每2分钟发送心跳 } }, HeraGlobalEnvironment.getHeartBeat(), TimeUnit.SECONDS); // 3. 日志刷盘服务 workSchedule.scheduleWithFixedDelay(() -> { workContext.getHeraJobHistoryService().updateHeraJobHistoryLog(/*日志*/); }, 0, 5, TimeUnit.SECONDS); }
2. 连接Master
public synchronized void connect(String host) throws Exception { ChannelFuture connectFuture = bootstrap.connect( new InetSocketAddress(host, HeraGlobalEnvironment.getConnectPort())); connectFuture.addListener(future -> { workContext.setServerChannel(FailFastCluster.wrap(future.channel())); }); }
四、心跳机制实现
Worker心跳发送
public boolean send(WorkContext context) { // 采集系统指标 MemUseRateJob memJob = new MemUseRateJob(1); CpuLoadPerCoreJob cpuJob = new CpuLoadPerCoreJob(); // 构造Protobuf消息 RpcHeartBeatMessage.HeartBeatMessage hbm = RpcHeartBeatMessage.HeartBeatMessage.newBuilder() .setHost(WorkContext.host) .setMemTotal(memJob.getMemTotal()) .setMemRate(memJob.getRate()) .setCpuLoadPerCore(cpuJob.getLoadPerCore()) .addAllRunnings(context.getRunning().keySet()) .build(); // 发送心跳 context.getServerChannel().writeAndFlush( RpcSocketMessage.SocketMessage.newBuilder() .setKind(REQUEST) .setBody(hbm.toByteString()) .build()); return true; }
Master心跳处理
public static void handleHeartBeat(MasterContext context, Request request) { HeartBeatMessage heartBeatMessage = HeartBeatMessage.parseFrom(request.getBody()); HeartBeatInfo heartBeatInfo = new HeartBeatInfo( heartBeatMessage.getHost(), heartBeatMessage.getMemRate(), heartBeatMessage.getCpuLoadPerCore(), heartBeatMessage.getRunningsList() ); // 更新Worker状态 MasterWorkHolder workHolder = context.getWorkMap().get(channel); workHolder.setHeartBeatInfo(heartBeatInfo); }
五、任务调度执行流程
1. 任务触发
// 调度中心控制器 public void execute() { workClient.executeJobFromWeb(JobExecuteKind.ManualKind, actionHistory.getId()); }
2. Master任务分配
public void run(HeraJobHistoryVo history) { // 创建任务元素 JobElement element = JobElement.builder() .jobId(history.getActionId()) .hostGroupId(history.getHostGroupId()) .priorityLevel(3) .build(); // 根据触发类型放入不同队列 if (history.getTriggerType() == TriggerTypeEnum.MANUAL) { masterContext.getManualQueue().put(element); } else { masterContext.getScheduleQueue().put(element); } }
3. Worker任务执行
private Future<Response> schedule(WorkContext context, Request request) { return context.getWorkExecuteThreadPool().submit(() -> { // 创建任务上下文 Job job = JobUtils.createScheduleJob(/*参数*/, context); context.getRunning().put(jobId, job); // 执行任务 try { int exitCode = job.run(); // 执行用户代码 StatusEnum status = getStatusFromCode(exitCode); } finally { context.getRunning().remove(jobId); } // 返回执行结果 return Response.newBuilder() .setStatus(exitCode == 0 ? Status.OK : Status.ERROR) .build(); }); }
六、架构特点总结
-
分布式锁选主:通过DB锁实现Master选举
-
双通道通信:
-
Master-Worker:Netty长连接
-
调度指令:Quartz + 内存队列
-
-
负载均衡:基于心跳数据的动态负载策略
MasterWorkHolder getRunnableWork(JobElement element) { return LoadBalanceFactory.getLoadBalance() .getWork(element, masterContext.getWorkMap().values()); }
-
容错机制:
-
心跳超时(10秒)切换Master
-
任务超时(3小时)自动终止
-
漏跑检测守护线程
-
系统通过将调度逻辑(Master)与任务执行(Worker)分离,结合Netty实现高效通信,Quartz保证调度精度,内存队列提升吞吐量,构建了高可用的分布式调度架构。
参考:Hera设计解析