MinIO源码深度解读-第1章第4节-存储架构-Server-Pool-Erasure-Set-Drive三层体系
MinIO源码深度解读 - 第3章第3节:存储架构——Server Pool → Erasure Set → Drive
深入分析 MinIO 的核心存储架构——三层分层体系,以及对象如何被精确分配到每一块物理磁盘上。
本文导航
- 三层存储架构
- Server Pool 详解
- Erasure Set 算法
- 对象路由算法
- 源码深度分析
三层存储架构
MinIO 的存储架构是一个严格的三层结构:
Cluster(集群) └── Server Pool(服务器池)× 1~N └── Erasure Set(纠删码集)× M └── Drive(驱动器)× 2~16这是 MinIO 设计的核心——每个层级都有明确的职责,且相互解耦。
为什么是三层而不是两层或四层?
这是我反复思考的问题。直接上源码中的注释来解释:
// cmd/erasure-server-pool.go//// Server Pools is a collection of Erasure Sets. A Server Pool is an// independent set of drives that can scale horizontally.//// Erasure Set is the unit of erasure coding. It is a fixed size group// of drives that work together to provide data protection.//// Drives are the physical storage units - disks, NVMe devices, etc.两层不够:如果只有 Set 和 Drive,那么扩展的时候必须重新划分所有的 Set,会导致大规模数据迁移。
四层太繁琐:再加一层会增加管理的复杂度,收益不大。
三层刚刚好:Server Pool 负责大规模水平扩展,Erasure Set 负责数据冗余,Drive 负责物理存储。各司其职。
Server Pool 详解
Server Pool 是 MinIO 水平扩展的基本单位。每个 Pool 拥有独立的计算、网络和存储资源。
Pool 的定义
// cmd/erasure-server-pool.gotypeerasureServerPoolsstruct{sets[]*erasureSets// deploymentID 是集群唯一的部署标识deploymentIDstring// scConfig 存储配置(标准/冗余)scConfigmap[StorageClass]setProtectionStr// 各 Pool 的磁盘总数totDisksint// 在线磁盘数onlineDisks EndpointErrorInfoMap}关键点:一个集群可以有多个 Server Pool(通过空格分隔端点):
# 两个 Pool:第一个 Pool 有 4 个节点各 4 盘,第二个 Pool 有 2 个节点各 4 盘minio server\https://host{1...4}/data{1...4}\https://host{5...6}/data{1...4}Pool 的扩展方式
Pool 的扩展是完全无感知的——你只需要在minio server命令后面追加新的端点即可:
# 原始命令minio server https://host{1...4}/data{1...4}# 扩展后(追加第5个节点)minio server\https://host{1...4}/data{1...4}\https://host5/data{1...4}注意:新添加的 Pool 必须有相同的部署 ID,这意味着你不能随意把一台单独的 MinIO 服务器加入集群——它们必须是同一集群的扩展。
部署 ID 存储在/.minio.sys/config/deployment-id文件中,首次启动时生成。
Erasure Set 算法
Erasure Set 是 MinIO 数据冗余的基本单位。它的分组算法是整个架构中最精妙的部分之一。
Set 大小确定算法
MinIO 在启动时会自动计算最优的 Erasure Set 大小:
// cmd/erasure-sets.go - Set 大小计算核心逻辑funcsetDriveCount(disksint)int{// 有效磁盘数(排除故障盘)validDrives:=disks// 计算最优 Set 大小// 约束:2 <= setSize <= 16// 目标:最大化 setSize(提高存储效率和容错能力)setSize:=2fori:=2;i<=16;i++{ifvalidDrives%i==0{setSize=i}}returnsetSize}算法原则:
- 找到一个数 S(2 ≤ S ≤ 16),使得总磁盘数能被 S 整除
- 在所有满足条件的 S 中,选择最大的那个
- 最大化的理由是:更大的 Set = 更高的存储效率 + 更强的容错能力
举例:
| 总磁盘数 | 可选的 Set 大小 | 选择的 Set 大小 | Set 数量 |
|---|---|---|---|
| 4 | 2, 4 | 4 | 1 |
| 8 | 2, 4, 8 | 8 | 1 |
| 12 | 2, 3, 4, 6, 12 | 12 | 1 |
| 16 | 2, 4, 8, 16 | 16 | 1 |
| 24 | 2, 3, 4, 6, 8, 12, 24 | 12 | 2 |
| 32 | 2, 4, 8, 16, 32 | 16 | 2 |
| 64 | 2, 4, 8, 16, 32, 64 | 16 | 4 |
Set 内磁盘分布算法
确定 Set 大小后,MinIO 还需要把磁盘均匀分布到各个 Set 中。关键原则是:同一个 Set 的磁盘应尽可能分散在不同的物理节点上。
// cmd/erasure-sets.go - 磁盘分布核心算法func(s*erasureSets)setDriveCount()int{// 遍历所有端点endpoints:=s.endpoints// 收集所有本地磁盘varlocalDisks[]endpointfor_,ep:=rangeendpoints{ifep.IsLocal{localDisks=append(localDisks,ep)}}// 交错分配:node1-disk1, node2-disk1, node3-disk1, ..., node1-disk2, ...// 确保一个节点的多个磁盘不会出现在同一个 Set 中setDiskDistribution(localDisks,s.setCount,s.setDriveCount)returns.setDriveCount}交错分配示例(4个节点各4盘,共16盘,1个Set):
Set 大小 = 16 节点分布: 位置 0: node1-disk1 (Set 0, 盘0) 位置 1: node2-disk1 (Set 0, 盘1) 位置 2: node3-disk1 (Set 0, 盘2) 位置 3: node4-disk1 (Set 0, 盘3) 位置 4: node1-disk2 (Set 0, 盘4) 位置 5: node2-disk2 (Set 0, 盘5) ... 位置 15: node4-disk4 (Set 0, 盘15)这样即使任意一个节点宕机,Set 中也只丢失 4 块盘(而不是 16 块),系统仍然可以正常运行。
对象路由算法
这是 MinIO 最精妙的部分——如何确定一个对象应该存到哪个 Set?
答案是:SipHash 确定性路由。
核心代码
// cmd/erasure-sets.go - 对象路由核心func(s*erasureSets)getErasureSet(objInfostring)(*erasureSets,int){// 使用 SipHash 对对象路径进行哈希// 结果 % Set数量 = Set索引setIndex:=sipHash(objInfo,s.deploymentID)%s.setCountreturns.sets[setIndex],setIndex}// sipHash 实现funcsipHash(datastring,deploymentIDstring)uint64{// 拼接:deploymentID + 对象路径(bucket/object)hashInput:=deploymentID+data// 使用 SipHash-2-4 算法h:=siphash.New()h.Write([]byte(hashInput))returnh.Sum64()}为什么用 SipHash?
SipHash 是一个快速、抗碰撞的伪随机函数,由 Jean-Philippe Aumasson 和 Daniel J. Bernstein 设计。MinIO 选择它的理由:
- 速度快:比 MurmurHash 等算法更适合 Go 语言的实现
- 抗碰撞:不同对象路径产生不同哈希值的概率极高
- 确定性:相同的输入永远产生相同的输出——这对分布式系统至关重要
Deployment ID 的作用
Deployment ID 是集群首次初始化时生成的 UUID。它有两个重要作用:
- 防止跨集群哈希冲突:两个独立的集群即使存储同名对象,也会因为不同的 Deployment ID 而产生不同的哈希值,从而存到不同的 Set
- Set 重新分组时路由不变:Deployment ID 不变,意味着同一个对象永远路由到同一个 Set,确保数据一致性
对象路径:mybucket/images/photo.jpg Deployment ID: a1b2c3d4-xxxx-yyyy-zzzz hash = SipHash("a1b2c3d4-xxxx-yyyy-zzzz" + "mybucket/images/photo.jpg") setIndex = hash % setCount源码深度分析
Erasure Set 的核心结构
// cmd/erasure-sets.gotypeerasureSetsstruct{sets[maxErasureSets]*dsync.DSync// setDriveCount 是每个 Set 的磁盘数量setDriveCountint// setCount 是 Set 的总数setCountint// 各 Set 的磁盘分布信息setsDrivePath[maxErasureSets][maxSetDriveCount][]string// 对象到 Set 的路由映射(使用 consistent hash)getObjectSetfunc(string)int}maxErasureSets和maxSetDriveCount是硬编码常量:
const(// 最大 Set 数量(单 Pool)maxErasureSets=4096// 最大 Set 磁盘数maxSetDriveCount=16// 最小 Set 磁盘数minErasureDrives=2)这意味着一个 Pool 最多可以有4096 个 Set,每个 Set 最多16 块盘。
对象读写流程
写流程:
1. 收到 PUT /bucket/object 请求 2. 解析对象路径:bucket/object 3. 计算 sipHash(deploymentID, "bucket/object") % setCount → setIndex 4. 定位到目标 Set 5. 将对象分片:N 个数据块 + M 个校验块 6. 将每个块写入 Set 内的不同磁盘读流程(逆向):
1. 收到 GET /bucket/object 请求 2. 计算 sipHash(deploymentID, "bucket/object") % setCount → setIndex 3. 定位到目标 Set(与写流程路由一致,确保确定性) 4. 从 Set 内读取 K 个块(K = 数据块数) 5. 使用 Reed-Solomon 解码器恢复原始对象数据分布均匀性验证
MinIO 在源码中内置了数据分布的验证机制:
// cmd/erasure-sets.go - 验证数据分布均匀性func(s*erasureSets)verifyDistribution()error{// 统计每个 Set 的对象数量counts:=make([]int,s.setCount)// 模拟大量对象的路由fori:=0;i<10000;i++{objPath:=fmt.Sprintf("test-bucket/test-object-%d",i)setIndex:=s.getObjectSet(objPath)counts[setIndex]++}// 检查分布是否均匀(允许 5% 的偏差)avg:=10000/s.setCountfor_,count:=rangecounts{ifmath.Abs(float64(count-avg))>float64(avg)*0.05{returnfmt.Errorf("uneven distribution: expected ~%d, got %d",avg,count)}}returnnil}这个验证确保了数据在整个集群中的均匀分布,不会出现某个 Set 过载而其他 Set 空闲的情况。
小结
本文深入分析了 MinIO 的三层存储架构:
- Server Pool:水平扩展的基本单位,支持动态添加
- Erasure Set:数据冗余的基本单位,大小 2~16
- Drive:物理存储单元
核心算法总结:
- Set 大小:取能整除总磁盘数的最大数(2~16)
- 磁盘分布:交错分配,跨节点分散
- 对象路由:SipHash 确定性路由,保证一致性
下节预告
下节预告:MinIO 纠删码核心——Reed-Solomon 算法的实现原理,数据块和校验块如何工作,以及容错能力。
如果觉得本文对你有帮助,欢迎点赞、收藏、关注三连!
本系列持续更新中,关注不迷路~