侧边栏壁纸
  • 累计撰写 126 篇文章
  • 累计收到 2 条评论

服务器存文件越来越乱?我用 MinIO 搭了一套自建对象存储,踩了几个坑说清楚

2026-6-28 / 0 评论 / 8 阅读
🤖AI摘要
本文介绍作者使用MinIO自建对象存储的经历,分析了放弃本地磁盘存储的原因,并详细介绍了使用Docker部署MinIO的步骤和注意事项,包括端口配置、密码设置、数据目录权限等。同时,作者分享了使用mc命令行工具进行基本操作的经验。通过MinIO,作者解决了多台服务器共享文件、备份迁移、权限控制等问题,提高了数据管理的效率和安全性。

项目里要存的东西越来越多,用户头像、上传的文档、生成的报表,一开始全扔服务器本地磁盘,路径随便起,/data/uploads/2024/ 下面套了一层又一层。后来服务器迁移,光是把这些文件拷过去就折腾了一整天,还丢了一批。再后来多台机器要共享,NFS 挂上去动不动超时,折腾得够呛。

我后来换了个思路:自建一套对象存储。AWS S3 太贵,国内的 OSS 又不想绑死一个云厂商,看了看 MinIO,用了半年多了,把踩过的坑整理一下。

为啥不直接用本地磁盘了

单机的时候问题不大,mkdir 一搞,fwrite 写进去就完事。但到了这些场景就开始疼了:

多台应用服务器要访问同一批文件。NFS 能解决,但性能和可靠性都不好。我遇到过 NFS 挂载点卡死导致整个 df -h 命令 hang 住,连 SSH 进去都费劲。

备份和迁移。几千个小文件 cp -r 慢得要死,rsync 好点但还是得跑很久。而且你还不知道拷完有没有缺的,得再跑一次校验。

权限和临时链接。有时候要给第三方一个临时下载地址,本地文件得自己写签名逻辑,或者干脆开个 Nginx alias 暴露出去,安全也没法保证。

MinIO 对这些问题都有现成方案,而且兼容 S3 API,后面换云厂商的 OSS 也方便,不用担心绑死。

Docker 部署 MinIO 的几个要点

我直接用 Docker 跑的, docker-compose.yml 大概长这样:

version: "3.8"
services:
  minio:
    image: quay.io/minio/minio:RELEASE.2024-11-07T00-52-20Z
    container_name: minio
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: your_strong_password_here
    volumes:
      - ./data:/data
    command: server /data --console-address ":9001"
    restart: unless-stopped

有几个地方踩过坑。

端口 9000 和 9001 要分开

9000 是 API 端口(S3 兼容),9001 是管理控制台。一开始我只映射了 9000,心想一个端口够了,结果发现没有控制台页面,想建 bucket 还得用 mc 命令行。后来把 9001 也映射出来,浏览器打开就能操作,省了不少事。

其实如果你不想暴露控制台端口,只留 9000 也行,用 mc 命令行工具全搞定。但我建议前期调试的时候把控制台开着,图形界面看 bucket 和文件比命令行直观多了。

密码别太短

MinIO 对 root 密码有最低长度要求,好象是 8 位。我第一次设了个 6 位的,容器直接起不来,日志报错 ERROR Unable to validate credentials。翻了一下文档才知道长度不够。密码设强点没错,反正存环境变量里也不用每次手敲。

数据目录的权限

./data 这个目录,Docker 里面 minio 进程的 uid 是 1000。如果你的宿主机目录权限不对,容器起来会报 permission denied。我碰到过一次,直接 chown -R 1000:1000 ./data 解决。

chown -R 1000:1000 ./data

mc 命令行工具的基本操作

MinIO 的管理控制台虽然能用,但实际操作我还是更习惯 mc(MinIO Client)。装起来也简单:

# 下载 mc
curl -O https://dl.min.io/client/mc/release/linux-amd64/mc
chmod +x mc
mv mc /usr/local/bin/

# 配置连接
mc alias set myminio http://localhost:9000 admin your_strong_password_here

# 创建 bucket
mc mb myminio/uploads

# 查看 bucket 列表
mc ls myminio

# 上传测试文件
mc cp test.txt myminio/uploads/

