RustFS
rustfs 是 Rust 重新实现的 S3 兼容对象存
储:同样的 S3 API、Web 控制台、纠删码多盘/多节点布局——但运行时模型与
MinIO/silo 完全不同。pgcli 将其作为独立的顶层插件运行,CLI 表面与
minio、silo 一致(install、TLS、BYO 证书、
drives、logs、autostart),共用同一个端口池;但有三点决定了本页的写法:
- 固定的容器用户。 上游 rustfs 在镜像里写死
User=rustfs(uid/gid 10001)。pgcli 把这件事完全放在自家镜像内部处理——见下节 权限与属主——所以 rustfs 存储不需要任何主机侧属主处理。 - 只有三种拓扑,没有多节点单盘。 rustfs 只支持 SNSD / SNMD / MNMD—— 见部署模式。
- 自己的 TLS 文件名。 rustfs 从
RUSTFS_TLS_PATH读取rustfs_cert.pem/rustfs_key.pem,而不是 MinIO 的public.crt/private.key。
想要一个精简、Rust 原生的 S3 端点就选 rustfs;它可与 minio、silo 在同一台 主机并存(三者共用一个端口池)。
平台支持: rustfs 插件实际上是仅 Linux。其运行时模型(固定容器 uid、独立块设备上的盘、host 网络)是 Linux 容器路径,也只在 Linux 上做过验 证;macOS 的 bridge 路径代码完整、wrapper 镜像也是双架构,但尚未在真机上跑 过。
工作原理
一个实例一个容器。数据存放在 bind 挂载的主机目录里(默认
<base-dir>/addon/rustfs/<name>/data,或 --drive 的每个盘各自一个目录),
生命周期长于容器。与 minio/silo 不同,pgcli 不直接驱动 rustfs 二进制
——它运行镜像自带的 /entrypoint.sh(经下面说的 wrapper 转发),因为正是这
个 entrypoint 会展开 RUSTFS_VOLUMES 里的多盘花括号范围、创建各盘目录、并
组装服务端 argv。
pgcli 拉取的镜像不是纯上游镜像——是
ghcr.io/mars-base/pgcli/pgcli-rustfs:1.0.0,一个建立在
docker.io/rustfs/rustfs:1.0.0(首个 GA 版本)之上的精简 pgcli wrapper。
pg addon install rustfs 只负责 pull;pgcli 从不在运行时构建它。tag 跟随被
pin 的上游版本。
凭据处理方式与 minio/silo 相同:
root_user默认admin(rustfs 对 access key 无最小长度要求,任意非空值 都可以;admin还顺带避开了对rustfsadmin的告警);root_password首次 install 时生成(或用--root-password显式提 供),存于pg.yaml(addons.rustfs.<name>.root_password),并在 install 摘要里只打印一次。
它们是 rustfs 的 root access key / secret key,通过 RUSTFS_ACCESS_KEY /
RUSTFS_SECRET_KEY 环境变量传入——即每个 S3 客户端(含 pg mc alias set)
口中的 access key 与 secret key。
权限与属主
这是 rustfs 与 minio/silo 真正不同的地方,而 pgcli 已把它收进镜像、在实际 使用中变得不可见。
上游 rustfs 以一个**固定的、不可配置的 uid/gid(10001)**运行。在 rootless
podman 下,主机用户对这个 uid 没有任何权限主张,所以让一个 bind 挂载的数据
目录对该进程可写的「朴素做法」是在主机侧做 chown 腾挪
(podman unshare chown 10001:10001 …)——脆弱,而且和操作者是否为 root 与
其说相关。pgcli 完全绕开了这一切:
- wrapper 镜像的 entrypoint 以容器内 root 启动,把它自己的 bind 挂载数据
目录
chown成 10001,然后su降到rustfs用户,再exec未做任何 改动的上游/entrypoint.sh。pgcli 不传--userflag,也完全不做主机 侧的属主操作。 - 两种 daemon 模式下的效果一致;只有主机可见的数字 uid 不同,因为那是
podman 用户命名空间的属性,与 pgcli 无关:
- rootful podman → 目录落在真实的主机 uid
10001; - rootless podman → 落在附属(subordinate)主机 uid
(
subuid_start + 10001,例如110001),10001只在容器内可见。
- rootful podman → 目录落在真实的主机 uid
两种情形都不需要操作者以 root 运行、也不需要手工 chown。唯一还值得知道的
约定是:当你自己挂载盘(--drive)时,挂载点应归运行 pg 的那个用户所有
——rootless podman 下 root 拥有的挂载点落在命名空间 uid 范围之外,容器内的
chown 无法认领它,跟任何 bind mount 一样。
TLS 是拷贝,不重新属主。 rustfs 必须以 10001 身份读它的 key+cert,但
pgcli 生成的证书目录(0700、pgcli 拥有)必须继续由 pgcli 拥有,pg cert、
CA 刷新、pg backup fetch-ca 都要在它上面工作。所以 wrapper 不 chown
那个目录:它以只读方式挂载该目录,然后以容器内 root 身份把那两个必需文件
拷贝到一个新建的、由 10001 拥有的容器本地目录——RUSTFS_TLS_PATH 指向
那里。任何主机文件都不会被改动;BYO 路径走同一套拷贝机制,BYO 的 key/cert
同样只读挂载、按所需文件名被拷贝、也从不被 chown。
安装
输出会报告端点和 root 凭据:
在 Console: URL 用打印出的 root 用户/密码登录控制台。把 S3 客户端(含
pgBackRest)指向 S3 API: URL,或用 pg mc 直
接在终端操作。
对一个在跑的实例重跑 install 是 no-op:容器不会被重建(停着的会被启动并
有提示),flag 会合并进已存配置,已有的 root 密码保留。传 --force 才重建
容器,从而让改动的端口、监听地址、端点列表或凭据生效——数据目录不受影响。
绑定地址: 默认
127.0.0.1让存储只在本机可达。--listen 0.0.0.0(或pg.yaml里的listen键)会把它暴露到网络上。能访问该端口的任何人 都可尝试 root 凭据,所以要么放在防火墙后、要么开 TLS(--tls)。
TLS(--tls)
--tls 让 rustfs 提供 HTTPS。pgcli 用标准库生成一个自签 CA 与一张叶子证书
——SAN 覆盖 loopback 名称、localhost 以及主机的每张网卡 IP——写入
<base_dir>/tls/rustfs/<name>/:rustfs_cert.pem / rustfs_key.pem(rustfs
要求的名字,同时另存一份 public.crt / private.key 便于 CA 工具复用)与用
于分发的 ca.crt。证书目录以只读挂载、并如权限与属主所述被
拷进容器;端点 URL 变成 https://。
为什么需要它:pgBackRest 对 S3 仓库强制 HTTPS,所以一个要接收 Patroni
archive-push 的 rustfs 必须支持 TLS。backup.repo.s3.ca_file 指向
ca.crt,pgBackRest 就会带完整证书校验连接——见备份 → S3 对象存储仓
库。该页关于 MinIO 仓库的一切同样适用于
rustfs:S3 契约完全一致(path-style、repo1-s3-uri-style=path)。
把 CA 送到远端主机——无需 scp。 服务端证书以 leaf+CA 链的形式被提供, 所以另一台机器上的消费方可以直接从一次 TLS 握手里抽出根证书:
fetch 是 trust-on-first-use——信任前先把打印出的 SHA-256 与存储主机上的
sha256sum ~/.pgcli/tls/rustfs/<name>/ca.crt 比对一下。
自带证书(--tls-cert / --tls-key)
--tls 只会服务 pgcli 自家的自签证书对。若要服务你已经持有的证书,传
--tls-cert <leaf(+chain).pem> --tls-key <key.pem>。配合 --tls(隐含)这
会取代生成的证书:两个文件以只读挂载,并按 rustfs 要求的名字被拷进容器
(/opt/rustfs/certs/rustfs_cert.pem、…/rustfs_key.pem);pgcli 既不重新
属主、也不写它们的源目录。
还没有证书?pg cert 能按你给定的 SAN 铸一张自签 leaf,rustfs 原样 serve
它(真机验证过:rustfs 实际呈现的证书与 pg cert 输出在实时握手上吻合,
pg mc 对它完成完整的 put/get 往返)。签发的参数、客户端信任锚、续期,见
自签证书。
BYO 模式的其余一切——为什么续期需要 --force(单文件挂载会钉住宿主源
inode)、客户端如何挑选信任锚、pg cert 作为测试证书铸造——与 MinIO 插件完
全一致,见 MinIO → 自带证书。关掉 BYO: 从 pg.yaml 里删
掉这个插件下的 cert_file/key_file,再 pg addon install rustfs --name store --force 重建。
部署模式
rustfs 有三种布局——没有多节点单盘模式:
| 模式 | 形态 | 适用场景 |
|---|---|---|
| SNSD(单节点单盘) | 一个节点、一个数据目录——不给 --endpoint/--drive 时的默认 |
dev、test、demo |
| SNMD(单节点多盘) | 一个节点、多块盘——每块一个 --drive,见下文 SNMD |
单主机上抵抗磁盘故障 |
| MNMD(多节点多盘) | 多节点、每节点多盘——本节点的 --drive + --endpoint 列表 |
同时抵抗磁盘故障与节点故障 |
rustfs 有意没有 MNSD(多节点单盘)。只给 --endpoint 不给 --drive 会在
install 期被拒:rustfs 自己派生每盘 volume 范围,所以一个分布式节点必须声明
自己有几块盘。
rustfs 严格要求物理盘互斥。 在 SNMD/MNMD 下,每个
--drive必须位于自 己的块设备上。若两块盘共享一个设备(同一st_dev),rustfs 进程启动即[FATAL]退出。pgcli 以分离方式启动容器,所以pg addon install仍返回 0,故障表现为一个永远变不健康的容器——若多盘 install 起不来,查pg logs addon rustfs --name store。(这比 MinIO/silo 更严,后者只告警。)
单节点多盘(SNMD)
一个 rustfs 进程、若干主机目录、在它们之间纠删码。用 --drive 一次一块盘替
代 --data-dir:
每个 --drive 是位于独立设备上的一个主机目录;第 N 块盘(0-indexed)被
bind 挂到容器路径 /data/rustfsN,RUSTFS_VOLUMES 被设为花括号范围
/data/rustfs{0...3},由镜像的 /entrypoint.sh 展开成各个独立目录。
--drive 与 --data-dir 互斥;把 --drive 和 --endpoint 组合就是 MNMD。
pg addon remove rustfs --name store --clean-data 会删除每个 drive 目录
——但会拒删仍然处于挂载状态的盘,这样一次手滑的 --clean-data 绝不会穿过
活挂载点 rm -rf 到下面的真实磁盘。若确实要清数据,先卸载。
分布式 / 集群模式(MNMD)
每个节点跑各自的 pgcli 与各自的 pg.yaml;每份 pg.yaml 带同一份完整
的 endpoint 列表与同一套 root 凭据。对 rustfs 而言,--endpoint 列表是
每节点一条 scheme://host:port(不带 path——rustfs 从 --drive 自己派生
/data/rustfsN volume 范围),每节点再各传各的盘:
除了一份份逐字节相同的配置,还有两点关键:--listen 必须是 0.0.0.0(或本节点
自己的可达地址)而不是默认的 loopback——podman host 网络下端点广播的是各节点真实
IP,只监听 127.0.0.1 的节点永远进不了集群。而且所有节点的 endpoint 列表与 root
凭据必须完全一致:rustfs 据此派生出同一个纠删码集合,一旦对不上就退化成彼此独立
的单实例。
--endpoint 传的是不带 path 的 scheme://host:port,每节点重复一条(写成逗号串
--endpoint a,b,c 也等效——它是 string-slice flag);pgcli 会给每条拼上本节点的
/data/rustfs{0...N-1} 盘范围,再用空格把四条 URL 连成单个 RUSTFS_VOLUMES。
(RUSTFS_VOLUMES 本身必须是空格连接、不能是逗号连接:rustfs 二进制会把逗号串错误
切分,把 http:// 塌缩成 http:/,节点因此解析不出自己的盘——列表中第一个节点以
VolumeNotFound 退出,其余节点卡在 waiting for storage_quorum,始终凑不出可写集
群。空格分隔的字面 URL 与官方文档的紧凑花括号形式
http://node{1...4}:9000/data/rustfs{0...3} 解析结果完全一致,却能直接用各节点的
普通 IP,不需要 /etc/hosts 或 DNS。)
跨主机 MNMD 已在真机 4 节点 × 4 盘集群上验证(Linux,rootful podman):四台全部
/health 返回 200,且写入一次的对象从每一台读回都逐字节一致——纠删码分片确实
散布在全部 16 块盘上。
使用 mc 客户端
pg mc 在一次性容器里跑 MinIO 的 mc 客户端,对 rustfs 的 S3 API 与其它
存储一样可用:
S3 数据面(PUT/GET)经 pgBackRest 与一次基础 mc 往返对 rustfs 已端到端证
实,其中包括一段完整的 PITR 流程——全量 base backup、WAL 的 archive-push、
以及一次 --time 恢复:从 rustfs 取回归档 WAL 回放并精确停在目标时间
(Linux,rootless podman 实测)。如实标注:管理面 的 mc 命令(alias set
校验、bucket policy、admin 系列)尚未对 rustfs 逐一验证。把它当作纯对象
put/get 用,理解某些 MinIO 专属的管理命令未必在 rustfs 上有对应。
端口
每个实例从 minio_start_port(默认 9000)起的同一个池里取两个连续端
口:先 S3 API、后 console。这个池在 minio、silo、rustfs 之间共享——三
者由同一个游标分配,故在同一主机并存不冲突(先按名字排 minio,再 silo,再
rustfs):
配置
实例位于 pg.yaml 顶层的 addons.rustfs 映射里:
改动 listen、端口、root_user、root_password、image_tag、data_dir、
tls/cert_file/key_file 或 endpoints,需在下一次
pg addon install rustfs --name store --force 后生效。
列表
pg addon list 从不打印密码——去 pg.yaml 读。健康端点是 GET /health
(返回 200),不同于 MinIO 的 /minio/health/live。
启停
install 会跳过仍在的容器(停着的则启动);start 只启动已存在的容器(并
在状态异常时从配置重建以自愈)。在 TLS 生成模式下 start 还会重新校验并按
需重签叶子证书。
开机自启
容器带 --restart unless-stopped 策略(崩溃重启,而非开机自启)。要在主机
重启后拉起实例:
rustfs 的固定容器 uid 在 pgcli 的 wrapper 镜像内部处理,所以开机时的启动除 了运行 podman 本身不需要任何特殊主机权限。开机是只启动;rustfs 独立于 PostgreSQL 栈,故最后启动。
移除
数据目录就是对象存储——丢了它就丢了里面所有 bucket——所以 remove 默
认保留它。--clean-data 才删除(并清理默认布局下已空的父目录)。在
rootless podman 下,对象归一个附属 uid 所有、普通 rm 无法 unlink,因此
--clean-data 会透明地回退到 podman unshare rm 来回收它们。
日志
故障排除
- install 时
pulling rustfs image ... : ...。 wrapper tagghcr.io/mars-base/pgcli/pgcli-rustfs:1.0.0不可达——检查到 ghcr.io 的网络 访问,或用podman pull预拉。 - 多盘 install 起不来(永不健康)。 两块盘共享一个物理设备时 rustfs
[FATAL]。查pg logs addon rustfs --name store看磁盘检查是否触发,并确 保每个--drive在独立设备上(独立磁盘或各自的 loop 挂载)。 - rootless podman 写不进某个盘。 该盘的挂载点归 root 所有、落在命名空间
uid 范围之外,容器无法认领。把挂载点
chown给运行pg的用户。 - 手工
--api-port撞端口。 池在 minio/silo/rustfs 之间共享,一个显式端 口必须被三者的自动分配器都视为已占用。每个存储需要一对连续端口。 --clean-data落了个目录没删。 rootless podman 下的附属 uid 目录树经podman unshare rm回退删除;若该路径不可用,自己动手podman unshare rm -rf <dir>。