2866 字
约 9 分钟
14
S06-X15 · 自建文件服务:MinIO、ZFile 与图床链路

把图片和附件从"服务器上的 uploads/ 目录"搬到正经文件服务,通常不是容量问题,而是几件事同时发生:磁盘写满要扩容、换机器要迁移、内部文档直链被转进群、CDN 账单突然涨三倍。这些问题在目录结构里没有答案,得先把文件服务当成有接口的系统来设计。


1. 自建文件服务要回答的五个问题

问题 要定的具体项 定错的后果
存哪 本地盘、对象存储、NAS 单盘无冗余,坏盘丢一批
怎么访问 直链、预签名、应用代理 直链公开后被爬、被刷流量
谁有权 桶策略、签名有效期、应用鉴权 私有文件被匿名读到
怎么算成本 存储、出网流量、请求次数 只算存储,流量超预算
怎么备份 版本化、异地镜像、回收站 误删后没有回滚点

后四个问题都依赖第一个问题的答案,所以选型必须先定。


2. 对象存储选型

方案 优势 代价 适用
MinIO 自建 S3 兼容、无出网费、可控 自己运维、自己做异地备份 内网、量大、数据不出域
云厂商 OSS/S3 免运维、CDN 生态完整 出网单价高、有请求费 面向公网的中小站
本地磁盘 + nginx 最简单、零依赖 无冗余、扩容停机、无签名 单机、小体量

常见组合:热数据放对象存储,图片走 CDN,冷数据转低频层。比全部放本地盘省事,也比全部放云上省钱。


3. MinIO 的部署要点

# 单机单盘:最简单,但没有纠删码
docker run -d --name minio \
  -p 9000:9000 -p 9001:9001 \
  -e MINIO_ROOT_USER=admin -e MINIO_ROOT_PASSWORD='CHANGE_ME' \
  -v /srv/minio/data:/data \
  quay.io/minio/minio server /data --console-address ":9001"

# 单机多盘:启用纠删码(4 块盘按半数校验,可坏 2 块)
minio server /data{1...4}
模式 冗余 最小规模 扩容 说明
单机单盘 无 1 盘 换大盘 测试或可重算数据
单机多盘 纠删码 4 盘 加盘需重建 单机故障仍全丢
分布式 纠删码跨节点 4 节点起 加节点集 生产常用,按集扩容

纠删码的关键是校验块比例:MinIO 的 EC:N 指每组校验块数量,EC:4 就是 4 个校验块配 4 个数据块,可用容量约一半。bucket replication 用于跨站点,替代不了纠删码。

$ mc alias set local http://127.0.0.1:9000 admin 'CHANGE_ME'
$ mc mb local/media
$ mc admin info local                 # 容量、节点、盘状态
$ mc ls --recursive local/media | head
$ mc du local/media                   # 桶占用
$ mc admin user add local appuser 'APP_SECRET'
$ mc admin policy attach local readwrite --user appuser

4. 桶与权限模型

模式 访问方式 风险 适用
私有桶 + 预签名 后端签发临时 URL 有效期太长等于公开 私有附件
公共读桶 直接 GET 被爬、被刷流量、外泄 公开图片
公共读写 匿名可传 被当免费图床 不该存在
前缀隔离 private/{userId}/... 前缀不是权限边界 多租户分目录
# 公共读(只给需要的桶开,且最好挂在 CDN 后面)
$ mc anonymous set download local/public-img

# 生成预签名下载链接(有效期按需,不要给 7 天)
$ mc share download --expire 1h local/media/2025/11/report.pdf

# 生命周期:把 90 天前的对象转低频层
$ mc ilm add --expiry-days 365 local/media

S3 SigV4 预签名 最长 7 天(604800 秒),这是协议上限不是建议值。"一次下载"的链接按分钟级给。


5. 上传链路:直传还是中转

方案 链路 优势 代价
服务端中转 浏览器 → 应用 → 存储 鉴权、校验、改名都在服务端 应用带宽与内存成瓶颈
预签名直传 浏览器 → 存储 应用只签发 URL,不碰字节 校验靠回调或 HEAD 复核
分片续传 按 part 上传后合并 大文件可续传 要维护 uploadId 与清单
# 预签名直传:应用只发一个短时 PUT URL
url = s3.generate_presigned_url(
    "put_object",
    Params={"Bucket": "media", "Key": key, "ContentType": "image/webp"},
    ExpiresIn=300,
)

分片上传的两个硬约束:除最后一片外每片不小于 5 MiB,总片数不超过 10000。100 GB 单文件不能靠"1 MB 一片"解决。

秒传靠内容哈希:客户端算 sha256,带哈希问服务端"有没有",命中则建引用。两点注意:哈希要整文件算;命中结果要区分"同内容已存在"和"同 key 已存在"。


6. 图片处理接入

# URL 参数式处理(七牛 Mogr2 风格,参数名按各家文档核对)
https://img.example.com/a.jpg?imageMogr2/thumbnail/!300x300r/quality/80/format/webp
https://img.example.com/a.jpg?imageMogr2/thumbnail/!50p/blur/1x0
// 自建链路:sharp 生成固定规格,结果落盘或回写对象存储
const sharp = require('sharp');
await sharp(src).rotate()
  .resize(300, 300, { fit: 'cover', position: 'centre' })
  .webp({ quality: 80 })
  .toFile(dst);