# 设置 bucket 的访问策略(只读)
mc anonymous set download myminio/uploads

mc alias set 里面的 myminio 是你自己起的别名,后面所有操作都拿这个别名跑。一开始我手抖把端口写错了,连不上还以为 MinIO 没起来,浪费了半小时。

坑一:Python SDK 连接报错 SSL 和 endpoint 的问题

项目用 Python,改成 MinIO 以后第一件事就是装 SDK。pip install minio 就行,代码也不复杂:

from minio import Minio

client = Minio(
    "your-server:9000",
    access_key="admin",
    secret_key="your_strong_password_here",
    secure=False  # 没有 HTTPS 的时候写 False
)

# 上传文件
client.fput_object("uploads", "test.txt", "/local/path/test.txt")

# 下载文件
client.fget_object("uploads", "test.txt", "/local/path/downloaded.txt")

# 生成临时下载链接
url = client.presigned_get_object("uploads", "test.txt", expires=timedelta(hours=1))
print(url)

但连上去就报错了。Secure=False 这个参数很重要。我的 MinIO 跑在内网没有配 HTTPS,一开始没写这个参数,SDK 默认走 HTTPS,连不上直接报 SSL 错误。折腾了一会才意识到得关掉。

还有一个坑,endpoint 里面不要带协议前缀。写成 http://your-server:9000 会直接报错,正确写法是 your-server:9000。SDK 自己拼协议。这俩坑我踩了得有一个小时。

坑二:bucket 策略搞反了,公开了不该公开的东西

MinIO 的 bucket 默认是私有的,上传的文件外面访问不到。但有些场景你又需要公开访问,比如用户头像。我用 mc anonymous set download myminio/uploads 把整个 bucket 设成了公开可读。

结果问题来了:uploads 这个 bucket 里还有内部报表之类的文件,不应该公开。后来我学乖了,把 bucket 分开:avatars 放头像(公开),private-docs 放内部文档(私有),temp 放临时文件(7天生命周期自动删)。

用代码设置 bucket 策略(只对某个前缀公开):

from minio.commonconfig import ENABLED
from minio.lifecycleconfig import LifecycleConfig, Rule
from minio.lifecycleconfig import Expiration

# 设置 temp bucket 的生命周期:7天后自动删除
config = LifecycleConfig(
    [
        Rule(
            ENABLED,
            rule_id="delete-after-7days",
            expiration=Expiration(days=7),
        ),
    ],
)
client.set_bucket_lifecycle("temp", config)

经验就是:bucket 按访问权限分开,别把所有东西扔一个桶里。

坑三:大文件上传超时和分片断传

用户上传一个 200MB 的文件,接口超时了。MinIO 的大文件上传跟小文件不一样,得用分片上传。Python SDK 里面 fput_object 其实内部会自动分片,但有个坑:你的应用层超时设置如果比 MinIO 的短,中间就断了。

我的做法是把 Nginx 的代理超时调大:

location /minio/ {
    proxy_pass http://127.0.0.1:9000/;
    proxy_connect_timeout 300;
    proxy_read_timeout 300;
    proxy_send_timeout 300;
    client_max_body_size 500m;  # 允许上传最大 500MB
}

client_max_body_size 这个参数默认才 1MB,不调的话稍大点的文件直接 413。我一开始被这个坑了好久,看日志全是 413 Request Entity Too Large,还以为是 MinIO 的问题,结果是 Nginx 拦的。

另外如果用的是分片上传(超过 64MB 的文件 SDK 会自动走分片),中途断了没有完整文件,需要你自己做一下清理或者续传。mc 里面有个 mc ls --incomplete myminio/uploads 可以看未完成的上传,mc rm --incomplete myminio/uploads/xxx 可以清掉残留的分片。不及时清的话,这些分片文件是算在你的存储空间里的,看着 bucket 容量莫名其妙就满了。

坑四:presigned URL 不更新权限

临时链接这个功能挺好用的,给第三方一个链接,时间到了自动失效。但有个容易忽略的地方:生成 presigned URL 时,它取的是当时那个账号对 bucket 的权限。

