混合编排架构:统一调度器设计、任务队列管理、资源池抽象层

好,咱们进入第四章。这一章是整套系统的骨架——混合编排架构。

说白了,就是怎么把实时任务和离线任务放在同一个调度器里管起来。我见过不少团队,实时用一套系统,离线用另一套,中间靠人肉拷贝数据。结果呢?凌晨三点告警响了,值班的人得同时盯着两个控制台,手忙脚乱。

所以这一章,咱们把统一调度器、任务队列、资源池抽象层这三个核心模块拆开讲清楚。

4.1 统一调度器:核心调度循环

统一调度器是整个系统的“大脑”。它不关心任务具体干什么,只关心两件事:什么时候该调度谁,以及资源够不够。

我习惯把调度器拆成三个子模块:

  • 调度触发器:负责接收事件(时间到了、依赖完成、手动触发)
  • 调度决策器:根据优先级、资源、依赖关系决定谁先跑
  • 调度执行器:把决策结果推给资源池,启动任务实例

核心调度循环的伪代码大概长这样:

while (true) {
    // 1. 从队列拉取待调度任务
    task = taskQueue.dequeue()

    // 2. 检查依赖是否全部完成
    if (!checkDependencies(task)) {
        taskQueue.enqueue(task)  // 放回队列,等下次
        continue
    }

    // 3. 检查资源是否充足
    resource = resourcePool.allocate(task.requirement)
    if (resource == null) {
        taskQueue.enqueue(task)  // 资源不够,排队等
        continue
    }

    // 4. 提交执行
    executor.submit(task, resource)
}

嗯,这里要注意:调度循环不能阻塞。我曾经见过一个项目,调度器在资源分配时用了同步锁,结果高并发下整个系统卡死。后来改成异步+回调,问题就解决了。

关键设计原则:调度器只做决策,不执行任务。执行是资源池的事。

4.2 任务队列管理:优先级与公平性

任务队列是调度器的“待办清单”。但这里有个矛盾:实时任务要求低延迟,离线任务要求高吞吐。怎么共存?

我推荐用多级队列方案。说白了,就是给不同优先级的任务分不同的队列:

队列级别 典型任务 调度策略 最大等待时间
P0(紧急) 实时告警、交易处理 立即调度,抢占资源 < 100ms
P1(高优) 准实时ETL、用户交互 优先调度,可等待 < 5s
P2(普通) 离线批处理、报表 按权重轮询 < 30min
P3(低优) 数据归档、模型训练 空闲时调度 无上限

你想想看,如果所有任务都塞一个队列,实时任务会被离线任务堵死。但分队列之后,还得解决一个问题:低优先级任务会不会永远得不到调度?

会的。这就是“饥饿”问题。我之前的做法是加一个老化机制:任务在队列里等得越久,它的优先级就自动提升。比如一个P3任务等了2小时还没跑,系统自动把它提到P2。

避坑指南:我曾经把老化时间设得太短(5分钟),结果所有任务都变成了P0,队列直接崩了。建议老化周期至少是任务正常执行时间的3倍以上。

4.3 资源池抽象层:屏蔽异构资源

资源池抽象层,说白了就是给调度器一个“统一接口”。调度器不需要知道底层是Kubernetes、YARN还是裸机,它只管说“我要2核CPU、4G内存”,资源池去搞定。

我设计的资源池抽象层包含三个核心组件:

  • 资源描述器:定义资源规格(CPU、内存、GPU、磁盘IOPS等)
  • 资源分配器:管理资源分配与回收,支持抢占和预留
  • 资源监控器:实时上报资源使用率,触发弹性伸缩

来看一个资源分配器的接口设计:

// 资源分配器接口
interface ResourceAllocator {
    // 申请资源,返回资源句柄或null
    ResourceHandle allocate(ResourceRequest request);

    // 释放资源
    void release(ResourceHandle handle);

    // 抢占资源(用于高优任务)
    ResourceHandle preempt(ResourceRequest request, int priority);

    // 查询当前资源状态
    ResourceStatus getStatus();
}

这里有个坑:资源碎片。比如一个节点还剩3.5G内存,但新任务要4G,这3.5G就浪费了。我建议在资源分配器里加一个碎片整理策略:定期把分散的小资源合并成大资源,或者用“资源预留”的方式提前锁定。

4.4 三者如何协作:一次完整的调度流程

光讲概念太抽象,咱们走一遍完整流程。假设有一个实时ETL任务(P1)和一个离线报表任务(P3)同时到达:

  1. 任务入队:实时ETL进入P1队列,离线报表进入P3队列
  2. 调度决策:调度器先看P0队列(空),再看P1队列,发现实时ETL任务
  3. 依赖检查:实时ETL依赖的上游数据已经就绪,通过
  4. 资源申请:调度器向资源池申请“4核CPU、8G内存”
  5. 资源分配:资源池发现集群有空闲节点,直接分配
  6. 任务执行:实时ETL开始运行
  7. 后续调度:等实时ETL跑完,资源释放,调度器再处理P3队列的离线报表

你看,整个过程调度器只做决策,不碰数据。资源池只管理资源,不关心业务。职责清晰,出了问题也好排查。

注意:如果实时任务和离线任务共享同一个资源池,一定要设置资源配额。我曾经没设配额,结果一个离线任务把集群CPU吃满,实时任务全部超时。后来给实时任务预留了30%的CPU,才算稳住。

4.5 架构总览图

下面这张图展示了统一调度器、任务队列、资源池抽象层之间的协作关系:

混合编排架构核心协作图 实时任务 离线任务 定时任务 多级任务队列 P0 紧急队列 P1 高优队列 P2 普通队列 P3 低优队列 统一调度器 调度触发器 → 调度决策器 → 调度执行器 资源池抽象层 Kubernetes YARN 裸机/VM

从图里能看出来,所有任务先进入多级队列,统一调度器从队列里拉取任务,经过决策后向资源池申请资源。资源池抽象层屏蔽了底层差异,让调度器可以统一管理Kubernetes、YARN、裸机等不同环境。

4.6 小结

这一章咱们把混合编排架构的三个核心模块拆开了:

  • 统一调度器:核心调度循环,只做决策不执行
  • 任务队列管理:多级队列+老化机制,解决优先级和公平性
  • 资源池抽象层:统一接口,屏蔽异构资源

我个人觉得,这三块里最容易出问题的是资源池抽象层。因为底层资源种类太多,接口设计得不够抽象,后面改起来很痛苦。我的建议是:一开始就把资源描述做得足够通用,别只考虑CPU和内存,GPU、NPU、磁盘IOPS这些最好都预留扩展点。

嗯,这一章就到这儿。代码实现的部分,咱们后面章节会手把手写出来。