Junie's Blog

多机协作下的任务调度与并发踩坑实录

全文共 1261预计阅读 5 分钟

最近在处理一个视频字幕提取的需求,需要利用局域网内的硬件资源进行批量处理。所有待处理的视频文件统一挂载在NAS中,为了加快进度,公司分配了6台电脑同时运行Whisper模型进行ASR推理。这原本是一个单纯的算力横向扩展问题,却在任务调度环节暴露出了一些有趣的架构隐患。

轻量级调度的隐患

在历史遗留的架构设计中,使用的是一张飞书表格作为轻量级的调度台。表格记录了所有待提取的视频路径,计划让这6台机器自主读取表格并领取任务。系统运行后很快暴露出了多机协作中经典的并发竞争问题,同一条视频记录经常会被多台电脑同时领走,导致重复执行高耗时的推理任务,造成了严重的算力浪费。

为了避免重复处理,我尝试在表格中新增了一个领取者ID字段,让机器在读取到空闲任务时顺手将自己的机器名写入该字段作为认领标记。系统实际运行结果显示这种做法并没有消除竞争,冲突依然频繁发生。

幽灵般的并发冲突

深入分析整个交互流程后,我发现问题出在API网络请求的时差上。机器与表格的交互分为读取空行、内存决策、调用API写入认领状态三个独立步骤。当机器A读取到某一行任务处于未认领状态时,由于网络延迟,它还没来得及将自己的ID写入表格,机器B可能也发起了读取请求并看到了同样的未认领状态。

随后两台机器都会调用写入接口,后到达的请求直接覆盖了前面的记录。最终两台机器都判断自己认领成功,双双开始处理同一个视频。这种现象在系统设计中被称为竞态条件,根本原因在于读取和写入操作缺乏整体的原子性保证,多个节点在没有互斥锁保护的情况下同时操作了共享状态。

寻找理论上的破局点

想要解决这类并发调度问题,业界的标准做法通常是确保状态检查和状态更新这两个动作不可分割。一种思路是引入Redis这样的组件作为消息队列,将表格中的任务推送至列表中,6台机器统一从队列中按序弹出任务,利用Redis单线程模型天然的串行化特性来避免争抢。另一种思路是维持直接读写表格的架构,但在操作前借助外部组件施加分布式互斥锁,强制隔离各个机器的执行周期。

如果没有任何外部组件可以依赖,纯靠代码逻辑也可以实现类似乐观锁的回查机制。机器在写入自身ID后强制等待短暂时间,再次读取核对标记是否被覆盖,以此来拦截大部分的并发冲突。

自建协调中心与心跳防错

经过对各种标准架构的评估,考虑到团队现有的技术栈和纯局域网的运行环境,引入大型中间件显得过于繁重。我最终放弃了让机器直接与飞书表格交互的去中心化思路,改为自行实现一套基于主从架构的协调中心机制。

具体做法是指定局域网内的一台电脑作为中心节点,专门负责读取总表并发布任务。其余的机器作为工作节点,通过HTTP请求定时向协调中心拉取可用的任务。这种中心化的单点分发从源头上避开了多台机器去同一个数据源抢夺资源的情况,协调中心在内存里就能按照先后顺序处理所有的领取请求,干净利落地消除了竞态条件。

考虑到系统长时间运行的健壮性,我还配套设计了心跳机制。由于运行ASR模型耗时较长,工作节点在处理任务期间需要定期向协调中心发送存活信号。一旦协调中心在规定时间内没有收到某个节点的心跳,就会判定该节点已经卡死或掉线,随后系统会自动将它正在处理的任务回收,重新放入待分配池中等待其他正常节点领取。这套方案既轻便又完美契合了当前的物理网络环境,彻底解决了算力闲置与任务冲突的问题。

评论