多机协作下的任务调度与并发踩坑记录
1176 字约 4 分钟
摘要记录用多台机器调度 ASR 任务时遇到的竞态条件,以及最终采用的协调中心与心跳机制。
最近在做一个视频字幕提取的需求,需要利用局域网内的硬件资源进行批量处理。所有视频文件统一挂载在 NAS 上,为了加快进度,公司分配了 6 台电脑同时运行 Whisper 模型做 ASR 推理。这原本只是算力的横向扩展,但在任务调度环节暴露了一些值得注意的架构问题。
轻量级调度的问题
原有的架构使用一张飞书表格作为轻量级的调度台。表格记录了所有待提取视频的路径,计划让这 6 台机器自行读取表格并领取任务。系统运行后很快出现了多机协作中常见的并发竞争问题:同一条视频记录经常被多台电脑同时领取,导致同一个高耗时的推理任务被重复执行,浪费了大量算力。
为了避免重复处理,我在表格中新增了一个“领取者 ID”字段。机器读取到空闲任务时,就把自己的机器名写入该字段,作为认领标记。但实际运行结果显示,这种做法并没有消除竞争,冲突依然频繁发生。
并发冲突的来源
仔细分析交互过程后,我发现问题出在网络请求的时间差上。机器与表格的交互分为三步:读取空行、在内存中做判断、调用 API 写入认领状态。假设机器 A 读到某一行处于未认领状态,但由于网络延迟,它还没有来得及写入自己的 ID。此时机器 B 也发起了读取请求,同样看到了未认领的状态。
随后两台机器都会调用写入接口,后到达的请求会覆盖前一条记录。最终两台机器都认为自己认领成功,于是同时开始处理同一个视频。这在系统设计中被称为竞态条件(race condition)。根本原因是读取和写入没有原子性保证,多个节点在没有互斥机制的情况下同时修改共享状态。
可行的方案
要解决这类并发调度问题,通常的做法是保证“检查状态”和“更新状态”不可分割。
一种思路是引入 Redis 之类的组件作为消息队列:把表格中的任务推入列表,6 台机器按顺序从队列中弹出任务。利用 Redis 单线程的串行特性,就能避免争抢。
另一种思路是保留直接读写表格的方式,但在操作前借助外部组件加上分布式互斥锁,强制各个机器的执行过程相互隔离。
如果没有任何外部组件可以依赖,也可以用纯代码实现类似乐观锁的回查机制:机器写入自己的 ID 后,等待一小段时间,再重新读取,检查标记是否被覆盖。这种方法能拦截大部分冲突,但无法保证完全没有问题。
自建协调中心与心跳机制
在评估了多种标准架构之后,考虑到团队现有的技术栈和纯局域网的运行环境,引入大型中间件显得过于复杂。因此,我放弃了让机器直接操作飞书表格的去中心化方案,改为自行实现一个基于主从结构的协调中心。
具体来说,局域网中指定一台电脑作为中心节点,专门负责读取总表并发布任务。其余机器作为工作节点,通过 HTTP 请求定时向协调中心拉取可用的任务。这种集中分发的方式从源头上避免了多台机器抢同一数据源的问题。协调中心在内存中按先后顺序处理所有领取请求,竞态条件也就消失了。
由于 ASR 模型运行时间较长,系统还需要考虑工作节点异常的情况。我设计了心跳机制:工作节点在处理任务期间,需要定期向协调中心发送存活信号。如果协调中心在规定时间内没有收到某个节点的心跳,就判定该节点已经卡死或掉线,并把它正在处理的任务收回,重新放入待分配的队列,交给其他正常节点领取。
这套方案结构简单,适合当前的网络环境,解决了算力闲置和任务重复执行的问题。