# 实验 02：RAID 嵌套的取舍——嵌套条带为什么赢不了扁平条带

> 手里固定 32 块一样的盘，可以拼成一个扁平 RAID 0，也可以拼成"RAID 0 里套 RAID 0"的多层结构：本实验把所有拼法枚举一遍，用成本模型算顺序读吞吐，看看嵌套到底亏在哪里。

## 一、实验目标

完成本实验后，你应当能够：

1. 能解释 RAID 0 条带化（striping）的并行原理，并写出成本模型中的顺序读吞吐公式 `read(RAID 0) = min(成员吞吐) × 成员数`，说明公式中每个符号的含义。
2. 能把一个整数分解成若干个不小于 2 的因子，并按因子顺序构造出对应的嵌套 RAID 0 结构。
3. 能运行枚举程序，列出 32 块设备下的全部嵌套方案及其吞吐，并归纳"吞吐只由顶层宽度决定"的规律。
4. 能用 `min` 语义解释为什么嵌套方案必然劣于同规模的扁平条带。
5. 能对比 RAID 0 / 1 / 5 / 6 / 10 在容量利用率、容错能力、顺序读吞吐三个维度上的取舍，并计算阵列级 MTTF。

## 二、预备知识

### 2.1 RAID 是什么

RAID（Redundant Array of Independent Disks，独立磁盘冗余阵列）把多块物理盘组织成一个逻辑盘。原始论文是 Patterson、Gibson、Katz 在 SIGMOD 1988 发表的《A Case for Redundant Arrays of Inexpensive Disks》；容量与各级别定义可参考 Red Hat 官方文档《Managing Storage Devices》的 RAID 章节。

把多块盘拼在一起，图的是三样东西，而这三样东西互相牵制：

- **容量**：n 块 1 TB 的盘，你能存多少有效数据？
- **吞吐（throughput）**：顺序读每秒多少 MB？
- **容错（fault tolerance）**：坏几块盘数据还在吗？

各级别的容量利用率（有效容量 / 装机容量）：

| 级别 | 容量利用率 | 说明 |
| --- | --- | --- |
| RAID 0 | 100% | 纯条带，无冗余 |
| RAID 1 | 50% | 两块盘互为镜像 |
| RAID 10 | 50% | 先镜像再条带 |
| RAID 5 | (N−1)/N | 每个条带组 1 块盘的奇偶校验 |
| RAID 6 | (N−2)/N | 双重校验，可同时坏 2 块 |

其中 N 是阵列中的盘数。例如 8 块盘的 RAID 5 有效容量是 7/8 = 87.5%。

### 2.2 条带化：并行读的原理

RAID 0 把数据切成一个个**条带（stripe）**轮流写到各块盘上。4 块盘、条带单元 64 KB 时：

```text
逻辑地址：  0    64   128  192  256  320 ...
盘 0：      [0]            [256]
盘 1：           [64]            [320]
盘 2：                [128]
盘 3：                     [192]
```

一次大文件的顺序读可以拆成 4 份同时进行。若每块盘顺序读 100 MB/s，理想情况下总吞吐 4 × 100 = 400 MB/s。

但"理想"有个前提：每块盘都要跟得上。整条链路的速度由**最慢的成员**决定——这就是木桶效应。把它写成公式（本仓库 `system/storage/RAID/cost_model.py` 中 `RAID_0.get_sequential_read_performance` 的定义）：

```text
read(RAID 0) = min( read(m₁), read(m₂), …, read(m_k) ) × k
```

其中 `m₁…m_k` 是阵列的 k 个成员（每块盘或每个子阵列），`read(mᵢ)` 是成员 i 的顺序读吞吐，单位 MB/s。前半截取最慢成员，后半截乘成员个数。

### 2.3 嵌套：把阵列当成一块盘用

