MinIO 自建对象存储:纠删码原理、部署与项目现状
📝 笔记摘要:MinIO 自建对象存储的原理、部署与项目现状
MinIO 是 S3 兼容的对象存储,用来存图片、视频、日志这类非结构化数据。它最值得讲的不是怎么装起来,而是两件事:冗余是怎么用纠删码算出来的,以及这个上游项目现在还剩什么、手里存着的老镜像 tar 包还能怎么用。
第二件事必须写在前面。MinIO 社区版在 2025 年被砍掉 Web 管理控制台,2025 年 10 月停止分发 Docker 镜像,2026 年 4 月仓库归档,2026 年 9 月 Docker Hub 上的 minio/minio 和 minio/mc 仓库被整个删除,匿名拉取只剩 404。现在照着老教程敲 docker pull minio/minio 是拉不下来的。
这篇的结构
1. 对象存储和文件系统的差别在哪
2. 纠删码:冗余是算出来的,不是复制出来的
3. 现在的镜像该用哪个
4. 手里有老镜像 tar 包
5. 单机多盘部署
6. 分布式部署
7. 控制台被砍掉了什么
8. Java SDK:9.0.x 的 API
9. 往哪迁
1. 对象存储和文件系统的差别在哪
对象存储只有两层结构:桶(Bucket)和对象(Object)。对象没有真正的目录,a/b/c.jpg 这样的 key 只是名字里带了斜杠,桶本身不参与寻址。
文件系统的强项 随机读写、改一个字节、列目录、文件锁
对象存储的强项 海量小文件、大文件分片上传、断点续传、跨机复制、HTTP 直达
差别带来的后果 对象一旦写完基本不改,要改就整个重写一遍
协议层面 MinIO 走的是 S3:建桶、列桶、PUT、GET、DELETE、预签名 URL。应用侧用任何 S3 兼容的 SDK 都能连,这一点在后面的选型里很关键。
把文件先存在应用服务器本地磁盘那一档怎么落地,见 Spring Boot 文件上传。那一档的问题是多副本不成立、容量跟着单机走、坏一块盘文件就没了,换到对象存储就是为了消掉这几条。
2. 纠删码:冗余是算出来的,不是复制出来的
「EC 码」这个说法不准确,完整叫法是 Erasure Coding,中文叫纠删码。区别在于三副本是复制三份,纠删码是存一份、算出几份校验片。

