二、调度系统架构设计:分层架构、核心模块划分、技术选型
调度系统这东西,说白了就是给因子生产装上一套「自动流水线」。我见过太多团队,因子代码写得挺漂亮,结果每天靠人工跑脚本、盯着日志、手动重试。嗯,这种搞法,早晚要出事。
这一章,我们聊聊调度系统的架构该怎么搭。我会结合自己踩过的坑,把分层设计、模块划分、还有技术选型这些核心问题讲清楚。
2.1 分层架构:别让所有代码搅在一起
我个人习惯,把调度系统拆成四层。为什么是四层?你想想看,如果所有逻辑都写在一个大脚本里,改一个地方可能牵动全局,调试起来简直想砸电脑。
四层架构(从上到下):
- 接入层 — 负责接收任务请求、参数校验、权限控制
- 调度层 — 核心调度逻辑,决定「什么时候跑、跑什么、跑完怎么办」
- 执行层 — 真正干活的地方,拉起 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 条日志,一天下来磁盘满了。后来加了日志采样和轮转策略。
- 监控告警不能少 — 调度系统挂了没人知道,因子数据断了一天。后来加了心跳检测和钉钉告警。
嗯,调度系统架构设计,说到底就是「分层清晰、模块独立、选型务实」。别追求花哨,稳定压倒一切。