成员不一定是裸盘，也可以是另一个阵列。例如"RAID 0 套 RAID 0"：外层条带的某个成员本身是一个内层 RAID 0。结构示意（外层 2 个裸盘成员 + 1 个内层阵列）：

```text
外层 RAID 0（3 个成员，min = 100）
├── 裸盘 A：100 MB/s
├── 裸盘 B：100 MB/s
└── 内层 RAID 0（2 个成员）
    ├── 裸盘 C：100 MB/s
    └── 裸盘 D：100 MB/s
```

按公式：内层读吞吐 = min(100,100) × 2 = 200 MB/s；外层成员是 {100, 100, 200}，min = 100，成员数 3，外层吞吐 = 100 × 3 = 300 MB/s。四块盘，只发挥了三块盘的作用——内层好不容易攒出来的 200 MB/s，被外层的 `min` 一票否决了。

这个例子说明了本实验的核心现象：**嵌套在纯条带场景下只会浪费设备**。后面用穷举验证它。

### 2.4 可靠性的算术：MTTF

MTTF（Mean Time To Failure，平均失效时间）是"一块盘平均多久坏一次"。n 块盘组成的无冗余阵列（RAID 0），任何一块坏则全阵列失效，所以：

```text
MTTF(阵列) = MTTF(单盘) / n
```

Schulze、Gibson、Katz、Patterson 的《How Reliable is a RAID?》（Spring COMPCON 1989，配套 UC Berkeley 技术报告 CSD-88-479）给出的算例：单盘 MTTF 40 000 小时、49 块盘的阵列，MTTF = 40 000 / 49 ≈ 816 小时，约 34 天。注意这篇与 RAID 概念的原始论文（Patterson、Gibson、Katz，SIGMOD 1988）**不是同一篇**，MTTF 估算出自前者。RAID 0 不改变这个残酷的算术，只会更糟。

### 2.5 术语约定

本实验里"嵌套方式"用因子序列表示，例如 `4 × 2 × 4` 表示：最外层 RAID 0 由第一个因子个裸盘成员加上 1 个内层阵列构成，内层阵列再按剩余因子递归展开。**顶层宽度 a** 指第一个因子，即最外层贡献的裸盘成员数。

## 三、实验原理

### 3.1 为什么用穷举而不是拍脑袋

嵌套方式不是唯一的：32 可以写成 8 × 4，也可以写成 4 × 8、2 × 2 × 8……顺序不同、层数不同，结构就不同。枚举所有"因子不小于 2、顺序有关"的分解，对每一种构造出真实的嵌套对象、调用成本模型取吞吐，比挑选几个"看起来典型"的方案更有说服力——结论是从全集中得出的，不是从样本里猜的。

### 3.2 关键推导：吞吐只由顶层宽度决定

设某个嵌套的顶层有 a 个裸盘成员加 1 个内层阵列，共 a + 1 个成员。把公式展开：

```text
read(顶层) = min( 100, …, 100, read(内层) ) × (a + 1)
```

内层阵列至少 2 块盘，read(内层) ≥ 200 > 100，所以 min 永远取 100（裸盘）。于是：

```text
read(顶层) = 100 × (a + 1)   单位 MB/s
```

内层折腾出多高的吞吐都无关紧要——它在外层眼里只是一个"比裸盘快"的单个成员，而外层的短板永远是最慢的裸盘。两条推论：

1. **吞吐只依赖顶层宽度 a**，与内层怎么摆完全无关。
2. **内层用掉的设备全部被"折叠"成 1 个成员**，参与并行的成员数只有 a + 1，远小于设备总数。这就是 2.3 节例子里"4 块盘只发挥 3 块"的一般化。

对照：32 块盘拼一个扁平 RAID 0，32 个成员全部是 100 MB/s 的裸盘，吞吐 = 100 × 32 = 3200 MB/s。任何嵌套方案的成员数都到不了 32，所以任何嵌套都输。

### 3.3 穷举结果

