分布式基础:操作系统、网络、并发与存储
背景
学习分布式系统时,很容易一开始就看到 Raft、Paxos、CAP、一致性、分布式事务这些概念。但如果底层基础没有建立起来,这些概念会比较悬空。
对我现在的实习方向来说,组内工作内容和训练-推理一体存储、KV Cache 相关。这个场景里,分布式问题经常不是先表现为“算法问题”,而是先表现为:
为什么请求变慢?
为什么 P99 延迟突然升高?
为什么 GPU 显存满了?
为什么 cache 迁移很贵?
为什么一次远程访问比本地访问慢很多?
为什么某个 block group 明明存在,却读不到?
这些问题背后,最先需要补牢的是四类基础:
操作系统
网络
并发
存储
这一篇先不追求把每个方向讲得很深,而是建立一个基础判断能力:看到一个系统设计时,能够大概判断这个系统可能慢在哪里、可能失败在哪里。
学习目标
这一组基础的学习目标可以概括成一句话:
理解系统为什么会慢,为什么会失败。
具体来说,需要掌握:
进程、线程、协程
锁、条件变量、原子操作
内存、页表、mmap
磁盘、SSD、顺序读写、随机读写
TCP、连接、超时、重传
带宽、延迟、吞吐、P99
放到 KV Cache 场景里,还要额外关注:
GPU HBM 很快但容量小。
CPU Memory 容量大但访问慢。
SSD 更慢但容量更大。
跨机器访问比本机访问更慢。
跨 GPU / 跨节点搬运 KV Cache 代价很高。
如果这些概念不清楚,后面学习调度、迁移、淘汰、元数据、一致性时,就很难判断一个方案到底好不好。
操作系统基础
分布式系统虽然运行在多台机器上,但每台机器内部仍然要依赖操作系统管理资源。
操作系统负责:
1. 管理进程和线程。
2. 调度 CPU。
3. 管理内存。
4. 管理文件和磁盘。
5. 管理网络 I/O。
6. 提供系统调用接口。
在分布式系统里,操作系统层面的开销会被放大。比如一次远程请求,可能会经历:
用户态编码请求
系统调用
内核协议栈
网卡发送
对端网卡接收
对端内核协议栈
对端用户态处理
这比一次本地函数调用复杂得多。
进程
进程是操作系统进行资源隔离的基本单位。每个进程有自己的虚拟地址空间、文件描述符、堆、栈和代码段。
分布式系统里,很多服务都是以进程形式运行的,例如:
scheduler 进程
metadata server 进程
worker 进程
cache manager 进程
gateway 进程
进程的优点是隔离性强。一个进程崩溃,不一定会直接影响另一个进程。
但进程之间通信成本比线程高,常见方式包括:
pipe
unix domain socket
shared memory
TCP socket
RPC
如果一个 KV Cache 系统把调度器、模型 worker、cache manager 拆成多个进程,就要考虑进程间通信成本和故障边界。
线程
线程是 CPU 调度的基本单位。同一个进程里的多个线程共享进程地址空间。
线程适合处理并发任务,例如:
接收网络请求。
执行 RPC handler。
后台迁移 cache。
后台淘汰 cache。
定期上报 heartbeat。
异步刷新元数据。
线程共享内存,所以通信方便,但也带来并发问题:
数据竞争
死锁
锁竞争
内存可见性
状态更新顺序错误
比如一个线程正在迁移 block_group_1,另一个线程同时准备淘汰它,如果没有正确的锁、状态机和版本校验,就可能出现错误释放或重复迁移。
协程
协程通常比线程更轻量,适合高并发 I/O 场景。它由用户态调度,不一定每个协程都对应一个内核线程。
在网络服务中,协程常用于:
同时处理大量 RPC 请求。
等待远程存储返回。
等待元数据服务响应。
异步执行 cache prefetch。
协程的优势是切换成本低,适合大量等待 I/O 的任务。缺点是它并不会让 CPU 计算变快,如果任务是重 CPU 计算,协程不能替代并行计算。
对 KV Cache 系统来说,协程适合处理远程读写、元数据查询、异步迁移等 I/O 密集任务。
并发基础
分布式系统里,很多错误不是网络导致的,而是并发状态没有处理好。
例如:
一个请求正在读取 cache,后台淘汰线程把 cache 删除了。
一个 block group 正在迁移,另一个线程又发起了一次迁移。
调度器认为某个 worker 还有空间,但 worker 实际已经分配满了。
ref_count 还没更新,cache 就被释放了。
这些问题都属于并发控制问题。
锁
锁用来保护共享状态,保证同一时间只有一个执行流修改某个关键数据。
常见锁包括:
mutex:互斥锁。
rwlock:读写锁,允许多个读者并发,但写者独占。
spinlock:自旋锁,等待时不睡眠,适合极短临界区。
锁能保证正确性,但也会带来性能问题:
锁竞争:大量线程争同一把锁。
死锁:多个线程互相等待对方持有的锁。
优先级反转:低优先级任务持锁,高优先级任务被阻塞。
临界区过大:持锁时间太长,系统吞吐下降。
在 KV Cache 场景中,如果整个 cache manager 只有一把全局锁,那么所有 block 的读写、迁移、淘汰都要串行,P99 延迟很容易变差。
更合理的做法通常是细粒度锁,例如:
按 block_group_id 加锁。
按 shard 加锁。
读多写少的元数据使用读写锁。
短状态更新使用 CAS。
条件变量
条件变量用于线程之间等待某个条件成立。
例如:
某个 block group 正在 MIGRATING_OUT。
decode 请求需要读取它。
请求线程可以等待迁移完成。
迁移线程完成后通知等待线程。
伪代码可以表示为:
while group.state == MIGRATING_OUT:
wait(group.cond)
read(group)
这里要注意,等待条件时通常要用 while,而不是 if。因为线程被唤醒后,条件不一定仍然成立。
原子操作
原子操作用于执行不可分割的更新,例如:
ref_count += 1
pin_count += 1
version compare-and-swap
在 KV Cache 中,ref_count 和 pin_count 非常关键。
ref_count:表示当前有多少使用者引用这份 cache。
pin_count:表示这份 cache 当前被固定,不能淘汰或迁移。
典型规则是:
ref_count > 0:不能直接释放。
pin_count > 0:不能淘汰,通常也不能迁移。
如果这些计数不是原子更新,就可能出现:
线程 A 准备读取 cache,但 ref_count 还没加成功。
线程 B 看到 ref_count == 0,于是释放 cache。
线程 A 继续读取,读到了已经释放的内存。
这类问题非常隐蔽,所以并发状态要尽早设计清楚。
内存基础
分布式系统的性能很多时候受内存影响,尤其是 KV Cache 系统。
需要理解几个概念:
虚拟内存
物理内存
页表
缺页异常
内存拷贝
mmap
NUMA
虚拟内存和页表
进程看到的是虚拟地址,CPU 通过页表把虚拟地址翻译成物理地址。
虚拟内存的好处是:
进程之间地址空间隔离。
程序可以使用连续的虚拟地址。
操作系统可以按页管理内存。
可以把文件映射到内存。
但虚拟内存也有成本:
地址翻译需要 TLB。
页表会占用内存。
缺页会触发内核处理。
随机访问可能导致更多 TLB miss。
对 KV Cache 来说,大量随机访问、频繁分配释放、跨 NUMA 节点访问都可能影响性能。
mmap
mmap 可以把文件或设备内存映射到进程地址空间。程序可以像访问内存一样访问文件内容。
它常用于:
大文件读取。
共享内存。
内存映射持久化文件。
零拷贝场景。
但 mmap 不是免费的。第一次访问某个页时可能触发缺页异常,操作系统需要把数据加载进内存。
如果 KV Cache 下沉到 SSD,并通过 mmap 访问,那么要注意:
首次访问可能很慢。
随机访问可能导致大量 page fault。
预取策略会影响性能。
内存压力大时,映射页可能被回收。
内存拷贝
数据从一个位置移动到另一个位置,往往需要拷贝。拷贝会占用 CPU、内存带宽和总线带宽。
KV Cache 的迁移尤其要关注拷贝路径:
GPU HBM -> CPU Memory
CPU Memory -> GPU HBM
GPU HBM -> Remote GPU HBM
GPU HBM -> Remote CPU Memory
CPU Memory -> SSD
SSD -> CPU Memory
如果没有 RDMA 或 GPUDirect,跨节点搬运 GPU 数据可能需要经过 CPU 中转,路径更长:
GPU HBM -> CPU Memory -> NIC -> Network -> NIC -> CPU Memory -> GPU HBM
每多一次拷贝,就多一段延迟和带宽消耗。
网络基础
分布式系统和单机系统最大的区别之一是:请求需要经过网络。
网络访问有几个基本指标:
延迟 latency:一次请求从发出到收到响应的时间。
带宽 bandwidth:单位时间内最多能传输多少数据。
吞吐 throughput:单位时间内实际完成多少请求或传输多少数据。
尾延迟 tail latency:P99、P999 这类慢请求延迟。
延迟
延迟会直接影响在线推理体验。
例如一次 decode 请求需要访问远端 KV Cache:
worker -> remote cache manager -> remote memory -> response
如果每生成一个 token 都要远程访问一次,那么远程延迟会被不断放大。
因此 KV Cache 系统通常希望:
尽量让 cache 和计算在同一张 GPU 或同一台机器。
尽量批量访问,而不是频繁小请求。
尽量预取后续会用到的 block group。
避免在 decode 关键路径上做大规模迁移。
带宽
带宽决定单位时间内能搬多少数据。
KV Cache 的数据量很大,迁移时很容易吃满网络带宽。例如:
一个 block group 几百 MB。
多个请求同时迁移。
后台训练任务也在使用网络。
远端存储也在读写。
这时即使每次远程访问延迟不高,整体吞吐也可能被带宽限制。
所以系统需要控制:
同时迁移的任务数量。
单个租户的迁移流量。
后台任务和在线请求的带宽优先级。
是否压缩或量化传输内容。
是否按 block group 批量搬运。
吞吐
吞吐不是单纯由带宽决定,还受 CPU、锁、队列、下游服务、批处理策略影响。
例如一个 cache manager 的吞吐可能被限制在:
网络带宽。
RPC 线程池。
元数据锁。
内存分配器。
GPU/CPU 拷贝带宽。
远端存储 IOPS。
所以当吞吐上不去时,不能只看网络,也要看系统内部是否有串行瓶颈。
P99
平均延迟不够用,在线服务更关心 P99。
原因是:
一个 batch 里最慢的请求可能拖慢整个 batch。
一个慢 worker 可能拖慢调度队列。
用户感受到的是慢请求,而不是平均值。
P99 变差的原因可能包括:
锁竞争。
网络重传。
队列堆积。
GC 或内存回收。
SSD 随机读。
远端 cache miss。
后台迁移占用带宽。
GPU 显存不足触发淘汰。
所以观察系统时,不能只看平均值,还要看 P50、P90、P99、P999 的变化。
TCP 基础
TCP 是可靠的字节流协议。它提供:
连接管理。
有序传输。
丢包重传。
流量控制。
拥塞控制。
但可靠不等于没有成本。
连接
TCP 通信前需要建立连接。频繁创建连接会有额外开销,所以服务通常会使用连接池。
在 RPC 系统中,连接池可以减少:
三次握手成本。
频繁创建和销毁 socket 的成本。
内核资源开销。
但连接池也要管理:
最大连接数。
空闲连接回收。
连接异常重建。
连接上的请求排队。
超时
远程调用必须有超时。
没有超时会导致:
请求一直卡住。
线程或协程被占用。
连接池被耗尽。
上游队列堆积。
故障传播到更多模块。
在 KV Cache 系统里,例如远程读取 block group:
get_block_group(block_group_id, epoch)
如果没有超时,远端节点变慢时,本地 worker 可能一直等待,进而拖慢整个推理请求。
重传
TCP 会在丢包时重传,这保证了数据可靠到达,但会增加延迟。
当网络拥塞、丢包或网卡异常时,重传会让 P99 延迟明显变差。
所以线上排查时,如果发现远程访问 P99 突然升高,可以考虑查看:
网络丢包。
TCP retransmission。
网卡错误。
交换机拥塞。
远端节点 CPU 或内存压力。
存储基础
KV Cache 系统常常会涉及多级存储。
从快到慢,大致可以这样理解:
GPU HBM
CPU Memory
Local SSD
Remote Memory
Remote SSD / Distributed Storage
它们的容量、延迟、带宽和成本完全不同。
GPU HBM
HBM 是 GPU 上的高带宽显存,速度很快,适合存放正在计算中高频访问的数据。
它的特点是:
带宽高。
延迟低。
容量有限。
成本高。
分配失败会直接影响推理任务。
KV Cache 在 decode 阶段会被频繁访问,所以最理想情况是放在 GPU HBM。
但 HBM 容量有限,当长上下文、多并发、多 batch 同时存在时,很容易不够用。
这时系统需要决定:
哪些 block group 留在 HBM?
哪些迁移到 CPU Memory?
哪些下沉到 SSD?
哪些直接淘汰或重算?
CPU Memory
CPU Memory 容量比 GPU HBM 大,但 GPU 访问 CPU Memory 通常更慢。
它适合存放:
暂时不在 decode 热路径上的 cache。
可以较快拉回 GPU 的 cache。
远端迁移的中转数据。
元数据和索引。
但是 GPU 和 CPU 之间的数据搬运会消耗 PCIe 或 NVLink 带宽。频繁在 HBM 和 CPU Memory 之间来回搬 cache,会显著影响延迟。
SSD
SSD 容量大,单位成本低,但延迟和带宽都比内存差,随机访问尤其要小心。
SSD 适合:
存放冷 cache。
存放 checkpoint。
存放可延迟加载的数据。
作为内存不足时的下沉层。
但如果在线 decode 阶段频繁从 SSD 读取 KV Cache,通常会严重影响 P99。
所以 SSD 更适合作为冷数据层,而不是热路径上的高频访问层。
顺序读写和随机读写
存储系统里,顺序读写通常比随机读写更友好。
原因是:
顺序读写更容易预取。
顺序写更容易合并。
随机读写会产生更多寻址和 I/O 放大。
SSD 虽然比 HDD 擅长随机读写,但随机 I/O 仍然比顺序 I/O 更贵。
对于 KV Cache,如果 block 太碎、访问模式太随机,下沉到 SSD 或远端存储后性能会更差。
因此 block group 的设计也和存储访问模式有关:
把经常一起访问的 block 放进同一个 group。
让 group 在物理存储上尽量连续。
迁移和预取时按 group 批量处理。
减少大量小随机读。
多级存储和 KV Cache
KV Cache 管理的本质之一,是在多级存储之间做取舍。
可以把不同层级理解为:
GPU HBM:最热的数据,正在 decode 使用。
CPU Memory:温数据,可能很快会再次使用。
SSD:冷数据,重算成本高但短期不访问。
Remote Memory:本地放不下,但远端还有较快访问能力。
Remote SSD:更冷、更便宜、更慢。
系统要不断回答:
这个 block group 当前热不热?
未来是否很快会访问?
重算成本高不高?
迁移成本高不高?
留在 HBM 是否值得?
下沉后再拉回来是否划算?
这就是为什么 KV Cache 系统需要缓存策略、迁移策略和调度策略。
一个请求为什么会慢?
假设一次推理请求需要读取一个远端 block group,它可能慢在很多地方:
1. 本地元数据缓存过期,需要重新查 metadata server。
2. metadata server 排队或锁竞争。
3. 路由发现 block group 在远端节点。
4. 远端 cache manager RPC 排队。
5. 远端 block group 正在迁移或被 pin。
6. 数据从 SSD 加载到 CPU Memory。
7. 数据从 CPU Memory 拷贝到 GPU HBM。
8. 网络传输受到带宽限制。
9. 本地 GPU HBM 空间不足,触发淘汰。
10. decode batch 等待最慢的请求。
所以当系统变慢时,不能只问“网络是不是慢”,而要拆开看:
CPU 是否满?
锁是否竞争?
队列是否堆积?
网络是否拥塞?
存储是否随机读太多?
显存是否触发频繁淘汰?
block group 是否被迁移打断?
一个请求为什么会失败?
失败也可能发生在很多层:
进程崩溃。
线程卡死。
锁死锁。
内存分配失败。
GPU HBM 不足。
RPC 超时。
TCP 连接断开。
远端节点不可达。
SSD 读取失败。
元数据版本过期。
block group 已经被淘汰。
分布式系统的难点在于:失败经常不是“全部失败”,而是部分失败。
例如:
迁移任务已经把数据复制到目标节点,但元数据还没更新。
元数据已经更新到新位置,但源节点还没释放旧副本。
客户端请求超时,但服务端其实执行成功了。
worker 挂了,但它 pin 住的 block group 还没释放。
因此后续学习 RPC、幂等、状态机、epoch、fencing、补偿任务时,都要带着这些失败场景去理解。
面向 KV Cache 的基础检查清单
学习完这一组基础后,看系统设计或代码时可以用下面的问题检查:
1. 这个模块运行在进程、线程还是协程中?
2. 共享状态由谁保护?锁粒度多大?
3. ref_count 和 pin_count 是否是原子更新?
4. 是否存在全局锁导致 P99 变差?
5. 一次远程访问会经过几次内存拷贝?
6. 数据在 GPU HBM、CPU Memory、SSD 还是远端?
7. 热路径上是否有 SSD 随机读?
8. 是否频繁跨 GPU 或跨节点搬运 KV Cache?
9. RPC 是否设置超时?
10. P99 升高时,是网络、锁、队列、存储还是显存导致?
11. block group 是否按访问局部性组织?
12. 后台迁移是否会影响在线 decode?
推荐学习顺序
这一组基础可以按下面顺序学:
1. 进程、线程、协程:理解执行模型。
2. 锁、条件变量、原子操作:理解并发状态。
3. 虚拟内存、页表、mmap:理解内存访问和缺页。
4. TCP、连接、超时、重传:理解网络访问为什么会失败。
5. 延迟、带宽、吞吐、P99:理解性能指标。
6. GPU HBM、CPU Memory、SSD:理解多级存储差异。
7. 顺序读写、随机读写:理解存储访问模式。
8. 结合 KV Cache 分析一次 block group 访问路径。
最后一步最重要。不要只背概念,而是要能画出一条访问路径:
worker
-> local cache manager
-> metadata server
-> remote cache manager
-> remote CPU Memory / SSD
-> network
-> local CPU Memory
-> local GPU HBM
然后逐段分析:
哪里会排队?
哪里会加锁?
哪里会拷贝?
哪里会超时?
哪里会触发淘汰?
哪里会导致 P99 变差?
总结
分布式系统不是脱离单机基础凭空出现的。很多分布式问题,本质上仍然是操作系统、网络、并发和存储问题在多机环境下被放大。
对训练-推理一体存储和 KV Cache 来说,第一组基础最重要的是建立下面几个判断:
1. 本地访问和远程访问不是一个量级。
2. GPU HBM 很快,但容量是核心瓶颈。
3. 数据迁移不是免费的,会消耗网络、总线、CPU 和内存带宽。
4. 并发状态如果没有保护,cache 迁移和淘汰很容易出错。
5. P99 比平均值更能反映在线推理体验。
6. block group 的划分会影响局部性、迁移成本和淘汰质量。
掌握这些之后,再继续学习 RPC、元数据、一致性、故障恢复和调度,会更容易把概念落到真实系统里。