Featured image of post MinIO 对象存储实战:单节点 / 集群 / 桶复制 / Spring Boot 集成

MinIO 对象存储实战:单节点 / 集群 / 桶复制 / Spring Boot 集成

部署 MinIO 单节点和 4 节点集群,配置 mc 客户端做桶复制和双向同步,修复 minio 桶路径遍历漏洞,Spring Boot 集成分片上传 / 断点续传 / 秒传

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
  • 部署简单:单二进制 + 一个目录就能跑

对比

维度MinIOCephSeaweedFS
部署难度极简复杂中等
性能极高
集群最少节点1(4 节点为生产推荐)33
S3 兼容100%
适用规模TB ~ 100PBPB+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_URLS3 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-110.0.1.1机房 1
node1-210.0.1.2机房 1
node2-110.0.2.1机房 2
node2-210.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 Policypublic 时,任何人通过桶路径就能列出所有文件

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(最佳实践)

  1. MinIO 不暴露公网:只在内网
  2. Nginx 反代 + 路径重写:唯一对外的入口
  3. Bucket 全部 private:永远不开 public
  4. 预签名 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 级别二进制)

下一步

使用 Hugo 构建
主题 StackJimmy 设计