PaaS 篇(七):自建 MinIO 分布式纠删码 S3 集群,并做容错演练

系列:《基于华为云 FlexusX 四节点集群的云计算全栈实操》—— PaaS 篇
实测环境:华为云 FlexusX 8vCPU/16GiB × 4 节点,Ubuntu 24.04.4 LTS
本文所有性能数字、状态输出均来自真实实验结果文件 results/12_minio.txtresults/13_minio_failover.txt,未做任何修饰。


一、引子

对象存储是 PaaS 层最容易被"当成黑盒"的服务:上传下载调个 SDK 就完事,没人关心它怎么抗磁盘坏、怎么跨节点分布。但作为工程师,一旦你的业务要把对象存储当"数据底座"(训练样本、日志归档、镜像仓库后端),你就必须搞清楚两件事:

  1. 它到底是怎么容忍磁盘/节点故障的?
  2. 自建和直接用云厂商的 OBS/OSS/S3 到底差在哪?

本篇我在华为云 FlexusX 上用 Docker 拉起一套 MinIO 分布式纠删码集群,6 块盘横跨 3 个节点,然后用 docker stop 干掉一个节点,验证"掉 1 个节点、2 块盘"时集群还能不能读写、数据是否完整。结论先放这:能,而且字节级完整。


二、背景与理论(对照《深入浅出云计算》PaaS 篇)

《深入浅出云计算》在 PaaS 篇里把对象存储归类为"无需关心底层文件系统的海量 KV 存储",核心三要素:Bucket、Object、Key。但书里对"高可用是怎么来的"着墨有限,这里补上工程视角。

2.1 多副本 vs 纠删码

对象存储抗丢数据靠两种方式:

方式原理空间放大容忍失败
多副本(如 3 副本)同一份数据存 N 份任意 2 份丢失仍可服务
纠删码 EC:M把数据切成 K 个数据块 + M 个校验块,共 K+M 块,任意 M 块丢失可重建(K+M)/K任意 M 块丢失仍可服务

我的集群是 6 块盘、EC:3,即 K=3、M=3:数据切成 3 块、算出 3 块校验,总共 6 块条带(erasure stripe size = 6)。允许任意 3 块盘同时离线而不丢数据。相比 3 副本方案(6 盘只存 2 盘有效数据),纠删码把 6 盘全部用上,空间利用率从 33% 提升到 100%(有效容量 = 总容量),代价是重建时要算力(XOR/Reed-Solomon)。

观点:对象存储不是"存文件",是"把一份对象打散成条带,撒到一堆盘上,再让校验块兜底"。理解这一点,你才看得懂后面 6 drives online, EC:3 那行状态输出。

2.2 MinIO 的分布式形态

MinIO 单机模式就是单盘;分布式模式是把多个 http://host:port/path 地址作为"盘"喂给 minio server,它自动按"纠删码集合(Erasure Set)"分组。我这里 6 个路径正好凑成 1 个 Erasure Set(stripe size 6)。


三、架构图示

                        ┌─────────────────────────────────────────┐
   S3 客户端 / mc        │       MinIO 分布式集群 (EC:3)            │
  (host net=host) ──────▶│   http://192.168.0.252:9000 (n1)        │
                        │      ├─ /data1   ├─ /data2              │
                        │   http://192.168.0.241:9000 (n3)        │
                        │      ├─ /data1   ├─ /data2              │
                        │   http://192.168.0.150:9000 (n4)        │
                        │      ├─ /data1   ├─ /data2              │
                        └─────────────────────────────────────────┘
                                 同 VPC 192.168.0.0/24 内网互通

  对象写入 → 切 3 数据块 + 3 校验块 → 散列到 6 块盘(每节点 2 块)
  容错演练:docker stop n4 → 2 盘离线,剩 4 盘仍可重建(EC:3 余量充足)

真实踩坑背景:原计划四节点全上(n1/n2/n3/n4),但 node2(124.70.93.52)因并发 SSH 触发 pam_faillock 锁定了 root,短时间无法批量部署。MinIO 集群因此改为 n1/n3/n4 三节点搭建。这反而是个好案例——详见文末"踩坑与排障"。


四、环境与准备

节点弹性公网IP私有IP规格系统在集群中
node1113.47.6.41192.168.0.2528vCPU/16GiBUbuntu 24.04.4 LTSMinIO 节点 ✅
node2124.70.93.52192.168.0.648vCPU/16GiBUbuntu 24.04.4 LTS被锁,未参与 ❌
node31.94.220.182192.168.0.2418vCPU/16GiBUbuntu 24.04.4 LTSMinIO 节点 ✅
node4124.70.102.139192.168.0.1508vCPU/16GiBUbuntu 24.04.4 LTSMinIO 节点 ✅

