二、调度系统架构设计:分层架构、核心模块划分、技术选型

调度系统这东西,说白了就是给因子生产装上一套「自动流水线」。我见过太多团队,因子代码写得挺漂亮,结果每天靠人工跑脚本、盯着日志、手动重试。嗯,这种搞法,早晚要出事。

这一章,我们聊聊调度系统的架构该怎么搭。我会结合自己踩过的坑,把分层设计、模块划分、还有技术选型这些核心问题讲清楚。

2.1 分层架构:别让所有代码搅在一起

我个人习惯,把调度系统拆成四层。为什么是四层?你想想看,如果所有逻辑都写在一个大脚本里,改一个地方可能牵动全局,调试起来简直想砸电脑。

四层架构(从上到下):

  • 接入层 — 负责接收任务请求、参数校验、权限控制
  • 调度层 — 核心调度逻辑,决定「什么时候跑、跑什么、跑完怎么办」
  • 执行层 — 真正干活的地方,拉起 worker 进程执行因子计算
  • 存储层 — 元数据、任务状态、日志、结果数据,各归各位

我在项目中遇到过一种情况:团队把调度逻辑和业务逻辑写在一起,结果每次改因子参数都要重启整个调度服务。后来我强制拆层,接入层只管接收请求,调度层只管编排,执行层只管跑任务。改业务代码?不影响调度。改调度策略?不影响执行。这才是正经的架构。

下面这张图,是我常用的分层结构,你可以参考一下:

因子生产调度系统 — 四层架构图 接入层 API网关 · 任务接收 · 参数校验 · 权限控制 调度层 DAG编排 · 依赖管理 · 重试策略 · 优先级队列 执行层 Worker池 · 因子计算 · 资源隔离 · 日志采集 存储层 元数据库 · 结果存储 · 日志中心 · 配置中心 请求入口 调度决策 任务执行 数据持久

2.2 核心模块划分:每个模块只干一件事

分层是骨架,模块是器官。我建议把调度系统拆成下面这几个核心模块,每个模块职责单一,互不干扰。

模块名称 核心职责 我踩过的坑
任务管理器 接收任务、校验参数、分配任务ID 曾经没做参数校验,传了个空值进去,整个调度链路崩了
DAG引擎 解析依赖关系、生成执行计划 有次循环依赖没检测出来,任务死循环跑了一整夜
调度器 时间触发、优先级排序、资源分配 默认用FIFO,高优任务被堵死,后来改成优先级队列
执行器 拉起worker、监控进程、采集日志 worker OOM没处理,整个节点被打挂
状态管理 记录任务状态、失败重试、超时处理 状态更新用乐观锁,并发高了直接冲突

💡 我的建议:模块之间通过消息队列通信,别搞RPC直调。RPC一旦挂了,整个链路都受影响。用MQ解耦,调度器只管发指令,执行器只管消费,互不依赖。

2.3 技术选型:Airflow vs DolphinScheduler vs 自研

这个问题,几乎每个做调度系统的团队都会纠结。我三个都用过,说说真实感受。

2.3.1 Apache Airflow

Airflow 是调度界的「老大哥」。生态丰富,社区活跃,Python 原生支持。如果你团队全是 Python 工程师,上手很快。

但说实话,Airflow 的调度延迟是个硬伤。它的调度器是轮询机制,最小间隔 60 秒。做日频因子没问题,做分钟级甚至秒级因子?嗯,不太行。

我曾经在一个项目里用 Airflow 调度 5 分钟频次的因子,结果调度器本身成了瓶颈。任务排队时间比执行时间还长,你说气不气人。

# Airflow DAG 示例 — 因子生产流水线
from airflow import DAG
from airflow.operators.python import PythonOperator
from datetime import datetime, timedelta

default_args = {
    'owner': 'quant_team',
    'retries': 2,
    'retry_delay': timedelta(minutes=5),
}

