Committer 服务分布式锁设计(2023)
背景 Committer 服务负责消费 Kafka 消息,将网络策略通过 NSO 提交到物理网络设备。原始设计使用单线程顺序消费:每次从 Kafka 拉取一条消息后提交,等待 NSO 返回结果再处理下一条。 单线程设计的瓶颈在于 NSO 提交设备的耗时差异很大,从几秒到几分钟不等。一条消息若因设备响应慢而阻塞,整个消费循环只能等待,整体吞吐严重受限。 改造目标是将 Committer 改为多线程并发消费,让不同设备的策略可以同时提交。 设备级并发约束 多线程并发带来一个新问题:物理网络设备在同一时刻只能处理一个配置提交。若并发向同一台设备提交多条策略,NSO 会返回冲突错误,设备状态也可能不一致。 因此,并发的粒度需要控制在设备维度:不同设备的提交可以同时进行,但同一台设备上的提交必须串行。 实现方案 多线程消费 Kafka 消费线程不再直接执行提交逻辑,而是将每条消息交给 ThreadPoolExecutor(默认 10 个 worker)异步处理。消费线程本身快速返回,继续 poll 下一批消息。 设备锁设计 引入一张设备锁表,记录当前正在进行中的提交和对应的设备,以及锁变更的时间戳。 提交任务在执行前先获取设备锁: SELECT ... FROM device_lock WHERE device_id = %s FOR UPDATE NOWAIT 加锁使用 NOWAIT:竞争失败时立即抛出异常而非阻塞等待,消费线程可以快速感知锁状态,不会因为等锁而阻塞整个提交流程。 提交完成后,在 finally 块中释放锁: SELECT ... FROM device_lock WHERE device_id = %s FOR UPDATE 解锁不使用 NOWAIT:此时当前任务本身就是锁的持有者,不存在竞争,阻塞等待语义更安全,确保成功取到行后再执行删除,不会因为瞬时异常而意外跳过清理步骤。无论 NSO 超时、解析异常还是其他未知错误,设备锁都会被清理,不会出现因为异常路径遗漏而导致的死锁。 锁的粒度是设备,不是请求。同一台设备的多条策略竞争同一把锁,不同设备的提交互不影响。 锁竞争与 Kafka 重投 竞争失败的消息不会被丢弃。Kafka 本身保证至少一次投递,消息会在下一个消费周期重新投递,再次参与锁竞争。NSO 侧做了幂等处理,重复提交不会导致设备状态错误。 ...