按 notebook 原文的构造方式（顶层 a 块裸盘 + 1 个内层阵列）本机实测，32 块设备共枚举出 15 种真正的嵌套方案（不含扁平）。全部结果按吞吐降序：

| 嵌套方式 | 层数 | 顶层宽度 a | 顺序读吞吐 (MB/s) |
| --- | --- | --- | --- |
| 16 × 2 | 2 | 16 | 1700 |
| 8 × 4 | 2 | 8 | 900 |
| 8 × 2 × 2 | 3 | 8 | 900 |
| 4 × 8 | 2 | 4 | 500 |
| 4 × 4 × 2 | 3 | 4 | 500 |
| 4 × 2 × 4 | 3 | 4 | 500 |
| 4 × 2 × 2 × 2 | 4 | 4 | 500 |
| 2 × 16 | 2 | 2 | 300 |
| 2 × 8 × 2 | 3 | 2 | 300 |
| 2 × 4 × 4 | 3 | 2 | 300 |
| 2 × 4 × 2 × 2 | 4 | 2 | 300 |
| 2 × 2 × 8 | 3 | 2 | 300 |
| 2 × 2 × 4 × 2 | 4 | 2 | 300 |
| 2 × 2 × 2 × 4 | 4 | 2 | 300 |
| 2 × 2 × 2 × 2 × 2 | 5 | 2 | 300 |

每行都满足 `吞吐 = 100 × (a + 1)`：顶层宽度 16 → 1700，8 → 900，4 → 500，2 → 300。最高嵌套（1700 MB/s）也不到扁平（3200 MB/s）的 54%。

![RAID 0 嵌套与扁平条带的对比](../figures/fig_raid_nesting.png)

图左是全部 15 种嵌套方式的吞吐（红虚线是扁平条带的 3200 MB/s，所有嵌套都在它左边）；图右把方案按顶层宽度归组取最大值，与理论式 $100 \times (a+1)$ 逐点重合——15 种方案无一例外。

### 3.4 那嵌套还有什么用

纯条带没有理由嵌套。嵌套真正的用武之地是**某一层提供扁平条带给不了的属性**——容错。典型的是 RAID 10（先镜像成 RAID 1 对、再对镜像对条带）：

- 容错：每个镜像对可以坏 1 块盘，最多容忍 n/2 块（每对至少剩 1 块）。
- 容量：50%。
- 顺序读：镜像对内部两块盘都可读，吞吐不受损甚至受益。

代价是容量砍半。三种级别的"容量 × 容错 × 吞吐"对照见图件：

![RAID 各级别的三要素取舍](../figures/fig_raid_levels.png)

### 3.5 一个提醒：RAID 不是备份

RAID（哪怕 RAID 6）防的是"盘坏了数据还能读"，防不了误删除、勒索软件、火灾和阵列控制器故障。业界通行的做法是 3-2-1 规则：3 份数据、2 种介质、1 份异地。Red Hat 官方文档在 RAID 章节也强调了类似立场。

## 四、实验环境与准备

1. 进入仓库根目录 `explaindb-main`（其下应有 `system/` 与 `notebooks/`）。
2. 安装依赖：

   ```bash
   pip install pandas numpy matplotlib jupyter
   ```

3. 打开配套 notebook：

   ```bash
   jupyter notebook notebooks/RAID-Nesting-Trade-offs.ipynb
   ```

4. 本实验用到仓库自带的成本模型 `system/storage/RAID/cost_model.py`，其中定义了 `Device`、`RAID_0`、`RAID_1`、`RAID_5` 等类。代码从仓库根目录运行才能正确 import。
5. 需要 Python 3.10 及以上。

## 五、实验步骤

### 步骤 1：把仓库根目录加入 import 路径

```python
import sys, pathlib

# 从仓库根目录（含 system/ 的目录）运行时，保证 from system... 可用
for _p in (pathlib.Path.cwd(), *pathlib.Path.cwd().parents):
    if (_p / "system").is_dir():
        if str(_p) not in sys.path:
            sys.path.insert(0, str(_p))
        break
```

