跳转至

阅读系统的方法

系统问题难,不是因为术语多,而是因为同一现象同时受到多层契约约束。最有效的读法不是背 API,而是建立一条可验证的因果链。

从现象写出可证伪的问题

“服务很慢”无法直接研究。先把它改写为带对象、时间窗口、负载与比较基线的问题:

在固定请求分布与并发度下,为什么 p99 延迟从 40 ms 上升到 180 ms,而吞吐量和 CPU 利用率几乎不变?

这个问题已经排除了很多无关方向,同时保留了排队、尾延迟、I/O、锁竞争、GC、调度抖动和下游重试等可能性。好的系统问题至少包含:

  • 工作负载:输入大小、读写比例、并发、到达过程、热点和突发;
  • 观察量:延迟分位数、吞吐、带宽、CPU 时间、运行队列、缺页、重传;
  • 边界:进程、容器、主机、NUMA 节点、网络路径、版本;
  • 对照:基线版本、另一台机器、另一种参数或可重复的空载实验。

七层检查表

1. 契约

先问规范保证什么,而不是实现“通常怎样”。例如 C++ 的 happens-before、POSIX write 的返回值、TCP 的字节流语义、文件系统的崩溃保证和数据库的隔离级别都属于契约。契约决定哪些观察能够跨版本成立。

2. 状态

列出状态及其唯一或共享所有者:

状态 常见所有者 需要追问
协程栈、程序计数器 运行时任务 谁调度?挂起点保存什么?
页表与 TLB 进程地址空间、CPU 修改怎样传播?何时失效?
TCP 发送窗口 内核协议栈 由确认、拥塞还是接收端限制?
缓存行 核心与一致性目录 谁可写?失效何时可见?
日志序列号 存储引擎 持久化顺序与提交点在哪里?

没有状态图,就很容易把控制消息、数据副本和逻辑所有权混在一起。

3. 控制流

把“调用函数”改写为状态迁移。下面的简化循环明确了就绪、执行、等待与取消的分叉:

from collections import deque
ready = deque(["request"])
waiting = {}
while ready:
    task = ready.popleft()
    event = step(task)
    if event.kind == "yield":
        ready.append(task)
    elif event.kind == "wait":
        waiting[event.key] = task
    elif event.kind == "cancel":
        release(task)

真实系统还必须回答:唤醒是否丢失、取消是否传播、资源由谁释放、回调是否可重入、队列是否有界。

4. 数据路径

画出数据的每次复制、编码、校验、缓存和队列边界。对一次发送,至少区分用户缓冲区、内核 Socket 缓冲区、协议分段、DMA 描述符、网卡队列和链路帧。send() 返回只说明数据进入了某个边界,并不等于对端应用已经处理。

5. 顺序与并发

“先发生”可能指程序顺序、编译器顺序、处理器可见顺序、缓存一致性顺序、内核事件顺序、网络到达顺序或分布式逻辑顺序。每次看到“之前”“之后”“同时”,都要写明是哪一种偏序关系。

6. 成本模型

先用数量级定位,再追求精确。一次操作的粗略响应时间可拆为:

\[ T = T_{\mathrm{queue}} + T_{\mathrm{service}} + T_{\mathrm{sync}} + T_{\mathrm{copy}} + T_{\mathrm{network}} + T_{\mathrm{retry}} \]

平均值不能解释尾部。排队系统在利用率接近上限时会出现非线性放大,因此 80% 与 95% 利用率并不是“只差 15 个百分点”。

7. 失败与恢复

失败不是最后附加的一节,而是机制的一部分。至少区分:

  • 进程、主机、网络分区、磁盘和时钟故障;
  • 超时、取消、重试、重复执行和部分成功;
  • 崩溃前已进入易失缓存、已落盘、已复制、已对外确认的不同状态;
  • 资源泄漏、优先级反转、活锁、饥饿和级联故障。

规格、实现与测量必须分开

同一句话常混入三种证据:

  1. 规格:允许或禁止哪些行为;
  2. 实现:某个版本如何实现;
  3. 测量:某台机器在某个负载下发生了什么。

例如“Python 线程不能并行”既不精确,也不稳定。需要写清解释器、版本、是否执行 Python 字节码、扩展是否释放解释器锁、工作负载是否 I/O 密集,以及“并行”指墙钟时间还是核心同时执行。

从最小实验到因果证据

一个有用实验应做到:

  • 一次只改变一个机制相关变量;
  • 预热与稳态分开;
  • 记录版本、CPU 拓扑、频率策略、内核、编译参数和输入;
  • 同时保留结果与反例;
  • 用观测工具验证实验确实走了预期路径。

不要在没有确认路径的情况下解释性能计数器。一个缓存未命中指标可能受采样、推测执行、复用计数器和事件定义影响;一个网络延迟也可能把客户端排队、DNS、TLS 和服务端处理揉在一起。

写出完整解释

最终答案至少应形成这条链:

现象 → 契约 → 状态与控制流 → 数据路径 → 成本模型 → 观测证据 → 反例与边界 → 修复后的新风险

当链条中出现“显然”“一般来说”“系统会自动”时,应继续追问:哪个系统、哪个版本、由哪个状态迁移实现、如何观测。

Reference