Files
ystp/docs/s3-storage-plan.md
2026-07-26 05:47:19 +08:00

9.9 KiB
Raw Permalink Blame History

ImageForge 双服务器 S3 方案

本文是部署前方案与验收清单。本次开发未在 118.145.177.79119.29.142.248 上安装、修改或启动任何服务。

1. 现状与结论

节点 实测资源 建议职责 关键约束
118.145.177.79 8 vCPU、31 GiB RAM、约 104 GiB 可用磁盘 ImageForge API、Worker、PostgreSQL、Redis、ZIP 临时生成 已有较多容器,9000 端口已占用,后续建议应用只绑定 127.0.0.1:18080
119.29.142.248 2 vCPU、1.9 GiB RAM、约 29 GiB 可用磁盘、200M 共享带宽 Garage S3、Nginx/TLS、下载出口 只能作为首期临时对象存储;系统盘容量而不是带宽会最先成为瓶颈

首期选择 Garage 2.3,而不是 MinIO。Garage 面向低资源和普通互联网环境,支持本项目所需的 SigV4、Path-style、预签名 URL、对象读写删除、分片上传和生命周期。MinIO 官方单节点说明包含 2 GiB 预分配并建议 32 GiB 内存,不适合 119 当前的 2 GiB 机器。

2. 目标架构

flowchart LR
    U["用户浏览器"] -->|"上传、鉴权、任务查询"| A["118: ImageForge API"]
    A --> P["118: PostgreSQL"]
    A --> R["118: Redis"]
    A --> W["118: Image Worker"]
    W -->|"WireGuard / S3 PUT"| S["119: Garage S3"]
    A -->|"鉴权通过后返回 307"| U
    U -->|"5 分钟预签名 GET"| N["119: Nginx TLS"]
    N --> S

核心规则:

  1. Bucket 始终私有,不开放匿名读。
  2. 用户先访问 ImageForge 下载接口;后端检查用户、匿名会话和过期时间后,返回 307 到 5 分钟预签名 URL。
  3. 图片和 ZIP 的实际下载字节只经过 119118 不再承担下载出口。
  4. 批处理原图只在 118 的共享 uploads/orig 临时保存Worker 完成或失败后删除,不上传 S3。
  5. 批量 ZIP 在 118 临时生成64 MiB 以上自动使用 S3 分片上传,上传后删除临时目录。
  6. 每个成品记录写入时的端点 ID。以后切换端点不会使旧文件失联旧端点凭据会保留到关联对象清空。
  7. 活动 S3 仍是首选后端;连接、凭据或上传失败时,本次成品自动写入 118 本地卷并记录 storage_backend=local。S3 恢复后,后续新对象会再次优先写入 S3。

/api/v1/compress/direct 保留原有“响应体直接返回图片”的 API 语义,避免破坏现有调用方;网站下载、历史记录下载、普通压缩结果和批量 ZIP 均走 S3。

3. 网络与端口

建议先在两台服务器之间建立 WireGuard

用途 118 119
WireGuard 地址 10.70.0.1/24 10.70.0.2/24
S3 内部 Endpoint 客户端 10.70.0.2:3900
Garage RPC 后续扩容节点 10.70.0.2:3901
公网下载 不开放 files.<你的域名>:443

119 防火墙只允许:公网 80/443、受限来源的 22、WireGuard 对端访问 3900/3901。Garage 管理端口 3903 只绑定回环地址。不要把 3900/3901/3903 直接暴露到公网。

119 已有 Nginx 和 wy.workyai.cn,后续只新增独立的 files.<你的域名> server block不能覆盖现有站点配置。公网 Nginx 只允许 GET/HEAD,应用上传走 WireGuard 内部 Endpoint。

4. 保留与清理

用户层级 数据库精确保留 对象前缀 S3 兜底生命周期
未登录、免费用户 24 小时 results/1d/archives/1d/ 3 天
低级会员 Pro 7 天 results/7d/archives/7d/ 9 天
高级会员 Business 15 天 results/15d/archives/15d/ 17 天

Worker 每 5 分钟按 expires_at 精确删除对象删除成功后才删除数据库任务。ZIP 构建 attempt 位于对应 archives/<retention>/.../attempts/ 前缀发布失败会立即删除进程崩溃遗留项仍由同一前缀生命周期兜底。S3 生命周期多保留 2 天,只负责处理数据库故障、进程崩溃或上传后未能落库的孤儿对象,不能作为精确会员权限判断。未完成的分片上传 1 天后由生命周期中止。

5. 119 首期容量