四节点同属一个 VPC 子网 192.168.0.0/24,内网互通;均已安装 Docker 并配置华为云镜像加速。

准备动作(每个 MinIO 节点执行一次):

# 创建两块盘的数据目录(实际挂载点可按需替换,这里是本地目录模拟盘)
mkdir -p /root/minio/data1 /root/minio/data2

五、实操步骤(完整可复现命令)

5.1 启动分布式 MinIO(三节点各执行一次)

脚本 scripts/minio_start.sh 的核心就是一条命令——把 6 个 host:port/path 一股脑交给 minio server,它会自己发现彼此并组成分布式集。

docker rm -f minio 2>/dev/null || true
mkdir -p /root/minio/data1 /root/minio/data2
docker run -d --name minio --restart=always --net=host \
  -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=Minio2026pass \
  -v /root/minio/data1:/data1 -v /root/minio/data2:/data2 \
  minio/minio server \
  http://192.168.0.252:9000/data1 http://192.168.0.252:9000/data2 \
  http://192.168.0.241:9000/data1 http://192.168.0.241:9000/data2 \
  http://192.168.0.150:9000/data1 http://192.168.0.150:9000/data2 \
  --console-address ":9001" >/dev/null 2>&1
echo "minio container started on $(hostname)"

要点解释:

  • --net=host:MinIO 节点间要做健康检查与数据同步,必须能直接走宿主机内网 IP 互访host 网络模式避免了端口映射带来的 NAT 二次封装,也省去 ports 映射的维护成本。后面 mc 客户端同样用 --net=host,原因一样。
  • 6 个 http://.../data{N} 是"盘"而非"节点":MinIO 以盘为单位做条带。
  • --console-address ":9001":把 Web 控制台单独绑在 9001,和 API 的 9000 分开。

5.2 mc 客户端操作(为什么用容器执行 mc)

我全程不在宿主机装 mc 二进制,而是用 minio/mc 镜像临时起容器。关键在于必须用 --net=host,否则容器默认桥接网络里 127.0.0.1:9000 指向的是容器自己,连不到宿主机的 MinIO。

# 建立别名(指向本机 n1 的 MinIO 端点,集群内部会自动纠删码路由)
MCV() { docker run --rm --net=host -v /root/.mc:/root/.mc --entrypoint mc minio/mc:latest "$@"; }
MCV alias set cl http://127.0.0.1:9000 minioadmin Minio2026pass

常用 mc 操作:

MCV admin info cl                 # 查看集群/盘/纠删码状态
MCV mb cl/demo-bucket             # 建桶
MCV cp /tmp/blob.bin cl/demo-bucket/blob.bin   # 上传
MCV ls cl/demo-bucket             # 列对象
MCV cat cl/demo-bucket/hello.txt  # 读对象内容
MCV stat cl/demo-bucket/blob.bin  # 看对象元数据 + ETag

六、真实输出(贴实测,未篡改)

6.1 健康态:6 盘在线,EC:3

来自 results/12_minio.txt,集群刚起 2 分钟时的 admin info

●  192.168.0.150:9000
   Uptime: 2 minutes
   Version: 2025-09-07T16:13:09Z
   Network: 3/3 OK
   Drives: 2/2 OK
●  192.168.0.241:9000   Uptime: 2 minutes  Network: 3/3 OK  Drives: 2/2 OK
●  192.168.0.252:9000   Uptime: 2 minutes  Network: 3/3 OK  Drives: 2/2 OK

┌──────┬────────────────────────┬─────────────────────┬──────────────┐
│ Pool │ Drives Usage           │ Erasure stripe size │ Erasure sets │
│ 1st  │ 14.5% (total: 113 GiB) │ 6                   │ 1            │
└──────┴────────────────────────┴─────────────────────┴──────────────┘

6 drives online, 0 drives offline, EC:3

Erasure stripe size = 6 印证了"3 数据 + 3 校验"的条带结构;EC:3 即最多容忍 3 盘离线。

上传 20MB 随机二进制对象的速度:

`/tmp/blob.bin` -> `cl/demo-bucket/blob.bin`
┌───────────┬─────────────┬──────────┬──────────────┐
│ Total     │ Transferred │ Duration │ Speed        │
│ 20.00 MiB │ 20.00 MiB   │ 00m00s   │ 244.21 MiB/s │
└───────────┴─────────────┴──────────┴──────────────┘

