任务调度基础:核心概念与分布式模型

聊到大规模向量检索,很多人第一反应是“索引怎么建”、“向量怎么算距离”。但我在几个生产项目里踩过坑之后,才真正意识到——调度才是那个让系统稳定跑起来的隐形骨架。你想想看,几千个检索请求同时涌进来,谁先处理?哪个Worker干活?任务失败了怎么办?这些事儿,都得靠调度系统来管。

调度核心三要素:队列、Worker、调度器

说白了,一个调度系统就三个东西:队列存任务,Worker干活,调度器做决策。我习惯把它们比作餐厅后厨——队列是待做菜单,Worker是厨师,调度器就是那个喊“三号桌的菜好了”的传菜员。

队列(Queue)

队列不只是个先进先出的列表。在大规模场景下,队列的设计直接影响系统吞吐。我个人常用的队列类型有这几种:

队列类型 适用场景 注意事项
内存队列 低延迟、小规模 宕机丢数据,需配合持久化
Redis队列 中等规模、需要去重 内存占用大,注意OOM
Kafka/RocketMQ 大规模、高吞吐 引入消息中间件,运维成本高
数据库表队列 需要事务保证 性能瓶颈明显,不推荐高频场景
我曾经踩过的坑: 用Redis做任务队列时,没给key设置过期策略。结果某个任务一直重试,队列越积越长,最后Redis内存爆了,整个集群挂了。后来我养成了习惯——所有队列必须配最大长度和TTL。

Worker(工作节点)

Worker就是实际干活的进程或线程。每个Worker通常维护一个任务循环:从队列拉任务、执行、上报结果、再拉下一个。这里有个关键点——Worker的并发数不是越多越好。

我记得有个项目,团队把Worker线程数设成了CPU核心数的4倍,结果上下文切换开销比实际计算还大,吞吐反而下降了。后来我们压测发现,对于向量检索这种CPU密集型任务,Worker数等于CPU核心数是最优的。如果是I/O密集型(比如从远端拉向量),可以适当多配一些。

调度器(Scheduler)

调度器是大脑。它决定:

  • 任务该进哪个队列
  • 哪个Worker来执行这个任务
  • 任务超时了怎么办
  • Worker挂了怎么重新分配

我见过不少团队把调度器做得太重,什么逻辑都往里塞。结果调度器本身成了瓶颈。我的建议是——调度器只做决策,不做执行。决策逻辑要轻量,最好能在毫秒级完成。

分布式调度模型

单机调度好做,分布式才是真挑战。这里我重点讲两种最常见的模型。

Master-Worker模型

这是最经典的分布式调度模型。一个Master节点负责任务分配和Worker管理,多个Worker节点执行任务。结构清晰,容易理解。

Master-Worker调度模型 Master 调度器 Worker 1 Worker 2 Worker N 实线:任务分配(Master → Worker) 虚线:心跳上报(Worker → Master) Master单点故障是最大风险,需引入选主机制(如ZooKeeper/etcd)

这个模型最大的问题是单点故障。Master挂了,整个集群就瘫痪了。我在一个线上系统里遇到过——凌晨三点Master节点OOM,所有任务卡住,检索服务全部超时。后来我们加了etcd做选主,备Master自动顶上,才解决了这个问题。

DAG调度模型

当任务之间有依赖关系时,Master-Worker就不够用了。比如向量检索里,先要建索引、再加载索引、最后才能执行检索——这就是个典型的DAG(有向无环图)。

DAG调度里,每个任务是一个节点,依赖关系是边。调度器按拓扑顺序执行:只有所有前置任务完成,后续任务才能启动。

实际案例: 我们做大规模向量索引更新时,需要先做数据预处理(清洗、归一化),然后构建IVF索引,最后把索引分片分发到各Worker。这三个阶段有严格依赖,用DAG调度天然合适。如果某个阶段失败,DAG调度器会自动重试或触发告警。

调度策略:不只是排队

有了队列和Worker,怎么分配任务?这就是调度策略的事儿了。我见过不少团队一开始只用FIFO,后来发现不行——高优先级的检索请求被低优先级的批量任务堵住了。

FIFO(先进先出)

最简单,也最公平。但公平不等于合理。如果有个大任务卡在队头,后面所有小任务都得等着。这就是所谓的队头阻塞。

优先级调度

给任务打上优先级标签。高优先级的任务可以插队。实现方式有两种:

  • 多队列:每个优先级一个队列,调度器优先处理高优先级队列
  • 优先级队列:用堆(Heap)实现,每次取优先级最高的任务
我的经验: 优先级不要超过5级。级数太多,调度器光排序就花不少时间,得不偿失。我一般只用3级:紧急(线上检索)、正常(离线分析)、低优(数据备份)。

负载均衡

这是分布式调度里最头疼的问题。Worker的能力不一样(有的机器好,有的差),任务的大小也不一样。怎么分配才能让所有Worker都“吃饱”又不“撑到”?

我常用的策略:

策略 原理 优缺点
轮询(Round Robin) 依次分配给每个Worker 简单,但不考虑Worker负载差异
最少连接(Least Connections) 分配给当前任务数最少的Worker 适合任务执行时间相近的场景
加权分配(Weighted) 根据Worker性能分配权重 需要手动配置权重,不够自适应
自适应负载均衡 实时监控Worker的CPU/内存/队列长度 效果好,但实现复杂

我个人最推荐自适应负载均衡。虽然实现起来麻烦点,但效果确实好。我记得有个项目,一开始用轮询,结果一台老机器总是超时,拖慢了整个集群。换成自适应后,调度器自动把大任务分配给性能好的Worker,老机器只处理小任务,整体吞吐提升了30%以上。

注意: 自适应负载均衡需要采集Worker的实时指标,这本身就有开销。采集频率别太高,我一般设5秒一次。太频繁反而影响Worker本身的性能。

小结

调度这事儿,看着简单,做起来全是细节。队列选型、Worker管理、调度策略——每个环节都可能成为瓶颈。我见过太多系统,算法模型很牛,但调度做得稀烂,线上跑起来各种问题。所以,别轻视调度,它才是系统稳定性的基石。