refactor(storage): 对象存储从 MinIO 换成 SeaweedFS(钉 4.47)
动机:MinIO 的 GitHub 仓库已于 2026-02 归档,不再产出安全补丁,已累积多个 CVE 且不修;本项目存身份证 / 实名材料 / 用户照片,无人修 CVE 才是真正驱动因素 (Docker Hub 下架只是表象,quay.io 仍在提供)。 变更: - 三套 compose(dev / staging / prod)的 minio 服务替换为 seaweedfs 4.47: 覆盖 entrypoint 渲染 -s3.config 注入 identity(S3 网关默认不鉴权,必须显式开启), -volume.max=100 解除第二个 bucket 写入时的 InternalError。 - 新增一次性 storage-init 容器,经 filer 接口预建 public / private 桶 (SeaweedFS PutObject 到不存在的 bucket 直接失败;全仓库原无任何 createBucket)。 - S3Config 显式开启 path-style:AWS SDK v1 默认 virtual-host 会把 bucket 拼进 主机名,容器名在 DNS 中不存在必然失败(本机有 http_proxy 时易误判为 502 拒绝)。 - ci.yml:镜像校验与起栈步骤改为 seaweedfs + storage-init。 - 运维脚本 health-check.sh / run-uat.sh 的 MinIO 探针改为 SeaweedFS (S3 网关开启鉴权后匿名根请求返回 403,200/403 均算存活)。 注:path-style 与建桶属换存储的必要项;presigned URL 的 host 须对外可达这一 既有缺陷(原 minio:9000 直发客户端不可达)留待下一轮「改缺陷」处理。
This commit is contained in:
@@ -32,44 +32,99 @@ services:
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
minio:
|
||||
# 2026-09-22 修正:镜像源从 Docker Hub 迁到 quay.io。
|
||||
seaweedfs:
|
||||
# 2026-09-22:对象存储从 MinIO 换成 SeaweedFS。
|
||||
#
|
||||
# 原 `minio/minio:RELEASE.2025-09-07T16-13-09Z` 在 CI 上根本拉不到,Actions 报:
|
||||
# pull access denied for minio/minio, repository does not exist
|
||||
# 问题不在 tag,而在**整个仓库已从 Docker Hub 移除**:MinIO 自 2025-10 起停止发布
|
||||
# 容器镜像(改为仅源码分发),其 GitHub 仓库于 2026-02 归档;Docker Hub 上
|
||||
# minio/minio 与 minio/mc 现均返回 404。quay.io/minio/minio 仍在提供镜像。
|
||||
# 动机不是「拉不到镜像」。MinIO 的 GitHub 仓库已于 2026-02 归档,**不再产出安全补丁**,
|
||||
# 已累积 CVE-2025-62506 / CVE-2026-39414 / CVE-2026-40344 / CVE-2026-42600 且不修。
|
||||
# 本项目存身份证、实名材料与用户照片 —— 无人修 CVE 才是真正的驱动因素。
|
||||
# (Docker Hub 上 minio/minio 仓库自 2026-09-12 起返回 404 只是表象;quay.io 仍在提供,
|
||||
# 2026-09-22 核实时发现 quay 上仍有 2026-04 的 hotfix tag,故「换源」可以再拖,
|
||||
# 「无人修 CVE」不能拖。)
|
||||
#
|
||||
# 本机此前正是用 quay.io 的镜像重打 tag 顶替才跑通的(见 known-risks KR-01),
|
||||
# 但那只是**本地绕过**、没有同步回仓库,于是「本地能起栈、CI 拉不到」。
|
||||
# 现在统一改为 quay.io,让本地与 CI 拉同一个镜像,消除这处不一致。
|
||||
# 选型依据来自本机实测而非文档推断:本项目只用到极窄的 S3 操作面
|
||||
# —— presigned PUT/GET + putObject + 两个 bucket,无 versioning / lifecycle / policy。
|
||||
# SeaweedFS 4.47 在 path-style 下这三项全部通过且**字节一致**。
|
||||
#
|
||||
# 版本来源(2026-09-22 于 CI 实测):迁移初期先用了 quay.io 的 latest,
|
||||
# 并在 e2e job 里打印实际版本,得到
|
||||
# minio version RELEASE.2025-09-07T16-13-09Z (commit-id=07c3a429bfed…)
|
||||
# 即 latest 当前正指向这个版本 —— 与本仓库原先在 Docker Hub 上钉的 tag 完全一致。
|
||||
# 现在据此钉死,恢复「可复现」这一原则(若日后 quay 上的 latest 滚动到别的版本,
|
||||
# 本配置不受影响)。
|
||||
# 长期风险:quay 上的 MinIO 镜像已冻结(最后发布 2025-09),同样可能下架,
|
||||
# 届时需自建镜像或迁移到其他 S3 兼容存储 ——
|
||||
# 「依赖第三方镜像」本身就是一条外部依赖风险。
|
||||
image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
|
||||
container_name: danyang-match-minio
|
||||
command: server /data --console-address ":9001"
|
||||
# 必须显式启用鉴权:SeaweedFS 的 S3 网关**默认完全不鉴权** —— 实测匿名 GET 私有对象
|
||||
# 200、匿名 PUT 200、匿名列举桶 200,AK/SK 形同虚设。对本项目的敏感数据不可接受。
|
||||
# 故用 -s3.config 注入 identity;配好后匿名访问回到 403(已实测)。
|
||||
#
|
||||
# 版本钉 4.47(实测 `weed version` = 4.47 c50733600),不用 latest:
|
||||
# 滚动 tag 会让 CI 拉到与本地验证时不同的镜像,属不可复现的偶发失败来源。
|
||||
image: chrislusf/seaweedfs:4.47
|
||||
container_name: danyang-match-seaweedfs
|
||||
# 凭据不入库:compose 无法把变量插值进挂载文件的内容,故在容器内把环境变量
|
||||
# 渲染成 s3cfg.json 再交给 weed。(未加引号的 heredoc 是故意的:需要展开变量。)
|
||||
# 必须覆盖 entrypoint:该镜像的 ENTRYPOINT 是 /usr/bin/weed,
|
||||
# 只写 command 会被追加成 `weed sh -c ...` → `weed: unknown subcommand "sh"`(实测踩到)。
|
||||
entrypoint: ["sh", "-c"]
|
||||
command:
|
||||
- |
|
||||
cat > /tmp/s3cfg.json <<CFG
|
||||
{"identities":[{"name":"app","credentials":[{"accessKey":"$${MINIO_ACCESS_KEY}","secretKey":"$${MINIO_SECRET_KEY}"}],"actions":["Admin","Read","Write","List","Tagging"]}]}
|
||||
CFG
|
||||
# -volume.max=100 是必需的,不是调优:
|
||||
# SeaweedFS 把 **bucket 名当作 collection**,每个 collection 至少要分到一个 volume。
|
||||
# 而 volume server 默认上限只有 8 个(实测 /dir/status:Volumes 8、Max 8、Free 0),
|
||||
# 于是第二个 bucket 写入时 master 报
|
||||
# failed to find writable volumes for collection:public … topo failed to pick 1 from 0 node candidates
|
||||
# 表现为 put_object 返回 InternalError —— 错误信息完全不提 volume 配额,
|
||||
# 不知道这条就只会反复重试。volume 是按需增长的(/data 下空卷仅 8 字节),
|
||||
# 故调大上限不占实际空间。
|
||||
exec weed server -s3 -s3.port=8333 -s3.config=/tmp/s3cfg.json -dir=/data -volume.max=100
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:?请在本地 .env 设置 MINIO_ACCESS_KEY}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:?请在本地 .env 设置 MINIO_SECRET_KEY}
|
||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:?请在本地 .env 设置 MINIO_ACCESS_KEY}
|
||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:?请在本地 .env 设置 MINIO_SECRET_KEY}
|
||||
ports:
|
||||
- "9003:9000"
|
||||
- "9004:9001"
|
||||
- "8333:8333" # S3 网关:后端签名、客户端直传都走它
|
||||
- "9333:9333" # master:集群状态 / 排查用
|
||||
- "8888:8888" # filer:storage-init 建桶用
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
- seaweedfs_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 10s
|
||||
# 不能用 curl -f:鉴权开启后匿名列桶返回 403,而 403 恰恰说明网关正在服务。
|
||||
# 因此显式接受 200(未鉴权时)与 403(已鉴权时)两种状态码。
|
||||
test: ["CMD-SHELL", "code=$$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8333/); [ \"$$code\" = 200 ] || [ \"$$code\" = 403 ]"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
retries: 24
|
||||
|
||||
storage-init:
|
||||
# SeaweedFS 与 MinIO 的一个关键差异:**PutObject 到不存在的 bucket 会直接失败**
|
||||
# (实测:匿名下 AccessDenied、正确凭据下 404),不会自动建桶。
|
||||
# 而本项目全仓库没有任何 createBucket 代码或初始化脚本 —— 桶必须显式预建,
|
||||
# 否则换完存储照片上传依然是断的(这一点在 MinIO 时代就已是既成缺陷,
|
||||
# 见 tests/e2e/storage_link_probe.py 的说明)。
|
||||
#
|
||||
# 建桶走 filer 的目录接口(POST /buckets/<name>/),无需 SigV4 签名,
|
||||
# 因此可复用 seaweedfs 镜像自带的 curl,**不引入新镜像依赖**。
|
||||
# 实测该方式建的目录会被 S3 网关识别为 bucket:list_buckets 可见、
|
||||
# head_bucket 200、put_object 成功,且鉴权照常生效(匿名 GET 403)。
|
||||
#
|
||||
# 幂等语义(实测):首次 POST=201,已存在 POST=409,GET 到目录=200。
|
||||
# 三者都视为「桶已就位」,故重跑 up 不会失败。
|
||||
image: chrislusf/seaweedfs:4.47
|
||||
container_name: danyang-match-storage-init
|
||||
depends_on:
|
||||
seaweedfs:
|
||||
condition: service_healthy
|
||||
entrypoint:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
for b in public private; do
|
||||
code=$$(curl -s -o /dev/null -w '%{http_code}' "http://seaweedfs:8888/buckets/$$b/" || echo 000)
|
||||
if [ "$$code" = "200" ]; then echo "bucket $$b: already exists"; continue; fi
|
||||
i=0
|
||||
while [ $$i -lt 30 ]; do
|
||||
code=$$(curl -s -o /dev/null -w '%{http_code}' -X POST "http://seaweedfs:8888/buckets/$$b/" || echo 000)
|
||||
case "$$code" in 200|201|409) break ;; esac
|
||||
i=$$((i+1)); sleep 2
|
||||
done
|
||||
echo "bucket $$b: create -> $$code"
|
||||
done
|
||||
restart: "no"
|
||||
|
||||
backend:
|
||||
build:
|
||||
@@ -81,8 +136,11 @@ services:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
seaweedfs:
|
||||
condition: service_healthy
|
||||
storage-init:
|
||||
# 一次性容器:桶没建好时后端任何 putObject / presigned 直传都会失败。
|
||||
condition: service_completed_successfully
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: docker
|
||||
DB_HOST: postgres
|
||||
@@ -93,7 +151,11 @@ services:
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
REDIS_PASSWORD: ${REDIS_PASSWORD:?请在本地 .env 设置 REDIS_PASSWORD}
|
||||
MINIO_ENDPOINT: http://minio:9000
|
||||
# 变量名仍沿用 MINIO_* 前缀(服务端 Bean 与 application-*.yml 都在读它),
|
||||
# 但它现在指向 SeaweedFS 的 S3 网关 —— 属历史命名,待下一轮统一改名:
|
||||
# 那时会同时处理「presigned URL 的 host 必须是对外可达地址」这一缺陷,
|
||||
# 届时 endpoint 语义会从单一值拆成「内网签发地址 + 外网访问地址」。
|
||||
MINIO_ENDPOINT: http://seaweedfs:8333
|
||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:?请在本地 .env 设置 MINIO_ACCESS_KEY}
|
||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:?请在本地 .env 设置 MINIO_SECRET_KEY}
|
||||
JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:?请在本地 .env 设置 JWT_ACCESS_SECRET}
|
||||
@@ -147,4 +209,4 @@ services:
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
minio_data:
|
||||
seaweedfs_data:
|
||||
|
||||
Reference in New Issue
Block a user