3. 调度器架构设计:中心化调度 vs 去中心化调度、时间轮算法、优先级队列
调度器这东西,说白了就是量化系统的「心脏」。你策略写得再好,信号算得再准,调度器一卡壳,整个系统就瘫了。我这些年见过太多团队,策略收益曲线漂亮得不行,一上实盘就崩,十有八九是调度器没扛住。
今天咱们就聊聊调度器的几种主流架构,以及两个核心算法。嗯,都是我在实战中踩过坑、填过坑的东西。
3.1 中心化调度 vs 去中心化调度
先看一张图,帮你快速建立整体认知:
3.2 中心化调度:简单但脆弱
中心化调度,就是整个系统只有一个调度器。所有任务都向它注册,由它统一分配执行。
优点很明显:
- 逻辑简单,好理解。你想想看,一个大脑指挥全身,出问题容易排查
- 全局视图清晰。哪个任务在跑、哪个任务在等,一目了然
- 资源分配容易做全局最优
缺点也很致命:
- 单点故障。调度器挂了,整个系统就停了
- 性能瓶颈。任务量一上来,调度器就成了「高速公路收费站」
- 扩展性差。想加节点?得先问问调度器扛不扛得住
有一次做高频回测系统,用了中心化调度。策略数量从50个加到200个时,调度器CPU直接飙到95%。查了半天,发现是任务状态轮询把调度器打满了。后来改成事件驱动才解决。
3.3 去中心化调度:健壮但复杂
去中心化调度,每个节点都有自己的调度器。节点之间通过消息传递来协调。
核心优势:
- 没有单点故障。一个节点挂了,其他节点照常运行
- 水平扩展容易。加节点就是加调度能力
- 延迟更低。任务直接在本地调度,不用跨网络请求
代价也不小:
- 一致性难保证。多个调度器同时决策,容易冲突
- 调试困难。问题可能出现在任何一个节点上
- 资源利用率可能不均衡。有的节点忙死,有的闲死
我个人习惯,小团队、策略少于50个时用中心化。超过100个策略,或者对可用性要求极高,就上分布式调度。别一上来就搞分布式,复杂度会把你拖死。
3.4 时间轮算法:高效定时任务
说到定时任务调度,很多人第一反应是「用DelayQueue不就行了?」嗯,确实可以,但性能呢?
假设你有10万个定时任务,每个任务延迟时间不同。用DelayQueue,每次插入和删除都是O(log n)。10万个任务,每次操作要比较17次。看起来不多,但如果是高频交易场景,每秒触发几千次呢?
时间轮算法就是来解决这个问题的。
它的核心思想很简单:把时间分成一个个「槽位」,每个槽位放一个任务链表。指针每走一个时间单位,就执行当前槽位的所有任务。
// 简单的时间轮实现
public class TimeWheel {
private final int tickDuration; // 每个槽位的时间间隔
private final int wheelSize; // 槽位数量
private final List<List<Task>> wheel;
private int currentIndex = 0;
public TimeWheel(int tickDuration, int wheelSize) {
this.tickDuration = tickDuration;
this.wheelSize = wheelSize;
this.wheel = new ArrayList<>(wheelSize);
for (int i = 0; i < wheelSize; i++) {
wheel.add(new LinkedList<>());
}
}
public void addTask(Task task, long delay) {
int ticks = (int) (delay / tickDuration);
int index = (currentIndex + ticks) % wheelSize;
wheel.get(index).add(task);
}
public void tick() {
List<Task> tasks = wheel.get(currentIndex);
for (Task task : tasks) {
task.execute();
}
tasks.clear();
currentIndex = (currentIndex + 1) % wheelSize;
}
}
你看,插入和删除都是O(1)操作。不管你有多少任务,每次tick只处理当前槽位的任务。
时间轮的精髓在于「空间换时间」。用固定的槽位数量,换来了O(1)的操作复杂度。我在做实时行情处理时,用时间轮管理了5万多个定时任务,CPU占用不到5%。
3.5 优先级队列:谁重要谁先跑
不是所有任务都平等的。有些任务必须优先执行,比如风控检查、资金划转。这时候就需要优先级队列。
实现方式:
- 基于堆的优先级队列(Java的PriorityQueue)
- 基于跳表的优先级队列(Redis的ZSET)
- 基于多级队列的优先级调度
我个人比较喜欢用多级队列。把任务分成几个优先级等级,每个等级一个队列。高优先级队列空了,才处理低优先级的。
// 多级优先级队列
public class PriorityTaskQueue {
private final Queue<Task>[] queues;
private final int levels;
public PriorityTaskQueue(int levels) {
this.levels = levels;
this.queues = new Queue[levels];
for (int i = 0; i < levels; i++) {
queues[i] = new ConcurrentLinkedQueue<>();
}
}
public void addTask(Task task, int priority) {
if (priority < 0 || priority >= levels) {
throw new IllegalArgumentException("优先级超出范围");
}
queues[priority].offer(task);
}
public Task pollTask() {
// 从高优先级开始取
for (int i = 0; i < levels; i++) {
Task task = queues[i].poll();
if (task != null) {
return task;
}
}
return null;
}
}
我建议把优先级分成3-5级就够了。太多级反而增加管理开销。比如:
| 优先级 | 任务类型 | 最大延迟 |
|---|---|---|
| P0 | 风控、资金 | 10ms |
| P1 | 交易执行 | 100ms |
| P2 | 行情处理 | 500ms |
| P3 | 日志、统计 | 5s |
3.6 如何选择?
没有银弹。我这些年总结了一个简单的判断标准:
- 任务量 < 1000,延迟要求不高 → 中心化 + DelayQueue,简单够用
- 任务量 1000-10000,有定时需求 → 中心化 + 时间轮,性能足够
- 任务量 > 10000,高可用要求 → 去中心化 + 时间轮 + 优先级队列
- 混合场景 → 时间轮处理定时任务,优先级队列处理紧急任务,两者配合使用
我曾经在一个项目里,把时间轮的槽位设成了1000个,每个槽位1毫秒。结果内存爆了。为什么?因为槽位越多,内存占用越大。1000个槽位,每个槽位一个空链表,就是1000个对象。合理做法是槽位数 = 最大延迟 / tickDuration,别贪心。
好了,调度器的核心架构和算法就聊到这儿。记住一句话:没有最好的架构,只有最合适的。根据你的业务场景,选一个能hold住的方案,先跑起来,再优化。
公众号:蓝海资料掘金营,微信deep3321