一行代码引起的分布式死锁
内容提要
本文介绍Mooncake分布式KV存储系统中,因一行配置代码引发分布式死锁的案例。在写入路径启用direct reclaim后,本地master的io线程等待其他节点响应,形成循环等待,导致集群大面积挂起。文章强调系统安全性依赖等待图单向性,任何改变都可能破坏这一前提。
延伸解读
等待图单向性是系统安全的隐含前提
文章指出,Mooncake 原本的设计中,只有读 miss 回填会触发 direct reclaim,且它阻塞的是 real client server 的 io 线程,等待的是从不阻塞的 local master server 的 io 线程,因此等待图单向无环。这种安全性并非来自“没有阻塞”,而是依赖等待图的单向性。任何改变,比如在写入路径启用 direct reclaim,都可能破坏这一前提,引入死锁风险。这提醒我们,在分布式系统中,隐含的不变量往往比显式规则更脆弱。
io 线程阻塞是分布式死锁的隐蔽资源
死锁通常与锁或内存相关,但本文展示了一个 io 线程成为死锁资源的案例。在 coro_rpc 框架中,同步 handler 在 io 线程内联执行,一旦 handler 阻塞,整个 io 线程停摆,影响所有绑定连接。当两个节点的 local master io 线程互相等待对方响应 PutStart 时,形成循环等待,导致节点无响应并扩散至整个集群。这提示我们,在事件驱动或线程池模型中,线程阻塞可能成为系统级故障的根源。
局部合理的优化可能引发全局风险
作者在写入路径添加 allow_direct_reclaim=1 的动机是合理的:写满时先尝试回收,避免数据直接降级到慢速存储。每个局部推理都正确,但忽略了该路径的 handler 运行在 local master server 的 io 线程上,使其从“从不等人”变为“可能等人”。这提醒我们,在分布式系统中,局部优化可能改变全局的等待关系,引入死锁等系统性风险。评估改动时,需考虑其对整个调用链和等待图的影响。
Q&A
Mooncake是什么系统?
Mooncake是LLM推理场景中的分布式KV存储系统,主要用于存储KV cache和各类中转数据,支持推理引擎将算好的KV cache存入,后续请求复用。
Mooncake中local master和RealClient进程的作用是什么?
每个节点有一个常驻的RealClient进程,内嵌本地master,管理本节点的共享内存池G。写入数据前需向本地master申请内存(PutStart),写完提交(PutEnd)。RealClient还运行两个RPC server:local master server处理master协议,real client server处理数据面协议。
coro_rpc框架中同步handler阻塞会有什么后果?
在coro_rpc中,同步handler在io线程内联执行,如果handler阻塞,该io线程就会停摆,期间所有绑定在该线程上的连接请求都无法处理,甚至无法接受新连接。
内存满时,Mooncake有哪些回收路径?
内存满时有两条回收路径:后台回收(独立线程周期性搬走冷对象)和direct reclaim(分配失败时由请求同步触发回收)。回收对象需先迁移到其他节点(remote swapout)或下级存储(offload)。
为什么在写入路径添加allow_direct_reclaim=1会导致分布式死锁?
因为写入路径的PutStart由local master server的io线程处理,原本该线程从不等待外部事件。添加配置后,当内存满时,该线程可能触发direct reclaim,进而向其他节点发送PutStart并等待响应。如果两个节点同时内存满并互相等待,就形成循环等待,导致死锁。
死锁发生后,为什么会导致集群大面积挂起?
死锁发生后,两个节点的local master io线程被循环持有,无法响应任何请求,包括本机dummy client的读写和其他节点的迁移请求。其他节点在尝试向这两个节点迁移数据时,发出的PutStart也会永远等待,导致调用线程被阻塞,最终扩散至整个集群。
系统原有的安全性依赖什么?
系统原有的安全性依赖等待图的单向性:只有real client server的io线程会等待其他节点的local master server,而local master server从不等待别人。这种单向等待保证了无环,从而避免死锁。