### 步骤 2：构造设备，感受成本模型

```python
from system.storage.RAID.cost_model import Device, SubSystem, RAID_0, RAID_1, RAID_5

def make_device() -> Device:
    """一块同样的盘：顺序读/写 100 MB/s，随机读/写 1000 IOPS。"""
    return Device(100, 100, 1000, 1000)

print("单盘顺序读:", make_device().get_sequential_read_performance(), "MB/s")

# 两块盘的 RAID 0：min(100,100) × 2
pair = RAID_0([make_device() for _ in range(2)])
print("2 盘 RAID 0 顺序读:", pair.get_sequential_read_performance(), "MB/s")   # 预期 200

# 故意放一块慢盘，验证 min 语义
slow = Device(40, 40, 500, 500)
mixed = RAID_0([make_device(), make_device(), slow])
print("100+100+40 三盘 RAID 0:", mixed.get_sequential_read_performance(), "MB/s")  # 预期 120
```

第三行是本实验的灵魂：40 MB/s 的慢盘把 100 MB/s 的快盘一起拖到 40，3 个成员只有 120 MB/s，还不如拆开各用各的。

### 步骤 3：枚举因子分解

```python
from typing import Iterator

def factorisations(k: int) -> Iterator[list[int]]:
    """生成 k 的所有因子不小于 2、顺序有关的分解，不含单因子的 [k] 本身。"""
    assert isinstance(k, int) and k >= 1
    for divisor in range(2, k):
        quotient, remainder = divmod(k, divisor)
        if remainder != 0 or quotient < 2:
            continue
        for rest in factorisations(quotient):
            yield [divisor] + rest
        yield [divisor, quotient]

print(sorted(factorisations(8)))
# 预期输出（排序后）：[[2, 2, 2], [2, 4], [4, 2]]
```

注意 `[16, 2]` 和 `[2, 16]` 都会出现：顺序有关。除数从 2 遍历到 k−1，配合递归保证每个分解恰好生成一次。

### 步骤 4：按因子序列构造嵌套 RAID 0

```python
def build_nested_raid_0(config: list[int]) -> SubSystem:
    """按因子序列构造（可能嵌套的）RAID 0：
    本层取 config[0] 块裸盘，再加上由剩余因子递归构造出的 1 个子阵列。"""
    assert len(config) >= 1
    if len(config) == 1:
        return RAID_0([make_device() for _ in range(config[0])])
    inner: SubSystem = build_nested_raid_0(config[1:])
    return RAID_0([make_device() for _ in range(config[0])] + [inner])

demo = build_nested_raid_0([2, 4])
print("2×4 嵌套吞吐:", demo.get_sequential_read_performance(), "MB/s")  # 预期 300 = 100×(2+1)
```

### 步骤 5：全量枚举并列表

```python
import pandas as pd

NUMBER_OF_DEVICES = 32

rows = []
for config in factorisations(NUMBER_OF_DEVICES):
    raid = build_nested_raid_0(config)
    rows.append({
        "nesting": " × ".join(map(str, config)),
        "levels": len(config),
        "top_width": config[0],
        "seq_read_MB_s": raid.get_sequential_read_performance(),
    })

nestings = pd.DataFrame(rows).sort_values("seq_read_MB_s", ascending=False, ignore_index=True)
print(nestings.to_string())
print("方案数:", len(nestings))
print("最好嵌套:", nestings.iloc[0]["nesting"], nestings.iloc[0]["seq_read_MB_s"], "MB/s")
print("理论公式 100×(a+1) 验证:",
      (nestings["top_width"] + 1) * 100 == nestings["seq_read_MB_s"])
```

最后一行对每一行方案验证 `吞吐 = 100 × (顶层宽度 + 1)`，应全部成立。

### 步骤 6：与扁平条带对比

