MinIO 是 S3 兼容的开源对象存储——国内用得最多的"自建 S3"。本篇把单节点部署、4 节点集群搭建、桶复制双向同步、漏洞修复、Spring Boot 集成一次性整理清楚。
阅读对象:需要自建对象存储的后端 / 运维工程师
覆盖范围:MinIO 单节点 / 4 节点集群 / mc 客户端 / 桶复制 / 主主双向同步 / CVE 漏洞修复 / Nginx 反代安全加固 / Spring Boot 集成 / 分片上传 / 断点续传
一、MinIO 简介
MinIO 的核心特点:
- S3 兼容:所有 S3 SDK / 工具都能直接对接
- 高性能:单机读写可达 100+ GB/s
- 纠删码:默认 EC:4(4 份数据分片可容忍 4 块盘坏)
- 多语言 SDK:Java / Go / Python / Node.js / .NET
- 部署简单:单二进制 + 一个目录就能跑
对比:
| 维度 | MinIO | Ceph | SeaweedFS |
|---|
| 部署难度 | 极简 | 复杂 | 中等 |
| 性能 | 极高 | 高 | 高 |
| 集群最少节点 | 1(4 节点为生产推荐) | 3 | 3 |
| S3 兼容 | 100% | ✅ | ✅ |
| 适用规模 | TB ~ 100PB | PB+ | TB ~ PB |
When to use:
- 替代 AWS S3、阿里云 OSS 做自托管
- 存图片 / 视频 / 备份文件 / 日志
- 大数据 / AI 训练数据湖
二、单节点部署
2.1 拉取镜像
1
| docker pull minio/minio:RELEASE.2023-06-09T07-32-12Z.fips
|
2.2 启动
1
2
3
4
5
6
7
8
9
| docker run -d --name minio --restart=always \
-p 9091:9091 \
-p 9090:9090 \
-v /home/docker/data:/data \
-e "MINIO_ROOT_USER=root" \
-e "MINIO_ROOT_PASSWORD={{REDACTED}}" \
-e "MINIO_SERVER_URL=http://internal.example.com:9091" \
minio/minio:RELEASE.2023-06-09T07-32-12Z.fips \
server /data --console-address ":9090" --address ":9091"
|
端口分配:
9091:S3 API(程序用这个)9090:Web Console(管理用这个)
环境变量:
MINIO_ROOT_USER / MINIO_ROOT_PASSWORD:管理员账号(旧版本是 MINIO_ACCESS_KEY / MINIO_SECRET_KEY)MINIO_SERVER_URL:S3 API 外部地址——代码里要用这个
2.3 启动后状态
1
2
3
| Status: 1 Online, 0 Offline.
S3-API: http://172.17.0.12:9000 http://127.0.0.1:9000
Console: http://172.17.0.12:9090 http://127.0.0.1:9090
|
进入容器:
1
2
| docker exec -it minio bash
# 配置文件:/usr/local/minio/config
|
三、4 节点集群部署
3.1 集群规划
| 节点 | IP | 角色 |
|---|
| node1-1 | 10.0.1.1 | 机房 1 |
| node1-2 | 10.0.1.2 | 机房 1 |
| node2-1 | 10.0.2.1 | 机房 2 |
| node2-2 | 10.0.2.2 | 机房 2 |
3.2 /etc/hosts 配置
1
2
3
4
5
6
7
| # 4 个节点都执行
cat >> /etc/hosts <<'EOF'
10.0.1.1 node1-1
10.0.1.2 node1-2
10.0.2.1 node2-1
10.0.2.2 node2-2
EOF
|
3.3 启动集群
1
2
3
4
5
6
7
8
9
| docker run -d --restart=always --name minio \
--net=host \
-e MINIO_ROOT_USER=root \
-e MINIO_ROOT_PASSWORD={{REDACTED}} \
-v /data/minio/data1:/data1 \
-v /data/minio/data2:/data2 \
minio/minio:RELEASE.2023-06-09T07-32-12Z.fips \
server http://node1-{1...2}:9000/data{1...2} http://node2-{1...2}:9000/data{1...2} \
--console-address ":9090" --address ":9091"
|
关键参数:
--net=host:集群通信必须 host 网络- 4 节点 8 盘:默认 EC:4 编码,可容忍 4 块盘同时损坏
/data1 /data2:每个节点挂 2 块盘node1-{1...2}:Docker 展开为 node1-1 node1-2
3.4 集群运维
1
2
3
4
5
| # 查看集群状态
mc admin info minio-cluster
# 看每个节点磁盘
mc admin info minio-cluster --json | jq '.info.servers[].state'
|
四、mc 客户端
4.1 安装
1
2
3
4
5
6
| # Linux
curl https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
chmod +x /usr/local/bin/mc
# Windows
# 下载 https://dl.min.io/client/mc/release/windows-amd64/mc.exe
|
4.2 常用命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # 配置 server alias
mc config host add <ALIAS> <S3-ENDPOINT> <ACCESS-KEY> <SECRET-KEY>
# 列出文件
mc ls <ALIAS>/<bucket>
# 创建 bucket
mc mb <ALIAS>/<bucket-name>
# 上传
mc cp <local-path> <ALIAS>/<bucket>/<key>
# 同步
mc mirror <src> <dst>
# 删除
mc rm <ALIAS>/<bucket>/<key> --recursive --force
|
4.3 完整示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # 1. 加 alias
mc alias set prod http://minio.internal.example.com:9091 root {{REDACTED}}
mc alias set staging http://minio-staging.internal.example.com:9091 root {{REDACTED}}
# 2. 列出所有 bucket
mc ls prod
# 3. 上传整个目录
mc cp ./uploads/ prod/file/ --recursive
# 4. 同步两个 bucket(增量)
mc mirror prod/file staging/file
# 5. 带版本号恢复
mc cp --rewind "2025-12-12T08:00:00Z" --recursive prod/file/dev/ prod/file/dev/
|
五、桶复制:跨集群数据同步
5.1 单向复制
1
2
3
4
5
6
7
8
9
| # 源集群:minio-prod
# 目标集群:minio-staging
# 在目标集群创建 bucket
mc mb minio-staging/file
# 在源集群加复制规则
mc replicate add minio-prod/file \
--remote-bucket http://{{REPL_USER}}:{{REPL_PASS}}@minio-staging.internal.example.com:9091/file
|
注意:
- 源和目标 MinIO 版本必须一致
- 桶复制只同步新文件,存量数据用
mc mirror 全量同步
5.2 双向主主复制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # 在两个集群都执行
# prod 集群
mc replicate add minio-prod/file \
--remote-bucket http://{{REPL_USER}}:{{REPL_PASS}}@minio-staging.internal.example.com:9091/file \
--replicate "delete,delete-marker,existing-objects" \
--sync
# staging 集群
mc replicate add minio-staging/file \
--remote-bucket http://{{REPL_USER}}:{{REPL_PASS}}@minio-prod.internal.example.com:9091/file \
--replicate "delete,delete-marker,existing-objects" \
--sync
# 启用版本控制
mc version enable minio-prod/file
mc version enable minio-staging/file
|
效果:
- 上传到 prod 自动同步到 staging
- 上传到 staging 自动同步到 prod
- 删除同步
停机测试:
1
2
3
4
5
6
7
8
9
10
11
| # 1. 停 staging
docker stop minio-staging
# 2. 上传到 prod
mc cp test.txt minio-prod/file/
# 3. 启动 staging
docker start minio-staging
# 4. 等待 sync 自动同步(一般 30 秒内)
mc ls minio-staging/file/
|
六、漏洞修复:桶路径遍历
6.1 漏洞描述
CVE 严重等级:高。当 MinIO 暴露在公网 + 配置 Access Policy 为 public 时,任何人通过桶路径就能列出所有文件:
1
| http://minio.example.com:9091/file/
|
返回的 XML 包含整个桶的所有文件元信息(文件名 + 大小 + 修改时间)。敏感数据直接泄露。
6.2 漏洞根源
listObjects 默认返回 1000 条- 公开 bucket + 公开 URL = 可遍历所有资源
6.3 修复方案
方案 A:Nginx 反代 + 路径屏蔽(最推荐)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| server {
listen 80;
server_name minio.example.com;
# 屏蔽桶路径直接访问
location ^~ /images {
rewrite ^/images/(.+)$ /images/$1 break;
proxy_pass http://minio_local_url/;
}
# 其他路径禁止
location / {
return 403;
}
}
|
效果:
http://minio.example.com/images/bucket/file.txt → ✅ 正常下载http://minio.example.com/images/bucket/ → ❌ 403 禁止列出
方案 B:Bucket 权限收紧
进入 MinIO Console → Bucket → Access Policy → Custom:
1
2
| Prefix: *
Access: Read Only (no List)
|
这样单个文件还能下载,但桶路径列出被禁止。
方案 C(最佳实践):
- MinIO 不暴露公网:只在内网
- Nginx 反代 + 路径重写:唯一对外的入口
- Bucket 全部 private:永远不开 public
- 预签名 URL 访问:临时分享用
mc share 或 SDK 生成
七、Spring Boot 集成
7.1 添加依赖
1
2
3
4
5
| <dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>8.5.10</version>
</dependency>
|
7.2 配置文件
1
2
3
4
5
| minio:
endpoint: http://minio.internal.example.com:9091
access-key: root
secret-key: {{REDACTED}}
bucket: file
|
7.3 Spring Boot 配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| @Configuration
public class MinioConfig {
@Value("${minio.endpoint}")
private String endpoint;
@Value("${minio.access-key}")
private String accessKey;
@Value("${minio.secret-key}")
private String secretKey;
@Bean
public MinioClient minioClient() {
return MinioClient.builder()
.endpoint(endpoint)
.credentials(accessKey, secretKey)
.build();
}
}
|
7.4 上传服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| @Service
public class MinioService {
@Autowired
private MinioClient minioClient;
public String uploadFile(MultipartFile file, String bucket) throws Exception {
String objectName = UUID.randomUUID().toString() + "-" + file.getOriginalFilename();
// 1. 确保 bucket 存在
boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
}
// 2. 上传
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build()
);
return minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.bucket(bucket)
.object(objectName)
.method(Method.GET)
.build()
);
}
}
|
八、Spring Boot 高级特性
8.1 分片上传
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| public String initiateMultipartUpload(String bucket, String fileName) throws Exception {
return minioClient.initiateMultipartUploadAsync(
InitiateMultipartUploadArgs.builder()
.bucket(bucket)
.object(fileName)
.build()
).get().uploadId();
}
public void uploadPart(String bucket, String fileName, String uploadId,
int partNumber, InputStream data, long size) throws Exception {
minioClient.uploadPartAsync(
UploadPartArgs.builder()
.bucket(bucket)
.object(fileName)
.uploadId(uploadId)
.partNumber(partNumber)
.stream(data, size, -1)
.build()
).get();
}
|
8.2 断点续传
1
2
3
4
5
6
7
8
9
10
| // 1. 客户端检查已上传的分片
ListPartsResponse parts = minioClient.listParts(listPartsArgs);
// 2. 跳过已上传的分片
for (int i = 0; i < parts.result().partList().size(); i++) {
Part part = parts.result().partList().get(i);
log.info("已上传分片: {} ({} bytes)", part.partNumber(), part.size());
}
// 3. 接着上传未完成部分
|
8.3 秒传
1
2
3
4
5
6
| public boolean isFileExist(String bucket, String md5) throws Exception {
Iterable<Result<Item>> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(bucket).prefix(md5).build()
);
return results.iterator().hasNext();
}
|
前端流程:
1
2
3
4
| 1. 计算文件 MD5
2. 调后端接口 "isFileExist(md5)"
3. 如果存在 → 秒传成功(直接返回 URL)
4. 如果不存在 → 走后端生成上传凭证 → 前端直传 MinIO
|
8.4 文件合并
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // 前端传完所有分片后调
public void composeFile(String bucket, String targetName, List<String> parts) throws Exception {
List<ComposeSource> sources = new ArrayList<>();
for (int i = 0; i < parts.size(); i++) {
sources.add(ComposeSource.builder()
.bucket(bucket)
.object(parts.get(i))
.build());
}
minioClient.composeObject(
ComposeObjectArgs.builder()
.bucket(bucket)
.object(targetName)
.sources(sources)
.build()
);
}
|
8.5 Nginx 上传调优
1
2
3
| # 加大请求体(默认 1m 太小)
client_max_body_size 1024m;
keepalive_timeout 75s;
|
8.6 Spring Boot 上传调优
1
2
3
4
5
| spring:
servlet:
multipart:
max-file-size: 1024MB
max-request-size: 1024MB
|
九、监控与运维
9.1 Prometheus metrics
MinIO 默认暴露 /minio/v2/metrics/cluster 端点:
1
2
3
4
5
6
| # prometheus.yml
scrape_configs:
- job_name: 'minio'
metrics_path: '/minio/v2/metrics/cluster'
static_configs:
- targets: ['minio.internal.example.com:9091']
|
9.2 关键指标
minio_cluster_disk_used_total:磁盘使用minio_cluster_disk_free_total:剩余空间minio_s3_requests_total:S3 请求数minio_s3_requests_errors_total:错误数
9.3 告警规则
1
2
3
4
5
6
7
8
9
10
| groups:
- name: minio
rules:
- alert: MinIODiskFull
expr: minio_cluster_disk_free_total / minio_cluster_disk_total < 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "MinIO 磁盘使用率超过 90%"
|
十、典型坑位
10.1 启动报 “no credentials”
修复:
1
2
| # 环境变量要用 MINIO_ROOT_USER / MINIO_ROOT_PASSWORD(v4 之后)
# 不是 MINIO_ACCESS_KEY / MINIO_SECRET_KEY
|
10.2 集群节点无法通信
症状:集群启动后某个节点状态 Offline。
修复:
- 4 个节点
/etc/hosts 都加完整映射 - 检查 9000 端口互通:
nc -zv node1-2 9000 - 防火墙放过 9000
10.3 上传文件 0 字节
症状:Java 上传后文件 0 字节。
修复:
1
2
3
4
5
6
7
8
9
10
11
| // 错误写法
minioClient.putObject(bucket, objectName, inputStream);
// 正确写法:必须传 size
minioClient.putObject(
PutObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.stream(inputStream, inputStream.available(), -1)
.build()
);
|
inputStream.available() 在某些流上返回 0,必须手动传 size。
十一、最佳实践清单
- MinIO 永远不暴露公网:内网 + Nginx 反代
- Bucket 全部 private:用预签名 URL 临时分享
- Nginx 路径重写:屏蔽桶路径直接访问
- 集群至少 4 节点:生产强烈推荐
- EC 编码:默认 EC:4,可容忍 4 块盘坏
- 版本控制必开:防止误删除
- 桶复制必配:跨机房 / 跨地域容灾
- Prometheus 监控:磁盘 / 请求 / 错误率
- 定期备份:用
mc mirror 备份到 OSS
2024+ 视角补充
本文写于 2022-09,2024-2026 期间 MinIO 关键演进:
- MinIO RELEASE.2024-08+:AI 推理优化(GPU 直通 / 大模型权重加载);Iceberg / Delta Lake 表格式支持(数据湖场景)
- MinIO 2024+ 性能:单节点 100+ GB/s(NVMe + 网络优化);EC 编码 8+2(vs 之前 4+2)—— 更大集群容错
- MinIO 2025+:Kubernetes Operator GA——
minio/operator 已是生产级(强烈推荐 K8s 部署) - MinIO 许可证变更(2024-01):AGPL 3.0——之前是 Apache 2.0。这对SaaS 厂商影响大(必须开源),企业内部自用无影响
- 竞品(2024+ 视角):
- Ceph Quincy / Reef(2024-2025):Ceph 18 Reef 性能优化(vs MinIO 优势在多协议——RBD / CephFS / RGW)
- Garage 1.0+(2024-2025):Rust 写的小型 S3 兼容存储——轻量替代
- SeaweedFS 3.x(2024):FUSE 挂载 / 极简部署
- Cloudflare R2 / Backblaze B2 / 阿里云 OSS(公有云 S3 兼容)——按量计费无需运维
实战建议(2025-2026 视角):
- 企业内部 S3 兼容存储 → MinIO 仍是首选(AGPL 不影响自用)
- K8s 部署 → MinIO Operator(K8s Operator 模式)2025+ 是生产级
- 数据湖 / 大数据 → MinIO + Iceberg / Delta Lake(2024-2025 主流)
- 公有云 → 直接用 R2 / OSS / S3,避免运维
- 轻量自托管 → Garage 1.0+(10MB 级别二进制)
下一步