做法 时机 优势 代价
URL 参数(Mogr2 类) 请求时 规格随便改,不用预生成 首请求要回源,缓存必须打到位
预生成规格 上传时 首屏快、可长缓存 规格多时存储翻倍
自建(sharp/ImageMagick) 上传或请求时 可控、可加水印 要防 SSRF 与解码炸弹

ImageMagick 同等操作:convert in.jpg -resize '300x300^' -gravity center -extent 300x300 -quality 80 out.webp。哪条路都要限制输入尺寸与解码像素数,否则一张超大图能打满 worker。


7. ZFile 这类"挂载层"的定位

维度 ZFile 的定位 不是什么
角色 把多种存储挂成统一目录,提供浏览、搜索、直链 不做上传治理、去重、生命周期
存储后端 本地、S3 兼容、OneDrive、Google Drive、WebDAV 不适合高并发上传后端
鉴权 站点密码、目录权限、直链开关 不是多租户 IAM
适用 把已有云盘内容变成可浏览站点 需要回源鉴权的图床

它解决的是展示层:把对象存储或网盘的目录树映射成网页目录。真正的生命周期仍在存储侧。


8. CDN 与回源

# 源站给 CDN 的缓存头:公开图片长缓存,私有资源不缓存
location /media/ {
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header X-Content-Type-Options nosniff;
}
location /private/ {
    add_header Cache-Control "private, no-store";
    # 或改为校验签名后再 proxy_pass 到对象存储
}
策略 设置 原因
公开图片 长缓存加内容哈希文件名 内容变则 URL 变,缓存不失效
私有附件 private, no-store 或签名校验 边缘缓存会把私有文件变公开
签名 URL 缓存 TTL 小于签名有效期 过期后边缘仍有副本
回源鉴权 源站校验 Referer 或回源头 防绕过 CDN 直打源站

签名 URL 与 CDN 的冲突最易被忽略:签名保护源站,不保护已进边缘缓存的副本。


9. 成本与容量

成本项 典型量级对比 优化手段
存储 相对便宜,按月每 GB 转低频、删冗余规格
出网流量 通常比存储单价高一个数量级以上 CDN 命中、压缩、限制直链
请求次数 小文件多时不可忽略 合并小文件、长缓存
跨区复制 按流量计 只对关键桶开

判断口径:流量比容量痛。所以优先做的是"减少回源与出网",而不是"压缩存储"。同一批图片,统一转 WebP 并按需生成缩略图,通常能同时降低存储与流量。


10. 备份与恢复

手段 防的是什么 不防什么
纠删码/副本 单盘、单节点故障 误删、勒索、逻辑损坏
版本化 覆盖写与误删 桶被整体删除
异地镜像 机房级故障 源端已污染的数据
回收站 手滑 恶意删除
# 版本化 + 定期镜像到另一个站点,才是"备份"
$ mc version enable local/media
$ mc mirror --preserve --watch local/media remote-backup/media
$ mc mirror --newer-than 7d local/media /backup/snap-$(date +%F)/media

对象存储不能只靠副本:副本跟着源一起被删。mc mirror --watch 是持续同步,误删也会同步过去——要么用默认的不删目标端,要么加版本化与不可变窗口。


11. 排错:七种现场

现象 原因 处理
能上传、访问 403 桶私有却用直链 改预签名或对前缀开匿名读
大文件到 99% 失败 分片小于 5 MiB 或片数超 10000 调大分片
部分文件名 404 key 含空格、#、?、中文 key 用哈希,原名放 metadata
换 CDN 后老图 404 缓存 key 与源站路径不一致 统一路径规则,做一次全量预热
预签名发出去就失效 时钟偏差或有效期过短 校时(NTP),按场景定有效期
磁盘满但对象数没涨 分片残留与 .minio.sys mc admin heal、清未完成上传
扩容后旧数据读不到 按集扩容却没 rebalance 走官方扩容流程,别混盘

12. 边界

场景 更合适 理由
只放几张图 本地盘 + nginx 对象存储运维成本高于收益
多端强一致共享 NAS/SMB 对象存储是 HTTP 语义
版本控制与协同编辑 Git LFS 或文档系统 对象存储不懂文件语义
TB 级、偶尔读 归档层 + 解冻 标准层成本过高
极高频小文件 本地缓存 + 对象存储做后端 请求费很贵

13. 自测

  1. 单机四盘、半数校验的 MinIO,能容忍几块盘同时损坏,可用容量大约多少?
  2. 为什么"私有桶 + 直链"必然 403,而"公共读桶 + CDN"又可能被刷流量?
  3. SigV4 预签名 URL 的有效期上限是多少,为什么不该给到上限?
  4. 分片上传对单片大小与总片数分别有什么硬约束?
  5. 签名 URL 已经设了 1 小时有效期,为什么边缘缓存仍可能泄漏文件?
  6. mc mirror --watch 为什么不算备份,缺的是哪一环?
S06-X15 · 自建文件服务:MinIO、ZFile 与图床链路
https://www.clxhxhhr.top/posts/6007/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。