```python
flat = RAID_0([make_device() for _ in range(NUMBER_OF_DEVICES)])
print(f"扁平 RAID 0（{NUMBER_OF_DEVICES} 块）:", flat.get_sequential_read_performance(), "MB/s")
print(f"最好嵌套也只有它的 {nestings['seq_read_MB_s'].max() / flat.get_sequential_read_performance():.0%}")
```

### 步骤 7：画图复现结论

```python
import matplotlib.pyplot as plt

top = pd.concat([nestings.head(8),
                 pd.DataFrame([{"nesting": f"flat ({NUMBER_OF_DEVICES})",
                                "seq_read_MB_s": flat.get_sequential_read_performance()}])])
colors = ["#4C72B0"] * 8 + ["#C44E52"]

fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(top["nesting"], top["seq_read_MB_s"], color=colors)
ax.set_ylabel("顺序读吞吐 (MB/s)")
ax.set_title("嵌套 RAID 0 与扁平条带（32 块设备）")
ax.tick_params(axis="x", rotation=45)
for label in ax.get_xticklabels():
    label.set_ha("right")
fig.tight_layout()
plt.show()
```

红色柱是扁平条带，应显著高于所有蓝色柱。对照仓库 `教学产出/figures/fig_raid_nesting.png` 检查形状是否一致（绝对数值可能因口径略有差异，见 3.3 节说明）。

### 步骤 8：加上容错后的取舍（RAID 1 与 RAID 5）

```python
def report(name, arr):
    print(f"{name}: 顺序读 {arr.get_sequential_read_performance()} MB/s, "
          f"顺序写 {arr.get_sequential_write_performance()} MB/s, "
          f"随机读 {arr.get_read_IO_performance()} IOPS, "
          f"容量放大系数 {arr.get_storage_blow_up():.2f}")

raid1 = RAID_1([make_device(), make_device()])
report("2 盘 RAID 1 ", raid1)          # 容量放大 2.00 → 利用率 50%

raid5 = RAID_5([make_device() for _ in range(5)])
report("5 盘 RAID 5 ", raid5)          # 读 400 MB/s，容量放大 1.25 → 利用率 4/5

raid0big = RAID_0([make_device() for _ in range(5)])
report("5 盘 RAID 0 ", raid0big)       # 读 500 MB/s，容量放大 1.00，但坏 1 块全丢
```

`get_storage_blow_up` 返回"占用空间 / 有效数据量"，其倒数即容量利用率：RAID 1 为 1/2 = 50%，5 盘 RAID 5 为 1/1.25 = 4/5，与 2.1 节的公式一致。

## 六、观测记录表

**表 1：嵌套方案穷举（32 块设备，填你运行步骤 5 的结果）**

| 嵌套方式 | 层数 | 顶层宽度 a | 吞吐 (MB/s) | 100×(a+1) 吻合？ |
| --- | --- | --- | --- | --- |
|  |  |  |  |  |
|  |  |  |  |  |
|  |  |  |  |  |
|  |  |  |  |  |
|  |  |  |  |  |
|  |  |  |  |  |
|  |  |  |  |  |
|  |  |  |  |  |

**表 2：扁平对照**

| 方案 | 成员数 | 吞吐 (MB/s) |
| --- | --- | --- |
| 扁平 RAID 0（32 块） |  |  |
| 最好嵌套 |  |  |
| 两者之比 |  |  |

**表 3：级别取舍（5 块 100 MB/s 的盘）**

| 级别 | 顺序读 (MB/s) | 容量利用率 | 可容忍同时失效盘数 |
| --- | --- | --- | --- |
| RAID 0 |  |  |  |
| RAID 1（取 2 盘对） |  |  |  |
| RAID 5 |  |  |  |

## 七、思考题