图里用 6 块盘、4 数据 + 2 校验举例讲原理。MinIO 实际的默认参数不是这个值,它按集合盘数自己定:
4 或 5 块盘 默认 parity 2,即 2 数据 + 2 校验
6 或 7 块盘 默认 parity 3
8 到 16 块盘 默认 parity 4,冗余度 50%
生产环境 parity 至少要 EC:3,官方明确说 EC:3 以下只适合开发和测试
一条纠删码集合(Erasure Set)里最多 16 块盘,最少 2 块。集合内分片的规则是:
读仲裁 等于数据块数 K。任意 K 个分片都能把对象还原出来,数据片校验片都算
写仲裁 也是 K,但当 parity 恰好等于集合一半时是 K+1,多这一票是为了防「脑裂」——网络分区把集合劈成两半时,两边不能同时认领写入
可用容量 16 盘 EC:4 的配置是 12 TiB 可用,存储比 0.75
有个说法要纠正:「存一个文件,这八个文件夹都会存一份」是错的。一个对象会被拆成分片,每个盘只拿到其中一片。8 盘集合里,一个文件拆成 4 个数据片加 4 个校验片,分别落在 8 个目录各一份分片。所以「8 个盘都在」不是文件被复制了 8 份,而是 8 片拼起来才是完整文件。
这套设计带来一个容易被忽略的好处:单块盘坏了不用等重建。读的时候直接拿校验片反推缺失的数据片顶上,写入时也会自动把对象的 parity 往上抬一级,保证降级期间写入的数据和健康时期一个保护级别。
3. 现在的镜像该用哪个
这是 2026 年之后变化最大的一段。老教程里 docker pull minio/minio 这条命令已经失效:
2025-05-24 社区版 Web 控制台被移除,只剩对象浏览器。最后一个带完整控制台的版本是
RELEASE.2025-04-22T22-12-26Z
2025-10 停止分发社区版二进制和 Docker 镜像。最后一个社区构建是RELEASE.2025-10-15T17-29-55Z,含 CVE-2025-62506 修复
2025-12-03 GitHub 仓库进入维护模式
2026-02 README 标注不再维护
2026-04-25 仓库归档,进入只读状态
2026-09-11 Docker Hub 删除minio/minio和minio/mc,所有 tag 失效
2026-09-24 quay.io 也开始对匿名拉取返回 401,官方下载站对二进制返回 410
报错长这样:
$ docker pull minio/minio
Error response from daemon: pull access denied for minio/minio,
repository does not exist or may require 'docker login'
现在还拉得到的三个选择:
cgr.dev/chainguard/minio:latest Chainguard 维护的 fork,入口就是 minio 二进制,原来的 command 不用改。免费方案只发 latest 这个 tag。以 UID 65532 运行,挂载的数据目录要先 chown,否则启动报
file access denied
pgsty/silo Pigsty 团队的社区 fork,把管理控制台装回来了,还带中文界面,按版本打 tag。AGPLv3,以 root 运行不用 chown。原来的minio/minio换成pgsty/silo即可,数据目录原样接管
rustfs/rustfs Apache-2.0 许可,2026-09-16 发布 1.0 正式版,形态最接近 MinIO,自带控制台、IAM 用户和版本化。容器内以 UID 10001 运行,同样要 chown
客户端 mc 一起没了。对应的替代是 cgr.dev/chainguard/minio-client,或者 Silo 自带的 mcli。
选哪个看诉求:要完整控制台选 Silo,要 Apache 许可选 RustFS,只要个能跑的 S3 端点选 Chainguard 那个。RustFS 还能直接读 MinIO 的磁盘格式,迁移是拷目录而不是走网络,这点在后面还会提到。
提示:无论选哪个,按 digest 钉版本,别只写 tag。tag 会被删也会漂,digest 不会——上次镜像下架时,钉了 digest 的 compose 也一起挂了,因为请求 manifest 同样要走注册表。
4. 手里有老镜像 tar 包
镜像下架之后,当年 docker save 出来的 tar 包就成了稀缺资源。这类包的价值不在于「能跑」,而在于它锁定了一个可复现的历史版本,还能在断网的机器上把整套服务拉起来。
手头这份 minio.tar 拆开看是这样的:
175 MB,10 层,amd64/linux 底层是 UBI9-micro 精简镜像,
docker save导出的明文 tar 包
标签 minio/minio:RELEASE.2025-04-08T15-41-24Z 构建于 2025-04-08,vcs-ref是519a52b1bc2ef31e2633c20f34a0df840e159172
带完整 Web 控制台 这个时间点比 2025-05-24 砍控制台早了两周,账户、策略、桶配置、监控全都能点
自带 mc 客户端/usr/bin/mc就在镜像里,版本RELEASE.2025-04-08T15-39-49Z。minio/mc同样在 2026-09-11 被下架,这是最后一批还能用的
AGPLv3 镜像里带着licenses/LICENSE全文和licenses/CREDITS
工作目录 /,以 root 运行 声明了VOLUME /data,只EXPOSE 9000/tcp
注意最后那条:控制台端口 9001 不在镜像元数据里。EXPOSE 本来就只是个声明,不影响 -p 映射,但用编排工具按 EXPOSE 自动开端口时会漏掉它。
先别急着 load,看一眼 tar 里装的到底是什么:
# tag 和配置文件 blob 的路径
tar -xOf minio.tar manifest.json | python -c "import json,sys; d=json.load(sys.stdin)[0]; print(d['RepoTags'], d['Config'])"
# 把上面打印出来的 Config 路径填进来,看 entrypoint、预置环境变量、暴露端口
tar -xOf minio.tar blobs/sha256/9a5b995451f9d661f670fdf52b156359d31b7bedb88a46fa961953236ae73c14 \
| python -c "import json,sys; c=json.load(sys.stdin)['config']; print(c['Entrypoint'], c['Env'], c['ExposedPorts'])"
加载并确认版本:
docker load -i minio.tar
docker images | grep minio
docker run --rm minio/minio:RELEASE.2025-04-08T15-41-24Z --version
最后那条能直接打印版本号,靠的是镜像自带的 docker-entrypoint.sh:它会判断第一个参数,不是 minio 就给它前面补一个 minio,所以 --version 实际执行的是 minio --version。同理 docker run ... image server /data 也会被补成 minio server /data。
两个二进制都带官方校验和,可以核对有没有被改过:
docker run --rm --entrypoint sha256sum minio/minio:RELEASE.2025-04-08T15-41-24Z /usr/bin/minio
# aa479bd2456d0722a6737f82a1cf193aa60f934269573a6a7e024aebe1069243 /usr/bin/minio
docker run --rm --entrypoint sha256sum minio/minio:RELEASE.2025-04-08T15-41-24Z /usr/bin/mc
# 43d44d801a936e48390c8d2dc02c1c1cc1e973becbe71897f28ca06a9f456489 /usr/bin/mc
这两个值就写在镜像自己的 /usr/bin/minio.sha256sum 和 /usr/bin/mc.sha256sum 里,sha256sum 这个命令 UBI9-micro 基础镜像自带,不用额外装。
起服务和现在没区别,只是镜像名换了:
docker run -d \
--name minio_legacy \
-p 9000:9000 -p 9001:9001 \
-v /mnt/disk1/data1:/data1 \
-v /mnt/disk2/data2:/data2 \
-v /mnt/disk3/data3:/data3 \
-v /mnt/disk4/data4:/data4 \
-v /mnt/disk5/data5:/data5 \
-v /mnt/disk6/data6:/data6 \
-v /mnt/disk7/data7:/data7 \
-v /mnt/disk8/data8:/data8 \
-e MINIO_ROOT_USER=${MINIO_ROOT_USER} \
-e MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} \
-e MINIO_UPDATE=off \
minio/minio:RELEASE.2025-04-08T15-41-24Z \
server --address ":9000" --console-address ":9001" /data{1...8}
MINIO_UPDATE=off 那行别省。镜像预置了 MINIO_UPDATE_MINISIGN_PUBKEY,二进制默认会联网尝试自更新,仓库归档之后官方下载站对二进制已经返 410,关掉省事。不想开控制台再加一个 -e MINIO_BROWSER=off。
命令行客户端直接用镜像里那份 mc,不用另装。镜像把 MC_CONFIG_DIR 指向 /tmp/.mc,容器一销毁配置就没了,所以要么把宿主机目录挂进去,要么干脆开个交互式容器一次做完:
docker run -it --rm --network host \
-v /opt/minio/.mc:/tmp/.mc \
-e MINIO_ROOT_USER=${MINIO_ROOT_USER} \
-e MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} \
--entrypoint /bin/sh \
minio/minio:RELEASE.2025-04-08T15-41-24Z
# 容器里:
# mc alias set local http://127.0.0.1:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD
# mc ls local
# mc mb my-bucket
凭据这块有坑,而且坑在镜像预置的环境变量上。这份镜像的 env 里挂着这么一串:
MINIO_ROOT_USER_FILE=access_key
MINIO_ROOT_PASSWORD_FILE=secret_key
MINIO_ACCESS_KEY_FILE=access_key
MINIO_SECRET_KEY_FILE=secret_key
MINIO_CONFIG_ENV_FILE=config.env
MC_CONFIG_DIR=/tmp/.mc
access_key 和 secret_key 不是路径,是 Docker Swarm secret 的名字。程序先找 /run/secrets/access_key,找不到就当普通路径读。这套逻辑在 cmd/common-main.go 里,四条规则:
文件不存在,静默跳过 读不到就当这个变量没设,不报错。所以普通
docker run不会因为镜像预置了这两个变量而起不来
文件里有值,会盖掉 -e 传的 内部是os.Setenv写回MINIO_ROOT_USER/MINIO_ROOT_PASSWORD,你在命令行传的同名变量被顶掉
config.env 优先级最高 它最后才读,文件里每一行KEY=VALUE都会写进环境变量,连前面从 secret 文件读出来的值也一起盖掉
config.env 格式错了直接启动失败 有一行没写=就报Unable to read the config environment file;文件本身不存在反而是静默忽略
所以凭据有三种给法,从低到高排:
环境变量
-e MINIO_ROOT_USER=${MINIO_ROOT_USER} -e MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD},最省事。只要没挂到叫access_key/secret_key的文件,就不会被顶掉
挂 secret 文件 把MINIO_ROOT_USER_FILE/MINIO_ROOT_PASSWORD_FILE指到自己的绝对路径,程序读文件内容当凭据
挂 config.env-e MINIO_CONFIG_ENV_FILE=/etc/minio/config.env,文件里按KEY=VALUE写两行,键名是MINIO_ROOT_USER和MINIO_ROOT_PASSWORD,值填实际凭据,优先级最高,会盖掉前两种
还有一条最要紧的:一个凭证变量都不给,服务照样起得来。这时程序退回内置的默认凭据 minioadmin / minioadmin,写死在源码里,全网公开。这两个值配上被直接 -p 9000:9000 暴露出去的端口,等于没有认证。启动后第一件事是换掉它。
数据目录能接着用。老版本写进磁盘的元数据格式和现在这几个替代品是通的,RustFS 明确声明能直接读 MinIO 的磁盘格式,Silo 本身就是 MinIO 的 fork。同一个 /data 目录,老版跑过的数据切过去不用走网络搬运。
提示:跨大版本接管之前,先拿一份数据副本试。别在唯一一份数据上直接换版本启动,真出问题没有回头路。
这类 tar 的正确去处是离线归档,不是随手删掉。镜像仓库 2026 年 9 月清空之后,某个具体版本可能再也找不回来第二份,而 tar 是自包含的——断网的机器上 docker load 就有了。归档的时候把版本号和上面那两个 sha256 一起记进台账,几年后还能确认自己手上这份是不是同一个。
真要往外发,175 MB 是个麻烦:传不进博客附件,传不进大多数 CI 流水线,走 git 更不现实。剩下两条路:
推自己的 registry 最省事的一条,推上去别人
docker pull就有
放对象存储给直链 175 MB 的文件适合扔进对象存储,配一个预签名 URL 就能分享,细节见 阿里云 OSS 接入
# 推到自己的 registry
docker tag minio/minio:RELEASE.2025-04-08T15-41-24Z \
registry.example.com/mio/minio:RELEASE.2025-04-08T15-41-24Z
docker push registry.example.com/mio/minio:RELEASE.2025-04-08T15-41-24Z
# 传输前压一道,docker save 出来的是没压缩的分层
docker save minio/minio:RELEASE.2025-04-08T15-41-24Z | gzip -1 > minio.tar.gz
风险得说清楚:
安全补丁停了 这个版本构建于 2025-04-08,之后社区版修过 CVE-2025-62506(2025-10 最后一个社区构建才带上),2026-04 仓库归档之后再没有更新。拿它长期对外服务,就是扛着一个不再收补丁的版本
AGPLv3 的网络使用条款 镜像里licenses/LICENSE就是 AGPLv3 全文。MinIO 作为网络服务被使用,AGPL 要求你把修改版的源码提供给使用者。内部自用不构成对外提供,对外提供服务就要留意这一点
控制台是完整管理员权限 能建用户、签密钥、配策略,不要把 9001 暴露到公网
定位是过渡 价值在于「还能起、数据还在、控制台还在」,适合当迁移期的临时垫脚石,不适合当长期基座
这份 tar 该怎么用,顺序是:先核对版本和校验和,再起一个临时实例验证数据能不能读,最后收进离线归档当退路。等新存储完整跑过一轮备份周期,它的使命就完成了。
5. 单机多盘部署
一台机器挂多块盘,MinIO 会把它们组成一个纠删码集合。注意命令的最后一个参数是服务器的数据目录,必须和 volume 挂载点对上:

