Hera调度系统运行时架构源码分析

目录

一、Hera启动过程

二、Master节点启动流程

三、Worker节点启动流程

四、心跳机制实现

五、任务调度执行流程

六、架构特点总结


在笔者的职业生涯中,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();
    });
}
六、架构特点总结
  1. 分布式锁选主:通过DB锁实现Master选举

  2. 双通道通信

    • Master-Worker:Netty长连接

    • 调度指令:Quartz + 内存队列

  3. 负载均衡:基于心跳数据的动态负载策略

MasterWorkHolder getRunnableWork(JobElement element) {
    return LoadBalanceFactory.getLoadBalance()
        .getWork(element, masterContext.getWorkMap().values());
}
  1. 容错机制

    • 心跳超时(10秒)切换Master

    • 任务超时(3小时)自动终止

    • 漏跑检测守护线程

系统通过将调度逻辑(Master)与任务执行(Worker)分离,结合Netty实现高效通信,Quartz保证调度精度,内存队列提升吞吐量,构建了高可用的分布式调度架构。


参考:Hera设计解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GawynKing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值