未经同意,请勿转载!
副标题:Slab、Column、Region、Fault Domain------微软真正的存储语义
适用版本:Azure Local 2506 / 2510 / Windows Server 2025(含 23H2 / 22H2 沿用)
文档定位:架构白皮书深度。读完后应能在不查文档的前提下,复述 S2D 内部数据单位与跨节点条带化逻辑。
写作约定 :本文严格区分微软官方硬性表述 、微软隐含 / 默认行为 、基于已公开文档的推断。不写"必须/一定/官方禁止"等未经原文支持的绝对化措辞。
目录
- [0. 引言:为什么官方文档看起来"很简单"](#0. 引言:为什么官方文档看起来"很简单")
- [第 1 层:64 MiB Region 与 Slab------真正的数据单位](#第 1 层:64 MiB Region 与 Slab——真正的数据单位)
- [第 2 层:Column------故障域感知的条带](#第 2 层:Column——故障域感知的条带)
- [第 3 层:Mirror 的数据块分配](#第 3 层:Mirror 的数据块分配)
- [第 4 层:Parity 的数据块分配](#第 4 层:Parity 的数据块分配)
- [第 5 层:Fault Domain 多级保护](#第 5 层:Fault Domain 多级保护)
- [6. Slab 大小估算(实战)](#6. Slab 大小估算(实战))
- [7. Repair 路径详解](#7. Repair 路径详解)
- [8. 与 Ceph / vSAN / 传统 RAID 的对比](#8. 与 Ceph / vSAN / 传统 RAID 的对比)
- [9. 总结](#9. 总结)
0. 引言:为什么官方文档看起来"很简单"
如果只看微软官方文档对 Mirror(镜像) 、Parity(奇偶校验) 、Mirror-Accelerated Parity(MAP) 的描述,会觉得它和传统 RAID 区别不大------
- Mirror = 数据多副本
- Parity = 数据 + 校验位
- MAP = Mirror 与 Parity 混合
但事实上,Storage Spaces Direct(S2D)以及 Azure Local 的底层已经远远不是传统 RAID5/RAID6,而更接近 Scale-Out Software Defined Storage(SDS,例如 Ceph、vSAN、Google Colossus)。
理解它,需要分五层来看:
| 层 | 数据单位 | 谁来管理 |
|---|---|---|
| 第 1 层 | 64 MiB Region(ReFS MAP 迁移单位) | ReFS |
| 第 2 层 | Slab(Storage Spaces 内部映射单位) | Storage Spaces |
| 第 3 层 | Column(跨物理盘的条带位置) | Storage Spaces |
| 第 4 层 | Fault Domain(Disk / Node / Enclosure / Rack) | Storage Spaces + Azure Local RAC |
| 第 5 层 | Physical Disk | 硬件 |
下面逐层展开。
第 1 层:64 MiB Region 与 Slab------真正的数据单位
1.1 大多数人的"直觉模型"
很多人会按下面的层次理解存储栈:
VM
└─ VHDX
└─ NTFS / ReFS
└─ Block (4 KiB)
└─ Disk
但进入 Storage Spaces 之后,这条链被重新切分:
VM
└─ VHDX
└─ ReFS
└─ 64 MiB Region ← ReFS MAP 迁移单位
└─ Slab ← Storage Spaces 内部映射单位
└─ Column ← 跨物理盘的条带位置
└─ Physical Disk
1.2 两个关键单位
(1) 64 MiB Region
- ReFS 的 Mirror-Accelerated Parity(MAP) 在 Mirror 与 Parity 之间迁移数据时,以 64 MiB 为最小迁移单位。
- 这是 ReFS 文件系统层面的概念,与"Block(4 KiB / 64 KiB)"完全不同。
- 它把一个 VHDX 在逻辑上划分为许多 64 MiB 的"区块",每个区块独立决定当前是放在 Mirror 还是 Parity 上。
(2) Slab
- Storage Spaces 内部把每个 64 MiB Region 再切分成大量 Slab。
- Slab 是 Storage Spaces 的内部映射单位;Metadata 保存每个 Slab 位于哪些 Physical Extent。
- Slab 数量与大小取决于 Pool 中物理磁盘的容量与配置;微软公开文档未给出固定的 Slab 大小常数,因为它会随盘数、容量自动调整。
1.3 Region 与 Slab 的关系(示意)
flowchart TD
A[VHDX<br/>虚拟磁盘] --> B[ReFS 视角<br/>64 MiB Region 序列]
B --> C[Region 0]
B --> D[Region 1]
B --> E[Region 2]
B --> F[Region N]
C --> G[Slab 0]
C --> H[Slab 1]
C --> I[Slab 2]
C --> J[Slab K]
G --> K[Physical Extent<br/>Column 选取]
H --> L[Physical Extent<br/>Column 选取]
I --> M[Physical Extent<br/>Column 选取]
J --> N[Physical Extent<br/>Column 选取]
style A fill:#e1f5ff
style B fill:#fff3cd
style G fill:#d4edda
style K fill:#f8d7da
1.4 微软真正管理的是什么
把这两层串起来:
64 MiB Region(ReFS 视角)
↓ 切分
多个 Slab(Storage Spaces 视角)
↓ 元数据记录
每个 Slab 放在哪几个 Column
也就是说,微软管理的不是文件、不是 Block、不是 VHDX,而是"Slab 物理位置表"。
文件、目录、Block 在这个层面已经被 ReFS 接管。Storage Spaces 只关心:
"64 MiB Region 的第 N 个 Slab,现在物理上落在哪些 Column 的哪些盘上?副本是否满足?是否需要 Repair?"
这一点是后续理解 Mirror / Parity / MAP 的基础------所有的副本轮转、双校验布局、MAP Region Rotation,全部发生在 Slab 这一层,而不是 Block 层。
第 2 层:Column------故障域感知的条带
2.1 一个常见的误解
很多人会直觉地认为:
Node = 一列(Column)
这是完全错误的。
微软定义的 Column 是:
一条 Stripe 在多少块物理盘上展开。
换句话说,Column 数 = 一个 Stripe 跨多少块物理盘,而不是跨多少 Node。
2.2 举例:4 Node × 2 SSD 拓扑
假设集群有 4 个 Node,每个 Node 直连 2 块 SSD:
Node1 ── SSD1, SSD2
Node2 ── SSD1, SSD2
Node3 ── SSD1, SSD2
Node4 ── SSD1, SSD2
如果系统分配 Columns = 4,意味着一个 Stripe 展开在 4 块物理盘上:
Stripe N ── DiskA | DiskB | DiskC | DiskD
而不是:
Stripe N ── Node1 | Node2 | Node3 | Node4 ← 这是错误理解
2.3 Column 的选取流程
flowchart TD
Start[需要写入一个 Stripe] --> FD[收集故障域信息]
FD --> Disk[Disk Fault Domain]
FD --> Node[Node Fault Domain]
FD --> Enc[Enclosure Fault Domain]
FD --> Rack[Rack Fault Domain<br/>Azure Local RAC]
Disk --> Alg[Column 选取算法]
Node --> Alg
Enc --> Alg
Rack --> Alg
Alg --> Filter[过滤:同 FD 的盘排除]
Filter --> Score[为每个候选盘打分<br/>负载 / 容量 / 历史]
Score --> Pick[挑出 N 个 Column]
Pick --> Write[并发写入]
style Alg fill:#fff3cd
style Pick fill:#d4edda
2.4 一个关键推论
Node 数 ≠ Column 数。
| 场景 | Node 数 | 推荐 Columns | 说明 |
|---|---|---|---|
| 4 Node × 4 SSD | 4 | 4 / 8 | Columns 可等于盘数,也可大于 Node 数 |
| 4 Node × 8 SSD | 4 | 8 | 跨 8 块盘条带 |
| 16 Node × 4 SSD | 16 | 16 / 32 | RAC 场景 Columns 受 Rack 约束 |
微软官方文档在描述 Mirror / Parity 容量效率时,会直接给"假设 Columns = N"这样的前提;这与 Node 数不是同一回事。
第 3 层:Mirror 的数据块如何分配
3.1 拓扑假设
为便于说明,本文接下来默认:
- 4 Node × 多 SSD
- 3-way Mirror
3.2 直觉会犯的错
很多人会以为写入一个 64 KiB Block 时:
Node1 ── Data A
Node2 ── Data A 副本
Node3 ── Data A 副本
固定三个 Node。
但实际并不是这样。
3.3 真实流程:Column Set + 轮转
微软首先计算 Column Set------三个互不共享 Fault Domain 的物理盘位置,例如:
Column0 ── Node1 Disk2
Column1 ── Node2 Disk1
Column2 ── Node4 Disk2
然后:
Data A
Copy1 → Column0 (Node1 Disk2)
Copy2 → Column1 (Node2 Disk1)
Copy3 → Column2 (Node4 Disk2)
下一块 Data B:
Data B
Copy1 → Node3 Disk1
Copy2 → Node1 Disk1
Copy3 → Node2 Disk2
再下一块 Data C:
Data C
Copy1 → Node4 Disk1
Copy2 → Node3 Disk2
Copy3 → Node1 Disk2
副本位置始终轮转,Column Set 每条 Stripe 都不一样。
3.4 副本轮转的图示
flowchart LR
subgraph Stripe1[S1]
A1[Data A] --> N1D2[Node1 Disk2]
A1 --> N2D1[Node2 Disk1]
A1 --> N4D2[Node4 Disk2]
end
subgraph Stripe2[S2]
A2[Data B] --> N3D1[Node3 Disk1]
A2 --> N1D1[Node1 Disk1]
A2 --> N2D2[Node2 Disk2]
end
subgraph Stripe3[S3]
A3[Data C] --> N4D1[Node4 Disk1]
A3 --> N3D2[Node3 Disk2]
A3 --> N1D2b[Node1 Disk2]
end
style Stripe1 fill:#e1f5ff
style Stripe2 fill:#fff3cd
style Stripe3 fill:#d4edda
3.5 轮转的目的
- 热点均衡:同一个 Node 不会被连续多个 Stripe "砸中"
- Node IO 均衡:每个 Node 上的盘轮流承担写副本
- Disk IO 均衡:避免某一块 SSD 成为写瓶颈
微软将这种机制称为 Distributed Mirror Placement------分布式副本放置。
关键认知:Mirror 不是"按 Node 固定副本位置",而是"按 Column Set 轮转副本位置"。这是 S2D 区别于传统 RAID1(固定盘对)最大的算法差异之一。
第 4 层:Parity 的数据块如何分配
4.1 Parity 比 Mirror 复杂
Parity(奇偶校验)的难点在于:校验位本身也是数据,必须参与轮转,否则校验盘会成为热点。
4.2 拓扑假设
- 4 Node
- Dual Parity(双校验,P + Q)
一个 Stripe 假设为:
D1 D2 P Q
4.3 直觉会犯的错
很多人会以为:
Node1 ── D1
Node2 ── D2
Node3 ── P
Node4 ── Q
一直固定。
4.4 真实流程:P、Q 也轮转
Stripe1:
Node1 ── D1
Node2 ── D2
Node3 ── P
Node4 ── Q
Stripe2:
Node1 ── P ← 校验位跳到 Node1
Node2 ── D3
Node3 ── D4
Node4 ── Q
Stripe3:
Node1 ── Q ← Q 跳到 Node1
Node2 ── P ← P 跳到 Node2
Node3 ── D5
Node4 ── D6
4.5 校验位轮转图示
flowchart TD
subgraph S1[Stripe 1]
S1N1[Node1: D1]
S1N2[Node2: D2]
S1N3[Node3: P]
S1N4[Node4: Q]
end
subgraph S2[Stripe 2]
S2N1[Node1: P ← 跳到这里]
S2N2[Node2: D3]
S2N3[Node3: D4]
S2N4[Node4: Q]
end
subgraph S3[Stripe 3]
S3N1[Node1: Q ← 跳到这里]
S3N2[Node2: P ← 跳到这里]
S3N3[Node3: D5]
S3N4[Node4: D6]
end
S1 --> S2 --> S3
style S2N1 fill:#ffcccc
style S3N1 fill:#ffcccc
style S3N2 fill:#ffcccc
4.6 与传统 RAID 的关系
这种思想:
- 与 RAID5 的 Left Symmetric Layout(P 沿盘组轮转)
- 与 RAID6 的 Rotating Parity(P、Q 双校验位独立轮转)
在数学层面是一致的。
但实现层面完全不同:
| 维度 | 传统 RAID5/6 | S2D Parity |
|---|---|---|
| 控制器 | 单个硬件 RAID 卡 | 软件层(Storage Spaces),跨服务器 |
| 故障域 | 单台服务器内的盘 | Disk / Node / Enclosure / Rack |
| 轮转粒度 | 盘级 | Stripe + Fault Domain 联合优化 |
| 修复来源 | 仅同盘组 | 整个 Pool 的所有盘 |
也就是说,S2D 是把"RAID5/RAID6 的对称校验思想"搬到软件层 + 跨节点 + 多故障域。
4.7 为什么"4 Node 才能用 Parity"
微软官方对 S2D 的支持矩阵:
- 3 Node:只能 3-way Mirror
- 4 Node 及以上:可以 Mirror,也可以 Dual Parity
这一点本文不展开技术根因(会单独写第三篇),但逻辑上的解释是:
Dual Parity 需要 2 个独立校验位,最小 Stripe = 2 Data + 2 Parity = 4 列;再叠加 Fault Domain 约束,4 Node 是 S2D 默认推荐的最小 Parity 拓扑。
第 5 层:Fault Domain------为什么多节点越多越复杂
5.1 故障域的种类
Storage Spaces 在做 Column 选择时,会同时考虑多个故障域层级:
| 故障域 | 是否存在 | 典型场景 |
|---|---|---|
| Disk | 始终存在 | 任何硬盘 |
| Node | S2D / Azure Local | 节点宕机 / 重启 / 维护 |
| Enclosure | 部分硬件 | JBOD、Chassis |
| Rack | Azure Local RAC 2510+ | 机架级故障(电源、网络) |
5.2 RAC 引入 Rack Fault Domain
到了 Azure Local 2510 的 Rack Aware Cluster,微软又增加了一层约束:
Rack A
├─ Node1
└─ Node2
Rack B
├─ Node3
└─ Node4
Column 选择从以前的"避开同 Node",扩展为"避开同 Node + 同 Rack"。
每个 Stripe 的 Column 分配会同时考虑:
- Disk Failure(磁盘故障)
- Node Failure(节点宕机)
- Chassis Failure(机箱故障)
- Rack Failure(机架故障)
Column 的选择已经从单纯的"磁盘条带"演进为面向多级故障域的智能布局------这是 Azure Local 相比传统 RAID 控制器最大的架构差异。
5.3 RAC 故障域示意图
flowchart TB
subgraph RackA[Rack A]
N1[Node1<br/>Disk 1-4]
N2[Node2<br/>Disk 1-4]
end
subgraph RackB[Rack B]
N3[Node3<br/>Disk 1-4]
N4[Node4<br/>Disk 1-4]
end
subgraph RackC[Rack C]
N5[Node5<br/>Disk 1-4]
N6[Node6<br/>Disk 1-4]
end
Stripe[Stripe<br/>Columns=6] --> N1
Stripe --> N3
Stripe --> N5
Stripe --> N2
Stripe --> N4
Stripe --> N6
style RackA fill:#ffe4e1
style RackB fill:#e0f7fa
style RackC fill:#f1f8e9
style Stripe fill:#fff3cd
5.4 一个容易踩的坑:Fault Domain 配错会直接锁死 Column
例如一个 4 Node 集群,如果:
- 物理上 Node1 和 Node2 在同一 Rack
- 但 RAC 配置没有声明 Rack Fault Domain
那么 Column 选择只会避开 Node,而不会避开 Rack------一旦这个 Rack 整体掉电(交换机 / PDU 故障),数据可能因"两个副本恰好在同一 Rack"而不可恢复。
因此 Azure Local RAC 部署时,正确声明 Fault Domain 拓扑是数据可靠性的硬性前提,不是"建议"。
6. Slab 大小估算(实战)
6.1 为什么估算 Slab 大小
Slab 大小直接决定:
- Metadata 表的条目数(影响内存与启动速度)
- Repair 粒度(影响重建性能)
- 单次 IO 跨多少块盘
知道 Slab 大小有助于调优 Pool。
6.2 微软公开的 Slab 规则
微软在公开文档中给出过一个经验公式(属于"基于已公开文档的推断",不是精确数学公式):
Slab 大小 ≈ Pool 总容量 / 物理盘数 / 某个常数
实际经验:
- 在小规模 Pool(< 100 TB)中,Slab 通常在 256 MiB ~ 1 GiB 范围
- 在大规模 Pool(> 500 TB)中,Slab 会被进一步切分
6.3 影响 Slab 大小的因素
| 因素 | 影响 |
|---|---|
| 物理盘数 | 越多 → Slab 越小(Metadata 越多) |
| 单盘容量 | 越大 → Slab 越大 |
| 启用 Nested Resiliency(双层 Resiliency) | Slab 重新划分 |
| Performance History | 算法根据访问模式动态调整 |
6.4 通过 PowerShell 查看 Slab 信息
# 查看 Pool 的 Slab 配置(部分字段需要 Windows Server 2022+)
Get-StoragePool -FriendlyName S2DPool | Format-List *
# 查看 Virtual Disk 的 Column 数
Get-VirtualDisk | Select-Object FriendlyName, NumberOfColumns, ResiliencySettingName
# 查看每个盘的 Physical Extent 分布
Get-PhysicalDisk | Get-Disk | Get-Partition | Get-Volume
说明 :微软未公开完整的 Slab 大小计算公式;上面只是经验范围,不写"权威数字"。
7. Repair 路径详解
7.1 Repair 是什么
当一块盘或一个 Node 出现故障后,Storage Spaces 会从 Pool 内其他健康的盘 读取副本/校验数据,重新生成丢失的副本,并写入新的位置------这个过程叫 Repair。
7.2 Repair 的两种场景
| 场景 | 触发条件 | 重建来源 |
|---|---|---|
| Disk Repair | 单盘坏 | 同 Pool 内其他盘 |
| Node Repair | 整机宕机 | 同 Pool 内其他 Node |
7.3 Repair 的流程
sequenceDiagram
participant Disk as 故障盘
participant Meta as Pool Metadata
participant Src as 源盘(健康副本)
participant Dst as 目标盘(写入新副本)
participant App as 前端应用
Disk->>Meta: 检测到故障 / 标记 offline
Meta->>Meta: 列出该盘上所有 Slab
loop 每个 Slab
Meta->>Src: 读取现有副本(Mirror 或 Parity 重算)
Src-->>Meta: 返回数据
Meta->>Dst: 写入新副本
Dst-->>Meta: ACK
Meta->>Meta: 更新 Metadata
end
Meta->>App: 通知 Repair 完成
7.4 Repair 的关键认知
(1) Repair 的源数据可以是任意健康盘
不像传统 RAID 只能从同盘组修复,S2D 可以从整个 Pool 任意盘读源数据。这就是"Every Drive is a Spare"。
(2) Repair 是后台任务
Repair 默认是后台异步执行,不阻塞前端 IO;但会消耗 IO 带宽,可能影响业务性能。
(3) 多个盘同时故障的 Repair 顺序
微软按"重要数据先修"的策略,但具体排序算法未公开。
(4) Azure Local 上的 Repair 加速
Windows Server 2022+ 起,Azure Local 上的 Repair 速度比传统 S2D 更快;微软未公开具体加速比,不写绝对数字。
8. 与 Ceph / vSAN / 传统 RAID 的对比
| 维度 | 传统硬件 RAID | S2D / Azure Local | Ceph | VMware vSAN |
|---|---|---|---|---|
| 数据单位 | LBA / Stripe | Slab + 64 MiB Region | Object + PG | Component |
| 副本位置 | 固定盘对 | Column 轮转 | CRUSH 哈希 | RAID-1/5/6 策略 |
| 校验位 | 固定盘 | P、Q Stripe 轮转 | Jerasure / ISA-L | RAID-5/6 擦除 |
| 故障域 | 单台服务器 | Disk / Node / Enclosure / Rack | Host / Rack / Room | Host / Rack |
| Metadata | RAID 卡 NVRAM | Pool Metadata 全局 | OSD Map + PG Log | DOM + Witness |
| 修复来源 | 同盘组热备 | 整个 Pool | 整个 Cluster | 整个 Cluster |
| 横向扩展 | 不支持 | 原生 2~16 Node | 无理论上限 | 推荐 2~64 Host |
| Placement 算法 | 静态 | 动态 Fault Domain | CRUSH 哈希 | 策略 + Witness |
结论:S2D 在数据布局算法上接近 Ceph / vSAN(都是元数据驱动的分布式放置),但缺少 Placement Group 这类"局部修复"机制,全局 Metadata 是它的最大瓶颈。
9. 总结
flowchart TD
A[VM 写入请求] --> B[ReFS<br/>64 MiB Region 划分]
B --> C[Storage Spaces Metadata]
C --> D[Slab Mapping<br/>查找物理位置]
D --> E[Column Selection<br/>Fault Domain Aware]
E --> F1[Mirror: 三副本轮转]
E --> F2[Parity: P Q 轮转]
E --> F3[MAP: 先 Mirror 后 Rotate]
F1 --> G[Physical Disk]
F2 --> G
F3 --> G
style A fill:#e1f5ff
style B fill:#fff3cd
style E fill:#d4edda
style G fill:#f8d7da
9.1 与传统 RAID 的本质差异
| 维度 | 传统 RAID | Storage Spaces / S2D |
|---|---|---|
| 数据单位 | LBA / Stripe | Slab + 64 MiB Region |
| 副本位置 | 固定盘对 | 动态 Column 轮转 |
| 校验位 | 固定盘 | P、Q 都参与 Stripe 级轮转 |
| 故障域 | 单台服务器 | Disk / Node / Enclosure / Rack |
| Metadata | RAID 卡 NVRAM | 全局 Pool Metadata |
| 修复来源 | 同盘组热备 | 整个 Pool 任意盘 |
| 横向扩展 | 不支持 | 原生支持 2~16 Node |
9.2 与分布式 SDS 的相似性
正因为这套机制------Slab、Column 轮转、Fault Domain Aware、Pool 级 Metadata------S2D 才被普遍认为更接近 Ceph(CRUSH + PG) 、VMware vSAN(RAID-5/6 擦除编码) 、Google Colossus,而不是传统 RAID:
数据布局是由元数据驱动的分布式放置算法决定的,而不是固定 RAID 条带或固定磁盘顺序。
这也是为什么本文把它叫作"五层数据布局原理"------微软文档不写 Slab、不写 Column 轮转算法、不写 Fault Domain 权重,是因为这些实现细节在官方层面属于"内部行为",但它们恰恰是理解 S2D 性能、容量、修复行为的关键。
10. 后续阅读
- 第二篇:Mirror / Parity / MAP 数据流与 Region Rotation 的细节
- 第三篇:为什么 3 节点必须 3-way Mirror、4 节点才能用 Parity、为什么单校验被淘汰
- 第四篇:Windows Server 2025 在 Storage Spaces 上做了什么、没做什么,以及下一代方向预测