Files
ystp/docs/deployment.md
237899745 f8f5da04db
Some checks failed
CI / verify (push) Has been cancelled
docs: document migration and quota safeguards
2026-07-26 05:48:14 +08:00

216 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 部署指南
## 生产部署
仓库提供完整的 `docker/Dockerfile``docker/docker-compose.prod.yml`。生产编排包含 API、Worker、PostgreSQL 和 Redis数据库与 Redis 不发布宿主机端口API 和 Worker 使用同一上传卷,并以非 root、只读根文件系统运行。
### 环境要求
- Linux x86_64
- Docker Engine 26+ 与 Docker Compose 2.20+
- 最低 2 核 CPU、4GB 内存;启用 AVIF 和独立 Worker 时建议 4 核、8GB 内存
- 首次构建可访问 Docker Hub 与 crates.io
Debian 13、4 核 CPU、8GB 内存的起始值建议为 `WORKER_TASK_CONCURRENCY=2``WORKER_CONCURRENCY=2``IMAGE_PROCESSING_CONCURRENCY=2`8 核应用服务器可从 `4/2/4` 开始。三者分别表示同时处理的任务数、单任务内文件数和单进程 CPU 图片处理上限。最后一项是全局 CPU 闸门,因此不要把它设置为 CPU 核数的数倍。任务并发提高后,数据库连接池建议至少为 `WORKER_TASK_CONCURRENCY * WORKER_CONCURRENCY + 4`,生产示例使用 16。
8 核 16GB 应用服务器的 ZIP 起始值为 `ZIP_BUILD_CONCURRENCY=2``ZIP_MAX_ENTRIES=200``ZIP_MAX_UNCOMPRESSED_BYTES=2147483648`。ZIP 使用 stored 模式,构建时同时存在下载源和归档文件,按两个 2 GiB 构建估算应至少保留约 8 GiB 临时磁盘余量;磁盘较小时应先降低总字节或并发,而不是提高 HTTP 超时。
生产 Compose 默认按 8 核 16GB 主机设置可覆盖的资源上限API 3GB、Worker 8GB、PostgreSQL 2GB、Redis 1GB对应变量为 `API_MEMORY_LIMIT``WORKER_MEMORY_LIMIT``POSTGRES_MEMORY_LIMIT``REDIS_MEMORY_LIMIT`。Redis 的 `REDIS_MAXMEMORY` 默认 768MB达到上限后返回写入错误而不是继续挤占宿主机内存。
### 首次启动
```bash
cp docker/.env.production.example .env.production
chmod 600 .env.production
# 分别生成 POSTGRES_PASSWORD、JWT_SECRET、API_KEY_PEPPER 和管理员密码。
# hex 不包含数据库 URL 与 .env 需要转义的保留字符。
openssl rand -hex 32
# 编辑公开地址和全部 replace-with-* 值后检查配置。
docker compose \
--env-file .env.production \
-f docker/docker-compose.prod.yml \
config --quiet
docker compose \
--env-file .env.production \
-f docker/docker-compose.prod.yml \
up -d --build
```
API 健康后 Worker 才会启动,避免两个进程在首次部署时同时执行迁移。
管理员可用邮箱或用户名登录。首次启动时设置 `ADMIN_USERNAME``ADMIN_PASSWORD` 即可创建管理员;若未设置 `ADMIN_EMAIL`,系统会生成仅用于满足内部数据约束的 `用户名@local.invalid` 占位邮箱。确认账号创建后应从生产环境文件中移除 `ADMIN_PASSWORD`,避免每次重启都重置密码。
```bash
docker compose --env-file .env.production -f docker/docker-compose.prod.yml ps
curl --fail http://127.0.0.1:8080/health
curl --fail http://127.0.0.1:8080/metrics
```
预期健康响应:
```json
{"status":"healthy","database":"connected","redis":"connected"}
```
### 更新与回滚
更新代码后保留 `.env.production` 和命名卷。包含迁移 `017``022` 的版本不能让旧、新 API 或 Worker 并行滚动:旧 API 不理解 ZIP 构建租约,旧 Worker 不理解任务 attempt fencing。先备份数据库并停止旧 API/Worker再构建新镜像。
迁移 `017` 会在发现重复 Customer 或同用户多条未取消 Stripe 订阅时主动失败,迁移 `019` 会在发现同一 Stripe 发票对应多行时主动失败。部署前先检查并人工对账,三个查询都必须返回 0 行:
```sql
SELECT billing_customer_id, COUNT(*)
FROM users
WHERE billing_customer_id IS NOT NULL AND billing_customer_id <> ''
GROUP BY billing_customer_id
HAVING COUNT(*) > 1;
SELECT user_id, COUNT(*)
FROM subscriptions
WHERE provider = 'stripe' AND status <> 'canceled'
GROUP BY user_id
HAVING COUNT(*) > 1;
SELECT provider, provider_invoice_id, COUNT(*)
FROM invoices
WHERE provider_invoice_id IS NOT NULL
GROUP BY provider, provider_invoice_id
HAVING COUNT(*) > 1;
```
推荐顺序:
```bash
git pull --ff-only
docker compose --env-file .env.production -f docker/docker-compose.prod.yml stop api worker
docker compose --env-file .env.production -f docker/docker-compose.prod.yml build api
docker compose --env-file .env.production -f docker/docker-compose.prod.yml up -d postgres redis api
docker compose --env-file .env.production -f docker/docker-compose.prod.yml up -d worker
```
新 API 启动后会消费迁移 `018` 创建的订阅对账队列。迁移 `020` 为历史发票写入待对账哨兵,发票不主动批量拉取,而是在首个后续事件到达时取 Stripe 快照。启动 Worker 前应确认 API 健康、`STRIPE_SECRET_KEY` 可用且服务器能访问 `STRIPE_API_BASE_URL`;订阅对账可以后台继续,但必须监控失败项:
```sql
SELECT status, COUNT(*)
FROM stripe_subscription_reconciliations
GROUP BY status;
SELECT object_type, requires_reconciliation, COUNT(*)
FROM provider_object_event_watermarks
WHERE provider = 'stripe'
GROUP BY object_type, requires_reconciliation
ORDER BY object_type, requires_reconciliation;
```
`failed` 会指数退避重试;持续失败通常表示 Stripe 凭据、网络、Customer/Price 映射不完整。上线验收要求订阅队列的 `pending/processing/failed` 最终归零,且 subscription 水位不再待对账invoice 水位在对应发票首个后续事件到达前保持 `requires_reconciliation=true` 属于预期状态。生产镜像应使用不可变的 `IMAGEFORGE_TAG`。数据库迁移已应用后,不能只回滚旧二进制;应保留新 schema并使用兼容该 schema 的修复镜像。
### 反向代理
生产 Compose 默认把 API 端口绑定到 `127.0.0.1`。若确实需要绕过反向代理直接通过服务器地址访问,显式设置 `IMAGEFORGE_BIND_ADDRESS=0.0.0.0`,同时保持 `TRUST_PROXY_HEADERS=false` 并配置主机防火墙。只有当 API 端口不对客户端开放、所有请求都经过可信反向代理时,才设置 `TRUST_PROXY_HEADERS=true`,并由代理覆盖 `X-Forwarded-For``X-Forwarded-Proto`
代理至少需要:
```nginx
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100m;
proxy_read_timeout 300s;
}
```
`/metrics` 包含运行状态与队列数据,不应通过公开域名暴露。生产 Nginx 应单独拒绝该路径Prometheus 直接抓取只绑定回环地址的 API 端口:
```nginx
location = /metrics {
allow 127.0.0.1;
allow ::1;
deny all;
proxy_pass http://127.0.0.1:8080;
}
```
应用会为所有响应设置 CSP、`X-Content-Type-Options``X-Frame-Options``Referrer-Policy``Permissions-Policy`。TLS 网关还应设置 `Strict-Transport-Security`
### 日志与备份
```bash
docker compose --env-file .env.production -f docker/docker-compose.prod.yml logs -f api worker
docker compose --env-file .env.production -f docker/docker-compose.prod.yml \
exec -T postgres pg_dump -U imageforge imageforge | gzip > imageforge.sql.gz
```
除数据库外,还要备份 Compose 的 `uploads` 命名卷。恢复前停止 API 与 Worker避免数据库记录与文件卷产生时间差。
## 本地开发
```bash
cp .env.example .env
docker compose -f docker/docker-compose.dev.yml up -d
# 终端 1
IMAGEFORGE_ROLE=api cargo run
# 终端 2
IMAGEFORGE_ROLE=worker cargo run
# 终端 3
cd frontend
npm ci
npm run dev
```
API 或 Worker 启动时会通过 SQLx 顺序执行尚未应用的迁移。迁移失败时进程退出,不会在不完整的数据库结构上继续提供服务。
## 压缩质量回归
基准工具会下载固定 Picsum 照片,分别生成 JPEG、PNG、WebP、AVIF 输入,再通过真实 HTTP API 测量目标达标率、格式签名、SSIM、PSNR、分辨率和耗时。
```bash
python3 -m venv .venv-benchmark
. .venv-benchmark/bin/activate
pip install -r scripts/requirements-benchmark.txt
python scripts/benchmark_compression.py generate --output-dir .bench/corpus
IMAGEFORGE_BENCH_EMAIL=admin@example.com \
IMAGEFORGE_BENCH_PASSWORD='replace-me' \
python scripts/benchmark_compression.py run \
--base-url http://127.0.0.1:8080 \
--input-dir .bench/corpus \
--output-dir .bench/results \
--formats jpeg,webp,avif \
--rates 30,50,70 \
--strict
```
`--strict` 会在请求失败、输出格式不匹配或目标体积未达标时返回非零退出码。测试素材与结果位于 `.bench/`,不会提交到 Git。
## 故障排查
```bash
# 容器与健康状态
docker compose --env-file .env.production -f docker/docker-compose.prod.yml ps
# 最近日志
docker compose --env-file .env.production -f docker/docker-compose.prod.yml logs --tail=200 api worker
# 数据库迁移状态
docker compose --env-file .env.production -f docker/docker-compose.prod.yml \
exec -T postgres psql -U imageforge -d imageforge \
-c 'SELECT version, success FROM _sqlx_migrations ORDER BY version;'
# 主机资源
docker stats
df -h
```
若图片压缩长时间排队,先检查 CPU再调整 `IMAGE_PROCESSING_CONCURRENCY`;大任务阻塞小任务时提高 `WORKER_TASK_CONCURRENCY`,单个批量任务推进过慢时再评估 `WORKER_CONCURRENCY`。若 API 健康但批量任务不推进,检查 Worker 日志、Redis pending 数和数据库连接池等待情况。