1. 用 `min` 语义解释：为什么 3.2 节的内层阵列吞吐（≥200 MB/s）在外层永远派不上用场？
2. 因子序列 `4 × 2 × 4` 与 `4 × 4 × 2` 的吞吐相同吗？设备总数相同吗？分别算出来。
3. 把顶层宽度 a 逐步加大（a = 2, 4, 8, 16），吞吐增长是什么形状？为什么加大内层复杂度（比如把 `8 × 4` 改成 `8 × 2 × 2`）完全不改变吞吐？
4. 32 块 100 MB/s 的盘，若要求阵列能容忍任意 1 块盘失效且有效容量尽量大，你会在 RAID 5 之外考虑"RAID 0 嵌套"吗？为什么？RAID 5 的顺序读吞吐是多少？
5. 单盘 MTTF 40000 小时，32 块盘的 RAID 0 阵列 MTTF 是多少小时？约合多少天？这提示 RAID 0 适合存什么数据？
6. RAID 0 的随机读 IOPS 公式是"成员求和"（见 `cost_model.py`），而顺序读吞吐是"min × 个数"。为什么随机读不受最慢盘的木桶效应限制（提示：随机请求彼此独立，可以各发各的）？
7. 某厂商宣传"我们把 8 块盘做成 2 × 4 的嵌套 RAID 0，性能更强"。用本实验的公式与数据写一段反驳。
8. 如果 32 块盘里有 1 块只有 50 MB/s，扁平 RAID 0 的吞吐变成多少？这说明了采购与运维上的什么问题？

## 八、思考题答案与解析

1. 外层公式的 `min` 在所有成员里取最小值。裸盘成员都是 100 MB/s，内层阵列无论多快（≥200），min 都落在裸盘上；内层的高吞吐只体现为"这个成员不拖后腿"，体现为加分。`min` 是一票否决制，没有第二名奖励。
2. 吞吐只看顶层宽度，两者顶层都是 4，吞吐同为 100 × 5 = 500 MB/s。设备数不同：`4 × 2 × 4` 用 4 + 2 + 4 = 10 块，`4 × 4 × 2` 用 4 + 4 + 2 = 10 块——本模型下设备数恰好都是 10（各因子之和），但一般情形（如 `16 × 2` 用 18 块）会随分解而变，需按构造式逐层累加。关键差异在内层：`4 × 2 × 4` 的内层是 2 裸盘 + 4 盘子阵列，`4 × 4 × 2` 的内层是 4 裸盘 + 2 盘子阵列，可它们都被折叠成 1 个成员，对外不可见。
3. 吞吐 = 100 × (a + 1)，是关于 a 的线性（一次）函数：a 每加 1，吞吐加 100 MB/s。a = 2, 4, 8, 16 对应 300、500、900、1700 MB/s。内层复杂度不影响吞吐，因为内层整体只占外层成员列表里的 1 个名额，且它不是短板（≥200 > 100）；短板永远是裸盘，总成员数只由 a + 1 决定。
4. 不考虑。RAID 0 的任何嵌套都不提供冗余，坏 1 块盘即全阵列失效（内层外层一荣俱荣、一损俱损）。5 盘 RAID 5 顺序读 = min × (N − 1) = 100 × 4 = 400 MB/s（见 `cost_model.py` 的 `RAID_5.get_sequential_read_performance`），同时拿到 80% 容量利用率和单盘容错——这是嵌套 RAID 0 用多少设备都换不来的属性。
5. MTTF = 40000 / 32 = 1250 小时 ≈ 52 天。也就是说 RAID 0 平均一个半月就会整阵失效一次。它只适合可重建的临时数据：视频剪辑的缓存盘、可重新下载的中间结果等，原始数据必须另有副本。
6. 随机读的各次请求互相独立，可以分散到每块盘上并发执行，总 IOPS 就是各盘 IOPS 相加（求和公式）；顺序读一个大文件时，所有成员必须在同一条逻辑流上协同步进，任何一个成员慢了，整条流都要等它——所以取 min 再乘成员数。求和对应"并联"，min×个数对应"串联"。
7. 反驳要点：(1) 按成本模型，2 × 4 嵌套顶层宽度为 2，吞吐 = 100 × (2 + 1) = 300 MB/s；(2) 同样 8 块盘的扁平 RAID 0 是 800 MB/s，是它的 2.7 倍；(3) 嵌套的额外层数不增加任何并行度，反而把内层设备折叠成单个成员；(4) 可以要求厂商给出实测顺序读数字对照，实测若真有 800 MB/s，说明其实现根本不是模型所描述的嵌套结构。
8. min(100, …, 100, 50) = 50，吞吐 = 50 × 32 = 1600 MB/s，恰好腰斩。含义：阵列吞吐被最慢的盘封顶，采购应买同型号同批次；运维中一块性能退化的"亚健康"盘（如坏道重试增多）会把整个阵列拖下水，发现后应尽快替换并重建，而不是继续混用。