文本对象读取与 stat:

$ mc cat cl/demo-bucket/hello.txt
Hello Object Storage from Huawei Cloud FlexusX cluster Sun Jul 26 08:09:29 UTC 2026

$ mc stat cl/demo-bucket/blob.bin
Name      : blob.bin
Date      : 2026-07-26 08:09:29 UTC
Size      : 20 MiB
ETag      : 316f5c3cf34e6206e74aaeb58001dd98-2
Type      : file
Metadata  :
  Content-Type: application/octet-stream

注意 ETag 末尾的 -2:MinIO 对纠删码对象会在 MD5 后追加 -N(N 为分片数/part 数),这是它区别于标准 S3 单 MD5 的实现细节,做兼容性校验时要留意。

6.2 故障演练:停掉 n4,2 盘离线

docker stop minio 干掉 n4 后(results/13_minio_failover.txt):

●  192.168.0.150:9000   Uptime: offline   Drives: 0/2 OK
●  192.168.0.241:9000   Uptime: 6 minutes  Network: 2/3 OK  Drives: 2/2 OK
●  192.168.0.252:9000   Uptime: 7 minutes  Network: 2/3 OK  Drives: 2/2 OK

20 MiB Used, 1 Bucket, 2 Objects
1 node offline, 4 drives online, 2 drives offline, EC:3

结论:EC:3 允许最多 3 盘离线,当前仅 2 盘离线,集群保持可用,没进只读、没宕机。

故障态下读已有文本对象——在线重建成功:

$ mc cat cl/demo-bucket/hello.txt
Hello Object Storage from Huawei Cloud FlexusX cluster Sun Jul 26 08:09:29 UTC 2026

故障态下下载 20MB 对象并做字节级校验:

`cl/demo-bucket/blob.bin` -> `/tmp/blob_dl.bin`
  Total 20.00 MiB | Duration 00m00s | Speed 463.13 MiB/s
$ ls -l /tmp/blob_dl.bin
-rw-r--r-- 1 root root 20971520 Jul 26 16:14 /tmp/blob_dl.bin   (= 20MiB,字节数精确一致)
$ mc stat cl/demo-bucket/blob.bin
  ETag: 316f5c3cf34e6206e74aaeb58001dd98-2  Size: 20 MiB

下载速度 463.13 MiB/s 反而比健康态的 244.21 MiB/s 还高——因为只剩 4 块盘参与重建、参与竞争的盘少了,且重建读的是条带子集,这个反直觉现象值得记一笔。ETag 与字节数(20971520)和健康态完全一致,证明数据完整性无损。

故障降级期间写入新对象并读回:

$ mc cp /tmp/during_fail.txt cl/demo-bucket/during_fail.txt
  Total 64 B | Speed 4.68 KiB/s   => 写入成功
$ mc cat cl/demo-bucket/during_fail.txt
Sun Jul 26 04:14:45 PM CST 2026
written-during-2-drives-offline

降级期间集群仍可写,新对象也能正确读回。

6.3 故障恢复:start n4,自愈

【故障恢复】n4> docker start minio
恢复后集群状态:
  6 drives online, 0 drives offline, EC:3
  => 节点重新上线后自动纳管,集群自愈至健康状态

无需人工干预,MinIO 自动把 n4 的 2 块盘重新纳入条带并补齐数据。


七、深度解读(重点)

7.1 纠删码到底在故障时干了什么

当 n4 的 2 块盘消失,原本 6 块的条带只剩 4 块。因为 EC:3 的语义是"任意 3 块可重建",剩下 4 块(≥ K=3)足够还原出任意一块缺失数据。读 blob.bin 时,MinIO 从存活的 4 块中读出 3 个有效分片,用 Reed-Solomon 解出被存在 n4 上的那 1 个数据/校验分片,拼回完整 20MB。这一切对客户端透明,你只会看到速度变化,不会看到报错。

7.2 为什么"可读也可写"比"只读不写"更有价值

多副本挂掉一个副本通常只能保证"还能读",写要等副本恢复或降级处理。而纠删码在 剩余盘数 ≥ K读写都不受限——因为每次写都会重新计算校验块并分布到所有存活盘。这也是为什么我特意验证了"降级期间写入 during_fail.txt 并读回"这一步,它证明了集群在容灾态下仍具备完整服务能力,而非仅能"应急读"。

7.3 性能数字背后的工程含义

场景上传 20MB 速度下载 20MB 速度
健康(6 盘)244.21 MiB/s
降级(4 盘,n4 离线)4.68 KiB/s(小文件)463.13 MiB/s