with DAG(
    'factor_production_pipeline',
    default_args=default_args,
    schedule_interval='0 */4 * * *',  # 每4小时跑一次
    start_date=datetime(2024, 1, 1),
    catchup=False,
) as dag:

    fetch_data = PythonOperator(
        task_id='fetch_market_data',
        python_callable=fetch_data_from_db
    )

    calc_factor = PythonOperator(
        task_id='calculate_factor',
        python_callable=calculate_alpha_factor
    )

    store_result = PythonOperator(
        task_id='store_to_database',
        python_callable=store_factor_result
    )

    fetch_data >> calc_factor >> store_result

2.3.2 Apache DolphinScheduler

DolphinScheduler 是国产调度框架,对中国人更友好。它最大的优势是可视化 DAG 编辑,拖拽式操作,业务人员也能用。

而且它的调度延迟比 Airflow 低很多,支持秒级调度。如果你做高频因子,DolphinScheduler 是个不错的选择。

不过,DolphinScheduler 的社区活跃度不如 Airflow,遇到冷门 bug 可能得自己啃源码。我记得有一次遇到 worker 节点失联的问题,查了两天才找到原因,原来是心跳超时配置太短。

⚠️ 注意:DolphinScheduler 的 Master 节点是单点,挂了整个集群不可用。生产环境一定要做高可用部署,至少两个 Master 节点。

2.3.3 自研调度系统

什么时候需要自研?我总结三个场景:

  • 调度延迟要求极高 — 秒级甚至毫秒级触发,开源框架做不到
  • 业务逻辑极其特殊 — 比如需要动态生成 DAG、实时调整优先级
  • 团队有足够的人力 — 自研不是终点,维护才是大头

我曾经在一个量化团队自研过调度系统,核心逻辑其实不复杂:一个时间轮 + 一个任务队列 + 一个 worker 池。但坑在于:

  • 任务持久化怎么做?宕机后怎么恢复?
  • 分布式锁怎么实现?Redis 锁还是 ZooKeeper?
  • 日志怎么收集?ELK 还是自建?

这些细节,每一个都能让你加班到凌晨三点。

# 自研调度器核心逻辑(简化版)
import heapq
import time
from threading import Thread

class SimpleScheduler:
    def __init__(self):
        self.task_queue = []  # 优先队列
        self.running = True

    def add_task(self, task, run_at):
        heapq.heappush(self.task_queue, (run_at, task))

    def run(self):
        while self.running:
            now = time.time()
            if self.task_queue and self.task_queue[0][0] <= now:
                _, task = heapq.heappop(self.task_queue)
                Thread(target=task.execute).start()
            else:
                time.sleep(0.1)  # 避免空转

2.4 选型对比:一张表说清楚

对比维度 Airflow DolphinScheduler 自研
调度精度 分钟级(60s轮询) 秒级 毫秒级(取决于实现)
可视化 一般(需插件) 优秀(拖拽式) 需自建
社区生态 非常活跃 中等
学习成本 中等(Python基础) 低(可视化配置) 高(全栈开发)
维护成本
适合场景 日频/低频因子 中高频因子 超高频/特殊需求

我的建议:如果团队小于 10 人,别自研。用 DolphinScheduler 做中高频,Airflow 做低频,够用了。自研是最后的选择,除非你有一个专门的 infra 团队。

2.5 避坑指南:我踩过的那些坑

最后,分享几个真实教训:

  • 任务幂等性 — 我曾经没做幂等,同一个任务被重复调度,因子数据翻了一倍。后来每个任务都加唯一ID,执行前先查状态。
  • 资源隔离 — 多个因子任务共享一个 worker 进程,一个 OOM 全挂。后来用容器隔离,每个任务独立资源。
  • 日志不要打太多 — 有次一个因子每秒钟打 100 条日志,一天下来磁盘满了。后来加了日志采样和轮转策略。
  • 监控告警不能少 — 调度系统挂了没人知道,因子数据断了一天。后来加了心跳检测和钉钉告警。

嗯,调度系统架构设计,说到底就是「分层清晰、模块独立、选型务实」。别追求花哨,稳定压倒一切。


专注资料整理