实验 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 节口径说明;结论一致,报告时注明所用口径即可 |