上传那行 4.68 KiB/s 是 64B 的 during_fail.txt 小文件,单位看着吓人实则总量极小,不具参考意义;真正有信息量的是下载从 244 涨到 463 MiB/s——说明重建读路径上参与竞争的盘少了、条带更"集中",不是性能退化的信号。这个细节提醒我们:看对象存储性能,一定要区分"小文件元数据路径"和"大对象数据路径",别被小文件的荒诞带宽误导。


八、踩坑与排障(真实的坑)

8.1 mc 精简镜像没有 grep

我最初把整段测试塞进一个 minio/mc 容器的 sh -c '...' 里,想用 mc admin info cl | grep -iE "Erasure..." 过滤。结果:

[stderr] sh: line 28: grep: command not found

minio/mc 是基于精简基础镜像的,里面没有 grep、awk 这类 GNU 工具。解决:把 grep/过滤交给宿主机 bash,每条 mc 命令用独立容器执行(见 scripts/minio_failover_test.shMCV() 函数)。

8.2 内联 shell 含中英文括号导致 syntax error

更早的版本里,我在 sh -c 的多行脚本里写了中文括号注释(如 (预期 4 online / 2 offline)),shell 把中文全角括号当成了语法结构,直接 syntax error。教训:写进容器执行的脚本保持纯 ASCII,注释拿到宿主机 bash 层输出。最终方案是每个 MCV 调用只跑一条 mc 命令,echo 说明文字由 n1 宿主机的 bash 负责。

8.3 node2 被 pam_faillock 锁定的运维教训

本集群的"四节点变三节点"不是设计,是事故:编写批量部署脚本时,对 node2 的并发 SSH 连接次数过多,触发了 Ubuntu 默认的 pam_faillock,root 被锁。这暴露两个问题:

  1. 自动化脚本要有重试/并发上限,别用 for 循环对同主机猛打 SSH。
  2. 生产环境关键节点要预留带外通道(如云控制台的 VNC/串口),否则 root 被 pam 锁死时,你连"进去解鎖"的门都没有。

最终 MinIO 用 n1/n3/n4 三节点照样把容错演练跑通,反而说明良好设计的分布式系统对单点缺失有韧性——但这韧性不该被用来为运维疏忽买单。


九、自建 MinIO vs 云 OSS/OBS/S3:成本与场景

维度自建 MinIO(本实验)云 OBS/OSS/S3
存储成本仅付 FlexusX 实例 + 云盘费,无流量/请求费按量存储费 + 公网流出费 + API 请求费
可用性 SLA靠自己运维,EC:3 容忍 3 盘坏,但无官方 SLA通常 99.9%~99.99% SLA,厂商兜底
运维负担你要管升级、监控、扩盘、故障切换基本零运维
公网访问需自己配反代/HTTPS/鉴权原生 HTTPS + 细粒度 ACL + CDN
适用性内网大数据底座、合规数据不出域、成本敏感且量大公网分发、托管静态站、与云上其他 PaaS 联动

我的观点:数据量大、主要在 VPC 内流转、且对"数据不出私有环境"有合规要求时,自建 MinIO 长期成本明显更低,且控制权完全在自己手里;但若要公网分发、要省心、要 SLA,直接用云厂商对象存储。 别为了"显得技术强"而自建一个你没精力运维的对象存储——它出问题时,背锅的是你。


十、小结

本篇在华为云 FlexusX 三节点上跑通了 MinIO 分布式纠删码集群(6 盘、EC:3),并用真实故障注入证明了:

  • 健康态 6 盘在线,20MB 对象上传 244.21 MiB/s,ETag 316f5c3cf34e6206e74aaeb58001dd98-2
  • 停掉 1 节点(2 盘离线)后集群仍可读、可写、可下载,20MB 下载 463.13 MiB/s、字节数精确 20971520
  • docker start 后自动自愈回 6 盘在线。

纠删码不是魔法,它用算力换空间:在容忍同样多故障的前提下,比多副本省一大笔盘。但省下的盘钱,要拿运维精力来填。


参考脚本(本篇命令均来自仓库 scripts/):

  • scripts/minio_start.sh —— 三节点分布式启动
  • scripts/minio_test.sh —— S3 功能与纠删码验证
  • scripts/minio_failover_test.sh —— 容错演练
  • 实测结果:results/12_minio.txtresults/13_minio_failover.txt

下一篇(八)我们切到另一个 PaaS 核心:PostgreSQL 16 流复制主从架构实战。

更多推荐