119 当前约 29 GiB 可用建议至少给系统、Docker/Nginx 日志和 Garage 元数据保留 9 GiB因此首期只按约 20 GiB 对象容量规划:

  • 70%(约 14 GiB告警检查日新增量和清理是否正常。
  • 85%(约 17 GiB紧急告警停止营销放量并准备扩盘或新端点。
  • 90%(约 18 GiB写入保护不要继续依赖该系统盘接收新对象。

粗略容量公式:日均压缩后新增量 × 加权平均保留天数 × 1.2。例如每天 1 GiB、平均保留 5 天,仅对象约 6 GiB每天 4 GiB 时就会接近首期上限。200 Mbps 理论上约 25 MB/s但共享带宽、磁盘随机读和 2 核 CPU 会使实际吞吐更低。

正式增长前优先给 119 挂载独立的 100 GiB 以上数据盘到 /srv/garage/data。单节点 replication_factor = 1 没有冗余,官方也不建议用于长期生产数据;本项目对象最长 15 天且可重新生成,低用户量阶段可以接受,但不能替代备份。后续可新增 Garage 节点并提升副本数,或在管理后台新增另一套 S3 端点并切换新对象。

6. 管理后台配置

部署 S3 并创建 Bucket/Key 后,在“管理后台 -> 对象存储”新增:

字段 首期建议值
名称 119 高带宽 S3
内部 Endpoint http://10.70.0.2:3900
公网 Endpoint https://files.<你的域名>
Bucket imageforge-results
Region garage
Force path style 开启
签名有效期 300

Access Key 和 Secret Key 使用项目现有 AES-256-GCM 机制加密入库,页面只显示掩码。保存后先执行“全链路测试”,再点“验证并启用”。后端激活前会再次执行 HeadBucket + 内部 PutObject/GetObject + 公网预签名 GET + 内部 DeleteObject。编辑现有端点时先对候选配置执行同等测试,并抽查历史对象是否仍可访问;测试通过后才保存,活动状态不变。删除采用软删除,立即停止新写入并隐藏端点,但保留历史对象所需的加密配置;历史关联清空 30 天后再彻底移除。若删除活动端点,新文件自动回退应用服务器本地,直到启用其他 S3。

浏览器通过公网预签名地址直接读取对象,因此 Bucket 必须允许应用站点跨域 GET/HEAD。将 docker/storage/cors.json 中的站点域名改成实际应用域名后,通过兼容 S3 的管理工具执行 PutBucketCors。后台全链路测试会携带 PUBLIC_BASE_URL 的 Origin并拒绝缺失或不匹配的 Access-Control-Allow-Origin,避免对象存在但浏览器无法下载。

7. 后续部署顺序(本次不执行)

  1. 为下载域名添加 DNS确认 119 的 80/443 可用,并建立 WireGuard。
  2. 在 119 创建 /srv/garage/{meta,data,snapshots},生成三个权限为 0600 的 secret 文件。
  3. 使用 docker/storage/garage.toml.exampledocker-compose.storage.yml.example 启动 Garage。
  4. 创建 imageforge-results Bucket 和仅限该 Bucket 的应用 Key应用生命周期文件 lifecycle.json
  5. 新增 nginx-files.conf.example 对应的 HTTPS 站点;确认访问日志不记录签名查询字符串。
  6. 在 118 备份数据库,部署新版本并执行迁移;首次启动没有活动 S3 时仍使用本地存储。
  7. 管理后台保存 119 端点,执行测试并启用。
  8. 使用 JPEG、PNG透明/非透明、WebP、AVIF、GIF、BMP、TIFF、ICO以及中文名、同名文件、大图和批量 ZIP 做验收。

Garage 初始化命令示例(实际部署时执行,输出的 Secret Key 只录入管理后台,不提交仓库):

docker compose -f docker-compose.storage.yml exec garage /garage bucket create imageforge-results
docker compose -f docker-compose.storage.yml exec garage /garage key create imageforge-app
docker compose -f docker-compose.storage.yml exec garage /garage bucket allow \
  --read --write --owner imageforge-results --key imageforge-app
aws --endpoint-url http://127.0.0.1:3900 \
  s3api put-bucket-lifecycle-configuration \
  --bucket imageforge-results \
  --lifecycle-configuration file://lifecycle.json
aws --endpoint-url http://127.0.0.1:3900 \
  s3api put-bucket-cors \
  --bucket imageforge-results \
  --cors-configuration file://cors.json

8. 上线验收

  • 管理端测试必须完成读、写、删Bucket 内不能残留健康检查对象。
  • 普通下载接口先返回 307Location 指向下载域名且有效期约 300 秒。
  • 携带应用站点 Origin 请求预签名地址时返回匹配的 Access-Control-Allow-Origin,浏览器可读取完整对象。
  • 未授权用户无法取得签名 URL任务过期后应用下载接口返回 404。
  • 单文件与批任务数据库均记录正确的 storage_endpoint_id 和对象键。
  • 切换到第二端点后,新对象进入第二端点,第一端点历史对象仍可下载。
  • S3 停止时同步请求和批任务均完成本地兜底,数据库记录 storage_backend=local,下载接口直接从应用服务器返回文件。
  • S3 恢复后下一次新对象重新记录为 storage_backend=s3,下载接口恢复 307 到签名 URL。
  • 删除 S3 对象失败时,过期任务数据库记录保留并在下一轮重试。
  • 119 磁盘 70%/85% 告警、容器重启、NTP 时间同步和证书续期均验证通过。