我碰到一次:用的是 admin 账号生成了链接,后来我给 admin 的权限改了(把某个 bucket 改成只读了),但之前生成的 presigned URL 居然还能写。我后来查了才知道,presigned URL 在生成时就把签名写死了,跟你后来改不改权限没关系。

所以如果你的 access key 泄露了,光改 bucket 策略是不够的,得把那个 access key 直接删掉换新的。

坑五:多节点部署的纠删码坑

后来业务大了,单节点扛不住,上了多节点。MinIO 的分布式部署用纠删码来做冗余,也就是说你的数据会被分片冗余存储,挂一块盘还能恢复。

docker-compose 多节点配置大概长这样:

version: "3.8"
services:
  minio:
    image: quay.io/minio/minio:RELEASE.2024-11-07T00-52-20Z
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: admin
      MINIO_ROOT_PASSWORD: your_strong_password_here
    volumes:
      - /mnt/data1:/data1
      - /mnt/data2:/data2
      - /mnt/data3:/data3
      - /mnt/data4:/data4
    command: server /data{1...4} --console-address ":9001"
    restart: unless-stopped

纠删码的规则是:N 块盘,最多允许挂 N/2-1 块。4 块盘最多挂 1 块还能读写,挂 2 块就只能读不能写了。

我之前以为 4 块盘能挂 2 块还正常读写,挂了 2 块以后上传全报错,仔细看文档才明白。所以节点数和允许挂盘数的关系得提前算好,别到出事了再翻文档。

另外一个容易搞错的地方:多节点的 MinIO 启动命令里的路径格式是 /data{1...4},这个花括号展开是 MinIO 自己的语法,不是 shell 的。你如果写 /data1 /data2 /data3 /data4 也行,但 1...4 这种写法更省事。只是要注意花括号前面不要有空格,{1...4} 不是 {1 ... 4},多一个空格都不行。

监控和告警

MinIO 自带了一个 Prometheus metrics 接口,http://your-server:9000/minio/v2/metrics/cluster,直接接到你的 Prometheus 里就行。我之前那套监控体系里加了几个关键指标:

# Prometheus scrape 配置
scrape_configs:
  - job_name: 'minio'
    metrics_path: /minio/v2/metrics/cluster
    static_configs:
      - targets: ['your-server:9000']

重点关注的指标:

  • minio_cluster_disk_free_bytes:磁盘剩余空间
  • minio_s3_requests_total:请求总量
  • minio_s3_errors_total:错误总量

我在 Grafana 里配了一个简单的面板,磁盘剩余低于 20% 就发钉钉告警。这一步不算难,就是 Prometheus 的 scrape 路径别写错了,/minio/v2/metrics/cluster/minio/v2/metrics/node 是两个不同的端点,cluster 看整体,node 看单节点,搞混了数据不全。

换云厂商怎么办

这是当初选 MinIO 的原因,S3 API 兼容。你代码里用的是标准 S3 SDK,endpoint 指向 MinIO 的地址。后面如果想换到阿里云 OSS 或者 AWS S3,只需要改 endpoint 和 access key,代码逻辑一行都不用动。

Python 这边,boto3 库直接接:

import boto3

s3 = boto3.client(
    "s3",
    endpoint_url="http://your-minio-server:9000",
    aws_access_key_id="admin",
    aws_secret_access_key="your_strong_password_here",
)

# 上传
s3.upload_file("local.txt", "uploads", "local.txt")

# 下载
s3.download_file("uploads", "local.txt", "downloaded.txt")

boto3 跟 minio SDK 的区别就是 endpoint_url 的写法,其他 API 调用一模一样。

现在的配置总结

跑了大半年了,现在比较稳了:

  • 单节点 4 块盘跑纠删码
  • bucket 按权限分开:avatars(公开)、private-docs(私有)、temp(7天生命周期)
  • Nginx 反代,超时调到 5 分钟,body 限 500MB
  • presigned URL 最多 24 小时
  • Prometheus + Grafana 监控磁盘和请求
  • 定期跑 mc ls --incomplete 清理残留分片

搭建到稳定运行大概花了两天,但从本地磁盘迁移到 MinIO 之后,后面不管扩容、备份还是共享访问,确实省了很多事。早知道早点搞了。

评论一下?

OωO
取消