docker run -d \
-p 9000:9000 -p 50000:50000 \
--name minio_ec \
-v /mnt/disk1/data1:/data1 \
-v /mnt/disk2/data2:/data2 \
-v /mnt/disk3/data3:/data3 \
-v /mnt/disk4/data4:/data4 \
-v /mnt/disk5/data5:/data5 \
-v /mnt/disk6/data6:/data6 \
-v /mnt/disk7/data7:/data7 \
-v /mnt/disk8/data8:/data8 \
-e MINIO_ROOT_USER=${MINIO_ROOT_USER} \
-e MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} \
minio/minio server --address ":9000" --console-address ":50000" /data{1...8}
提示:命令末尾的镜像名按你手上有的换。用第 4 节那个 tar 加载出来的是
minio/minio:RELEASE.2025-04-08T15-41-24Z,用社区 fork 就换成cgr.dev/chainguard/minio或pgsty/silo。前面提过,Chainguard 那个以 UID 65532 运行,挂载目录要先 chown,Silo 以 root 运行不用管。
提示:
/data{1...8}里的花括号是 shell 展开,别加引号。数据目录参数和挂载点必须一致,写错了 MinIO 不报错,它会自己新建一个空目录当数据区,volume 里的数据从头到尾没被用上。
8 块盘对应 stripe 8、默认 parity 4,也就是 4 数据 + 4 校验,坏 4 块盘还能读。但这是盘级容错——整机宕了数据全没,单机形态对节点故障的容错是 0。
端口上 9000 是 S3 API,50000 是控制台。现在的 MinIO 默认控制台端口是 9001,用 50000 也行,只要不和别的服务撞。
根账号密码有两条硬要求:至少 8 位,短了直接启动失败。别用 12345678 这种,它对公网开放的实例等于没有认证。更要紧的是不要把密码写进 compose 文件,提交进 Git 就等于公开了。
# 写在环境变量或 .env 文件里,不要进版本库
MINIO_ROOT_USER=${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
还有一条容易被忽略的:MinIO 要求对数据盘有独占访问权。不要写脚本直接去动那些盘上的分片文件,官方明确说过这么做造成的损坏超出它的修复能力。
6. 分布式部署
多节点部署要起 4 个以上节点,少于 4 个起不来。常用的写法是一个脚本批量拉起:

export MINIO_ROOT_USER=${MINIO_ROOT_USER}
export MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
MINIO_HOME=/usr/local/soft
MINIO_HOST=192.168.x.x
for i in {01..04}; do
nohup ${MINIO_HOME}/minio server \
--address ":90${i}" \
--console-address ":500${i}" \
http://${MINIO_HOST}:9001/mnt/data0${i} \
http://${MINIO_HOST}:9002/mnt/data0${i} \
http://${MINIO_HOST}:9003/mnt/data0${i} \
http://${MINIO_HOST}:9004/mnt/data0${i} &
done
提示:节点之间用
http://通信时,所有节点必须用完全一样的 endpoint 列表,顺序也得一致。少写一个、多写一个,节点之间就组不成同一个集合,表现是有的节点能看到桶,有的看不到。
选型上,官方给的生产最小配置是 4 节点 × 每节点 2 块盘:总盘数 8,stripe 8,parity 4,容忍 2 台节点离线继续读、1 台离线继续写。4 节点 × 每节点 1 块盘也能跑,但写容错只剩 1 台,生产上偏薄。
分布式部署前面建议再挂一层负载均衡做 TLS 终结。MinIO 自己的 API 是 HTTP 的,生产环境走公网必须外面套一层 HTTPS。
7. 控制台被砍掉了什么
这直接决定你现在的运维方式。2025 年 5 月那次改动删掉了约 11 万行前端代码,被移走的是全部管理员功能:
账户与策略管理 建不了用户、签不了访问密钥、配不了策略,OIDC / LDAP 单点登录也没了
存储桶管理 建桶、改桶策略、版本控制、生命周期规则,UI 里全没了
配置与集群管理 改不了服务端配置,看不了集群状态和监控指标
剩下的 一个纯对象浏览器:能列桶、列文件、下载
改动前的样子长这样,左侧那一列就是被删掉的部分:

所有管理操作现在只能走 mc 命令行或者 REST API。建桶、迁桶、查看分片、配置生命周期,命令都能干,只是没有图形界面了。改用 RustFS 或 Silo 的话,上面这些功能又能用鼠标点。
8. Java SDK:9.0.x 的 API
Java 客户端最新是 io.minio:minio:9.0.3,要求 Java 8 以上。8.x 的 API 已经是 Builder + Args 的形态,3.x 时代那套 new MinioClient(url, ak, sk) 加 putObject(bucket, name, is, contentType) 的写法早被移除了,升级时是整段重写而不是改参数。
<dependency>
<groupId>io.minio</groupId>
<artifactId>minio</artifactId>
<version>9.0.3</version>
</dependency>
MinioClient minioClient = MinioClient.builder()
.endpoint("http://127.0.0.1:9000")
.credentials(accessKey, secretKey)
.build();
建桶和检查桶存在:
boolean found = minioClient.bucketExists(
BucketExistsArgs.builder().bucket("my-bucket").build());
if (!found) {
minioClient.makeBucket(MakeBucketArgs.builder().bucket("my-bucket").build());
}
上传走流式:stream 三个参数分别是输入流、对象大小、分片大小。已知大小就填实际字节数,传 -1 表示未知,SDK 会自己分片,但已知时性能更好。
try (InputStream is = file.getInputStream()) {
minioClient.putObject(
PutObjectArgs.builder()
.bucket("my-bucket")
.object(objectName)
.stream(is, file.getSize(), -1)
.contentType(file.getContentType())
.build());
}
下载和生成预签名 URL:
try (InputStream is = minioClient.getObject(
GetObjectArgs.builder().bucket("my-bucket").object(objectName).build())) {
is.transferTo(response.getOutputStream());
}
String url = minioClient.getPresignedObjectUrl(
GetPresignedObjectUrlArgs.builder()
.method(Method.GET)
.bucket("my-bucket")
.object(objectName)
.expiry(60, TimeUnit.SECONDS)
.build());
提示:
MinioClient是线程安全的,建一个单例全局复用。每次上传都 new 一个的话,底层连接池跟着一起建,连接数和握手开销都上去了。
预签名 URL 是这套存储最实用的功能:桶保持私有,客户端拿一个有效期几分钟的临时链接直接下载,字节不经过应用服务器。图片直链、视频点播、给第三方回调,全靠它。
9. 往哪迁
上游已经归档,接下来只会越来越难。给几条路:
只求能跑 换
cgr.dev/chainguard/minio,数据目录原样挂载,业务代码一行不改。适合先把 CI 拉通
要控制台 换pgsty/silo,管理界面和中文都回来了,还是 AGPLv3,和原来一致
要换供应商 换 RustFS,Apache-2.0,自带控制台和 IAM,而且能直接读 MinIO 的磁盘格式,迁移就是拷数据目录,不用走网络
彻底换协议层 应用侧从 MinIO SDK 换到 AWS SDK v2 或通用 S3 SDK,后端换 Garage、SeaweedFS 还是云上的 OSS,代码改动收敛在一个 service 实现类里
迁移期间用 mc 做数据搬运,它说的是通用 S3 协议,对任何 S3 兼容端点都能用:
mc alias set old http://old-endpoint:9000 AK SK
mc alias set new http://new-endpoint:9000 AK SK
mc mb --ignore-existing new/my-bucket
mc mirror --dry-run old/my-bucket new/my-bucket
mc mirror --preserve old/my-bucket new/my-bucket
mc diff old/my-bucket new/my-bucket
顺序是:先切写入方,做最后一次 mirror 补漏,diff 验空,再切读取方。旧的实例先停着别删,等新存储完整跑过一个备份周期再清。
不想自建 阿里云 OSS 接入 同样是 S3 协议,运维责任交给云厂商,代码换的是 endpoint 和那对密钥
想把存储变成网盘 AList 容器部署 它支持挂 S3 后端,能把 MinIO 或 OSS 再包装成一个能预览、能用 WebDAV 挂载的网盘
💡 补充:换到 AWS SDK v2 时有个细节——它默认走 virtual-hosted 寻址,直连 IP 会做 DNS 解析然后失败,连 IP 的自建存储必须开 forcePathStyle(true)。另外新版 SDK 默认用分块流式签名,老版本 MinIO 解析不了,会报 XAmzContentSHA256Mismatch。
🔥 一句话总结:MinIO 的价值是 S3 兼容加纠删码,但上游已经归档、社区镜像在 2026 年 9 月从 Docker Hub 消失,现在要么换社区 fork,要么迁到 RustFS 这类还在维护的 S3 实现;手里那份 2025 年 4 月的老 tar 包值得留着当退路,核对完版本和校验和再起。