## 九、扩展任务（选做）

1. **异构盘下的最优排布。** 修改 `make_device`，让 32 块盘中随机 4 块是 50 MB/s 的慢盘。写代码回答：扁平 RAID 0 吞吐是多少？若允许把快慢盘分进不同子阵列再嵌套，能比扁平好吗？（提示：外层 min 仍会咬住含慢盘的成员，算一算每种摆法的 min 和成员数。）
2. **带容错的嵌套枚举。** 把构造函数改成：顶层 a 个"镜像对"（RAID 1）+ 可选条带，即 RAID 10 风格。枚举 a 从 2 到 16，输出"吞吐 / 容量利用率 / 可容忍失效盘数"三列表，找出"容量 ≥ 40% 且吞吐 ≥ 1200 MB/s 且容忍任意 1 块失效"的方案。提示：镜像对顺序读按 `RAID_1.get_sequential_read_performance` 是成员求和，注意与 RAID 0 公式区分。
3. **MTTF 曲线。** 固定单盘 MTTF 40000 小时，画 RAID 0（无冗余）阵列 MTTF 随盘数 n = 1…64 的衰减曲线，并在同图上标出 49 盘 → 816 小时这一 Schulze 等（COMPCON 1989 / CSD-88-479）算例的位置。对数坐标更直观。

## 十、常见错误与排查

| 现象 | 原因 | 处理 |
| --- | --- | --- |
| `ModuleNotFoundError: No module named 'system'` | 未在仓库根目录运行，`system/` 不在 import 路径 | 回到仓库根目录启动 Jupyter，或执行步骤 1 的路径探测代码 |
| 枚举结果比预期少，缺了 `16 × 2` | 递归终止条件里 `quotient < 2` 被写成 `quotient < 1`，或漏了 `yield [divisor, quotient]` | 对照步骤 3 逐行核对递归与两次 yield |
| 所有方案吞吐都等于 3200 | 把 `build_nested_raid_0` 写成了"每因子一层、每层铺满成员"，等价于扁平 | 核对：本层只取 `config[0]` 块裸盘 + 1 个内层阵列，成员数是 a + 1 而不是设备总数 |
| 吞吐出现 200、400 等"内层数值" | 在外层成员里忘了加裸盘，或把内层对象当成了唯一成员 | 检查 `[make_device() for _ in range(config[0])] + [inner]` 是否完整 |
| `RAID_5` 断言失败 `needs at least 3` | 传入成员少于 3 块 | RAID 5 至少 3 块盘，测试时用 `range(3)` 以上 |
| matplotlib 中文标题显示为方框 | 系统缺少中文字体映射 | `plt.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei"]` 后重画 |
| 自己算的最好嵌套是 1600 而资料说 1700（或反之） | 两种展开口径：顶层 a 块裸盘（=100×(a+1)）与顶层 a−1 块裸盘（=100×a） | 见 3.3 节口径说明；结论一致，报告时注明所用口径即可 |
