为什么个人站也需要一个自己的对象存储
很多人一想到对象存储,第一反应就是"那是大厂才玩的东西",或者直接上云厂商的 S3 服务。但作为一个自己维护博客、图床、论坛、附件下载站的草根站长,你会发现几件很现实的事:第一,云对象存储的流量费是不讲道理的,一个热门帖子里的图片被人盗链,账单能让你心跳加速;第二,附件、图片、备份、程序包这些东西越攒越多,全塞在网站服务器的本地磁盘上,迟早把根分区撑爆,而且扩容非常痛苦;第三,一旦要迁移服务器,几百 GB 的静态文件搬起来要人命。
所以给自己搭一套对象存储,对个人站长来说不是炫技,而是一个非常实际的运维决策。说到自建对象存储,绝大多数教程都会推荐 MinIO——S3 兼容、部署简单、生态成熟。这没错,MinIO 确实是首选。但 MinIO 有个绕不过去的短板:它是为"大对象"设计的,元数据全部维护在单机内存和本地磁盘里,当你手里有几百万个几 KB 的小文件(比如用户头像、缩略图、碎碎的小附件)时,MinIO 的元数据压力会非常大,内存吃紧、启动变慢、列举目录卡顿。
这就要引出今天的主角:SeaweedFS。它最初是为了解决 Facebook 那个著名的"海量小文件"问题而设计的,架构上和 MinIO、Ceph 走了完全不同的路子,特别适合个人站这种"文件多、单个文件小、总容量不算大但数量惊人"的场景。
SeaweedFS 的核心架构:Master、Volume、Filer 三层
要理解 SeaweedFS,必须先把它的三个组件弄清楚,否则配置起来会一头雾水。
Master(主节点)负责整个集群的元数据管理,也就是"哪个 Volume 在哪个 Volume Server 上"。它维护的是 Volume 级别的映射,而不是每个文件的映射,这是它内存占用极低的关键。Master 可以多节点部署,通过 Raft 协议选举,个人站单节点就够用。
Volume Server(卷服务器)是真正存文件数据的地方。文件数据被切成一个个 Volume,每个 Volume 有一个固定大小(默认 30GB),里面存的是真实的文件内容和一份非常轻量的索引(.idx 文件,位于 Volume 的 .idx 后缀文件中)。因为索引是追加写的、定长的,所以寻址极快,一个 Volume Server 能轻松扛住海量小文件。
Filer(文件管理器)是可选的,但强烈建议开启。Master + Volume 这一层对外其实是"扁平"的,你需要自己记住每个文件的 fid(文件 ID)。Filer 在它们之上提供了一套目录树,让你可以像访问普通文件系统一样访问,同时支持挂载成 NFS/FUSE、支持 HTTP API、支持 S3 兼容接口。有了 Filer,"个人站当图床用"这件事才真正好用。
一句话总结这套架构的取舍:Master 只记"卷在哪",Volume 自己管卷内偏移,Filer 负责目录语义。这样 Master 永远不会成为瓶颈,几十亿个文件也不会把 Master 撑爆——这正是 MinIO 在超多小文件下最头疼而 SeaweedFS 最擅长的点。
用 Docker 部署 SeaweedFS 单机版
个人站不需要多机集群,单机跑三个组件完全够用。用 Docker Compose 最省心。先建目录:
mkdir -p /opt/seaweedfs/{master,volume,filer}
cd /opt/seaweedfs然后写一个 compose 文件,把三个角色用同一个镜像的三种启动方式跑起来:
version: "3.8"
services:
master:
image: chrislusf/seaweedfs:latest
command: "master -ip=master -port=9333 -mdir=/data"
volumes:
- ./master:/data
ports:
- "127.0.0.1:9333:9333"
volume:
image: chrislusf/seaweedfs:latest
command: "volume -ip=volume -port=8080 -mserver=master:9333 -dir=/data -max=0"
volumes:
- ./volume:/data
depends_on:
- master
filer:
image: chrislusf/seaweedfs:latest
command: "filer -ip=filer -port=8888 -master=master:9333 -defaultStoreDir=/data"
volumes:
- ./filer:/data
ports:
- "127.0.0.1:8888:8888"
depends_on:
- volume
这里有几个参数值得说明。-mdir 是 Master 存放卷映射元数据的目录;-max=0 表示 Volume Server 不限制卷的数量,自动增长(生产环境建议设个上限,比如 30,避免磁盘被写满);Filer 的 -defaultStoreDir 指定它自己维护目录结构、存储映射关系的本地目录。
注意我把 Master 和 Filer 的端口都绑到了 127.0.0.1,没有直接暴露到公网。这是一个必须养成的习惯:SeaweedFS 默认没有任何认证,只要 9333 或 8888 暴露在公网,任何人都能往你的存储里上传文件、列举、甚至删除。要对外访问,必须经过 Nginx 反代并加认证,这一点后面会讲。
启动:
docker compose up -d
docker compose ps验证 Master 是否正常,访问它的状态接口:
curl -s http://127.0.0.1:9333/cluster/status | head正常的话会返回类似 {"IsLeader":true,"Leader":"master:9333",...} 的 JSON。再看卷列表,应该能看到一个分配好的卷:
curl -s "http://127.0.0.1:9333/dir/status?pretty=y"用 Filer 的 HTTP API 上传和访问文件
有了 Filer,上传文件就变得像操作一个 HTTP 文件系统。往 /images/logo.png 上传一个文件:
curl -X POST -F "file=@./logo.png" \
"http://127.0.0.1:8888/images/logo.png"返回的 JSON 里会有一个 name 和 size。要读取它,直接 GET 同样的路径即可:
curl -O "http://127.0.0.1:8888/images/logo.png"Filer 还支持一些很实用的查询参数。比如 ?metadata=true 只返回元数据不返回文件体;?pretty=y 让你列举目录时看得舒服:
curl -s "http://127.0.0.1:8888/images/?pretty=y"删除文件用 DELETE:
curl -X DELETE "http://127.0.0.1:8888/images/logo.png"这里有个非常重要的坑要提前说:SeaweedFS 的文件删除默认是"逻辑删除"。你调 DELETE 之后,目录树里看不到了,但 Volume 里的数据块还在,磁盘空间不会立刻释放。真正回收空间要靠 Volume 的 compaction(压缩整理),Master 会在后台按 -volume.max 相关的阈值触发。如果你发现"我删了几十 GB 文件,磁盘怎么还是满的",别慌,这是正常行为,等后台整理,或者手动触发:
curl -s "http://127.0.0.1:9333/vol/vacuum"用 Nginx 反代并加认证,安全地对外提供图床
前面说过绝不能把 9333/8888 直接暴露公网。个人站当图床用的典型做法是:让 Nginx 监听一个域名(比如 img.example.com),把读请求反代到 Filer,同时把写请求收口。
只读场景最简单,反代到 Filer 的对应路径即可:
server {
listen 443 ssl http2;
server_name img.example.com;
root /var/www/empty; # 提供一张 404 兜底图
location ~ ^/(images|avatar|attach)/ {
proxy_pass http://127.0.0.1:8888;
proxy_set_header Host $host;
proxy_cache img_cache;
proxy_cache_valid 200 7d;
add_header Cache-Control "public, max-age=604800, immutable";
}
location ~* \.(php|py|sh)$ {
return 403;
}
}
proxy_cache_path /var/cache/nginx/img levels=1:2 keys_zone=img_cache:100m
max_size=10g inactive=30d use_temp_path=off;这样图片就有了 Nginx 层缓存,热门图不会每次都打到 SeaweedFS,命中率高的话磁盘 IO 能降一个数量级。
写接口(上传)必须加认证。最简单的办法是用 Nginx 的 basic auth 保护上传路径,或者只允许你自己的 IP / 内网访问:
location = /upload {
allow 127.0.0.1;
allow 10.0.0.0/8;
deny all;
proxy_pass http://127.0.0.1:8888;
}所有上传走服务器本地的脚本或后台程序,公网用户只能读不能写。这是个人图床最稳的安全模型——比在 Filer 前面自己造一套 token 校验要简单可靠得多。
把 SeaweedFS 挂载成本地目录(FUSE)
如果你习惯了在命令行里用 cp、rm 操作文件,可以把 Filer 挂载成 FUSE 文件系统。前提是宿主机装了 fuse:
apt-get install -y fuse3
mkdir -p /mnt/seaweed
# 用官方镜像里的 weed mount 命令
docker run -d --name weed-mount --privileged --cap-add SYS_ADMIN \
-v /mnt/seaweed:/mnt --network host \
chrislusf/seaweedfs:latest \
mount -filer=filer:8888 -dir=/mnt -filer.path=/挂载之后,/mnt/seaweed/images/ 下面就是你所有图片,你可以直接 ls、rsync 到别处。注意 FUSE 挂载的写入是同步到 Filer 的,适合做批量导入,但不适合作为网站目录直接读(性能不如原生 HTTP)。
备份与迁移:SeaweedFS 最省心的一点
SeaweedFS 在备份这件事上有天然优势。Volume 的数据文件是自描述的——每个 Volume 对应磁盘上一组 .dat 和 .idx 文件,你可以直接 rsync 整个 ./volume 目录到另一台机器,再用同样的配置起一个 Volume Server,Master 重新加载一下卷映射,数据就回来了。不需要像数据库那样"先停服务再一致性快照"。
日常备份我就用一条 rsync:
rsync -a --delete /opt/seaweedfs/volume/ \
backup@remote:/data/seaweed-volume/Filer 的目录映射(./filer 目录,默认是 LevelDB)也要一起备份,否则文件在,目录树没了,你还是得靠 fid 去捞。所以三个目录 master、volume、filer 一个都不能少。
五个我踩过的坑
第一,Master 单点。Master 挂了,整个集群就没法定位卷,读写全部失败。个人站虽然不追求高可用,但至少要做到"Master 挂了能快速拉起"——把 ./master 目录纳入备份,重启容器即可恢复。有条件的话可以起两个 Master 做 Raft,但三个才是能自动选主的最小奇数集群,两个反而更麻烦。
第二,Volume 写满不报警。Volume Server 磁盘写满时不会主动通知你,只是上传开始报错。一定要给磁盘加监控(哪怕是 Netdata 或一个 cron 脚本 df 检查),90% 就要告警。
第三,把 8888 端口暴露公网。这是最严重的安全事故,等同把你的存储裸奔在互联网上。务必只绑 127.0.0.1,对外只走 Nginx 反代。
第四,误以为 DELETE 立刻释放空间。前面讲过,逻辑删除 + 后台 compaction,磁盘不会马上降。规划容量时要把"待回收空间"算进去。
第五,小文件不合并,直接海量 POST。如果你有几十万个几百字节的小文件(比如日志碎片),建议先打包成大文件再上传,SeaweedFS 虽然擅长小文件,但"小文件合并"这个习惯能进一步降低 Volume 数量、提升列举效率。可用官方自带的 weed filer.copy 或自己写脚本按天打包。
到底该选 SeaweedFS 还是 MinIO
最后给一个不绕弯的决策建议。如果你的场景是:文件数量以百万计、单个文件很小(几 KB 到几百 KB)、要做图床/附件站/爬虫落地存储,那么 SeaweedFS 的 Filer 架构会让你舒服很多,元数据不会撑爆内存。如果你的场景是:文件以 GB 级视频、备份归档为主,需要完整的 S3 生态和成熟的权限体系,那么 MinIO 更合适,SeaweedFS 的 S3 接口虽然能用但相对小众。
很多个人站长其实是前者——图片、头像、缩略图、小附件才是大头,视频归档只是极少数。所以别再想当然地"有对象存储需求就上 MinIO"了,先算算你手里小文件的数量级,答案可能完全不一样。