feat: optimize compression and production deployment

This commit is contained in:
237899745
2026-07-25 11:20:45 +08:00
parent 9d7668bdee
commit 0ff9eae56d
15 changed files with 882 additions and 771 deletions

View File

@@ -272,9 +272,9 @@ Idempotency-Key: <key> # 建议
| 字段 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `file` | File | 是 | 图片文件 |
| `compression_rate` | Integer | 否 | 压缩率 1-100(压缩后体积占原图比例,数值越小压缩越强100 表示不压缩优先级高于 `level` |
| `compression_rate` | Integer | 否 | 压缩率 1-100JPEG/WebP/AVIF 以该比例为体积上限,无损格式为尽力优化100 表示不压缩优先级高于 `level` |
| `level` | String | 否 | `high` / `medium` / `low`(兼容参数,默认 `medium` |
| `output_format` | String | 否 | 输出格式:`png/jpeg/webp/avif/gif/bmp/tiff/ico`(默认保持原格式) |
| `output_format` | String | 否 | 输出格式:`png/jpeg/webp/avif/gif/bmp/tiff/ico`(默认保持原格式ICO 自动等比缩至 256x256 边界 |
| `max_width` | Integer | 否 | 最大宽度(等比缩放) |
| `max_height` | Integer | 否 | 最大高度(等比缩放) |
| `target_size_bytes` | Integer | 否 | 目标体积(字节),仅 `jpeg/webp/avif` 输出支持;会优先保清晰度并在必要时小幅缩放 |
@@ -340,7 +340,7 @@ Idempotency-Key: <key> # 建议
| 字段 | 类型 | 必填 | 说明 |
|---|---|---:|---|
| `files[]` | File[] | 是 | 图片文件数组(上限由套餐决定) |
| `compression_rate` | Integer | 否 | 压缩率 1-100(压缩后体积占原图比例,数值越小压缩越强100 表示不压缩优先级高于 `level` |
| `compression_rate` | Integer | 否 | 压缩率 1-100JPEG/WebP/AVIF 以该比例为体积上限,无损格式为尽力优化100 表示不压缩优先级高于 `level` |
| `level` | String | 否 | `high` / `medium` / `low`(兼容参数) |
| `output_format` | String | 否 | 输出格式:`png/jpeg/webp/avif/gif/bmp/tiff/ico`(默认保持原格式) |
| `preserve_metadata` | Boolean | 否 | 是否保留元数据(默认 `false` |

View File

@@ -1,714 +1,155 @@
# 部署指南
## 环境准备
### 系统要求
- Linux (Ubuntu 22.04+ / Debian 12+ 推荐)
- 2+ CPU 核心(启用独立 Worker 建议 4+
- 4GB+ 内存
- 50GB+ 磁盘空间
### 依赖安装
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install -y \
build-essential \
pkg-config \
libssl-dev \
libpq-dev \
cmake \
libdav1d-dev \
nasm \
libjpeg-dev \
libpng-dev \
libwebp-dev
# 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
# 初始化数据库会用到 psql建议安装 PostgreSQL client
sudo apt install -y postgresql-client
# 安装 Node.js (前端构建)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
```
---
## 本地开发
### 1. 启动数据库服务
```bash
# 使用 Docker Compose 启动 PostgreSQL 和 Redis
docker-compose -f docker/docker-compose.dev.yml up -d
```
`docker/docker-compose.dev.yml`:
```yaml
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: imageforge
POSTGRES_PASSWORD: devpassword
POSTGRES_DB: imageforge
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
# 规划:接入 S3 存储后可增加 MinIO当前版本仅支持本地存储
# minio:
# image: minio/minio:RELEASE.2024-01-28T20-20-01Z
# command: server /data --console-address ":9001"
# environment:
# MINIO_ROOT_USER: minioadmin
# MINIO_ROOT_PASSWORD: minioadmin
# ports:
# - "9000:9000"
# - "9001:9001"
# volumes:
# - minio_data:/data
volumes:
postgres_data:
redis_data:
minio_data:
```
### 2. 配置环境变量
```bash
cp .env.example .env
```
`.env.example`:
```bash
# 运行模式:建议将 API 与 Worker 分开运行
IMAGEFORGE_ROLE=api # api | worker
# 服务配置
HOST=0.0.0.0
PORT=8080
PUBLIC_BASE_URL=http://localhost:8080
RUST_LOG=info,imageforge=debug
# 数据库
DATABASE_URL=postgres://imageforge:devpassword@localhost:5432/imageforge
# Redis
REDIS_URL=redis://localhost:6379
# Worker 并发(每个批量任务内同时处理的文件数)
WORKER_CONCURRENCY=4
# 图片处理全局并发(每个 API/Worker 进程)
IMAGE_PROCESSING_CONCURRENCY=4
# 仅在后端端口不对公网开放、请求必经可信代理时启用
TRUST_PROXY_HEADERS=false
# JWT网站/管理后台)
JWT_SECRET=your-super-secret-key-change-in-production
JWT_EXPIRY_HOURS=168
# API Key
API_KEY_PEPPER=please-change-this-in-production
# 存储(当前实现仅支持 local
STORAGE_TYPE=local
STORAGE_PATH=./uploads
# 计费已确认Stripe
BILLING_PROVIDER=stripe
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
# 限制(默认值;最终以套餐/用户覆盖为准)
ALLOW_ANONYMOUS_UPLOAD=true
ANON_MAX_FILE_SIZE_MB=5
ANON_MAX_FILES_PER_BATCH=5
ANON_DAILY_UNITS=10
MAX_IMAGE_PIXELS=40000000
IDEMPOTENCY_TTL_HOURS=24
# 结果保留(匿名默认;登录用户按套餐 retention_days
ANON_RETENTION_HOURS=24
# 管理员初始账户
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=changeme123
```
### 3. 初始化数据库
API 或 Worker 启动时会通过 SQLx 自动、顺序执行 `migrations/` 下尚未应用的迁移。迁移失败时进程会退出,不会在不完整的数据库结构上继续提供服务。
### 4. 启动开发服务器
```bash
# 后端 API (热重载)
cargo install cargo-watch
IMAGEFORGE_ROLE=api cargo watch -x run
# 后端 Worker另一个终端处理异步/批量任务)
IMAGEFORGE_ROLE=worker cargo watch -x run
# 前端 (另一个终端)
cd frontend
npm install
npm run dev
```
### 5. Stripe Webhook 本地调试(可选)
本地调试 Stripe 订阅/支付状态,通常需要将 Stripe Webhook 转发到本机:
```bash
# 1) 安装并登录 Stripe CLI按官方文档
# 2) 监听并转发到你的后端回调地址
stripe listen --forward-to http://localhost:8080/api/v1/webhooks/stripe
# CLI 会输出一个 whsec_...,写入 .env 的 STRIPE_WEBHOOK_SECRET
```
---
## 生产部署
> 注意:以下内容为生产部署模板示例;仓库当前首期仅提供开发用 `docker/docker-compose.dev.yml`,生产 compose/Dockerfile/nginx/k8s 等可在开工阶段按需落地并调整
仓库提供完整的 `docker/Dockerfile``docker/docker-compose.prod.yml`。生产编排包含 API、Worker、PostgreSQL 和 Redis数据库与 Redis 不发布宿主机端口API 和 Worker 使用同一上传卷,并以非 root、只读根文件系统运行
### 方案一Docker Compose推荐小规模
### 环境要求
`docker/docker-compose.prod.yml`:
```yaml
version: '3.8'
- Linux x86_64
- Docker Engine 26+ 与 Docker Compose 2.20+
- 最低 2 核 CPU、4GB 内存;启用 AVIF 和独立 Worker 时建议 4 核、8GB 内存
- 首次构建可访问 Docker Hub 与 crates.io
services:
api:
build:
context: ..
dockerfile: docker/Dockerfile
environment:
- IMAGEFORGE_ROLE=api
- TRUST_PROXY_HEADERS=true
- BILLING_PROVIDER=stripe
- PUBLIC_BASE_URL=https://your-domain.com
- STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}
- STRIPE_WEBHOOK_SECRET=${STRIPE_WEBHOOK_SECRET}
- DATABASE_URL=postgres://imageforge:${DB_PASSWORD}@postgres:5432/imageforge
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET}
- API_KEY_PEPPER=${API_KEY_PEPPER}
- STORAGE_TYPE=local
- STORAGE_PATH=/app/uploads
expose:
- "8080"
volumes:
- uploads:/app/uploads
depends_on:
- postgres
- redis
restart: unless-stopped
Debian 13、4 核 CPU、8GB 内存的实测起始值为 `WORKER_CONCURRENCY=2``IMAGE_PROCESSING_CONCURRENCY=2`。AVIF 是 CPU 密集型编码,不要直接把并发设置为 CPU 核数的数倍。
worker:
build:
context: ..
dockerfile: docker/Dockerfile
environment:
- IMAGEFORGE_ROLE=worker
- DATABASE_URL=postgres://imageforge:${DB_PASSWORD}@postgres:5432/imageforge
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET}
- API_KEY_PEPPER=${API_KEY_PEPPER}
- STORAGE_TYPE=local
- STORAGE_PATH=/app/uploads
volumes:
- uploads:/app/uploads
depends_on:
- postgres
- redis
restart: unless-stopped
### 首次启动
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: imageforge
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: imageforge
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
```bash
cp docker/.env.production.example .env.production
chmod 600 .env.production
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
restart: unless-stopped
# 分别生成 POSTGRES_PASSWORD、JWT_SECRET、API_KEY_PEPPER 和管理员密码。
# hex 不包含数据库 URL 与 .env 需要转义的保留字符。
openssl rand -hex 32
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- api
restart: unless-stopped
# 编辑公开地址和全部 replace-with-* 值后检查配置。
docker compose \
--env-file .env.production \
-f docker/docker-compose.prod.yml \
config --quiet
volumes:
uploads:
postgres_data:
redis_data:
docker compose \
--env-file .env.production \
-f docker/docker-compose.prod.yml \
up -d --build
```
`docker/Dockerfile`:
```dockerfile
FROM rust:1.92-bookworm AS builder
API 健康后 Worker 才会启动,避免两个进程在首次部署时同时执行迁移。
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
cmake \
libdav1d-dev \
nasm \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
COPY Cargo.toml Cargo.lock ./
COPY src ./src
COPY migrations ./migrations
COPY templates ./templates
RUN cargo build --release
# 前端构建阶段
FROM node:20-alpine AS frontend-builder
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend ./
RUN npm run build
# 运行阶段
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libdav1d6 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/target/release/imageforge ./imageforge
COPY --from=frontend-builder /app/frontend/dist ./static
COPY migrations ./migrations
RUN mkdir -p uploads
ENV HOST=0.0.0.0
ENV PORT=8080
EXPOSE 8080
CMD ["./imageforge"]
```bash
docker compose --env-file .env.production -f docker/docker-compose.prod.yml ps
curl --fail http://127.0.0.1:8080/health
```
`docker/nginx.conf`:
预期健康响应:
```json
{"status":"healthy","database":"connected","redis":"connected"}
```
### 更新与回滚
更新代码后保留 `.env.production` 和命名卷,重新构建并滚动重建:
```bash
git pull --ff-only
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
```
生产镜像应使用不可变的 `IMAGEFORGE_TAG`。回滚时把该值改回上一镜像标签,然后再次运行 `up -d`
### 反向代理
直接通过服务器地址访问时保持 `TRUST_PROXY_HEADERS=false`。只有当 8080 端口不对客户端开放、所有请求都经过可信反向代理时,才设置为 `true`,并由代理覆盖 `X-Forwarded-For``X-Forwarded-Proto`
代理至少需要:
```nginx
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
# 日志
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# 文件上传大小限制
client_max_body_size 100M;
# Gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript;
upstream backend {
server api:8080;
}
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name your-domain.com;
ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;
# SSL 配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# 静态文件
location /static/ {
proxy_pass http://backend;
expires 30d;
add_header Cache-Control "public, immutable";
}
# WebSocket
location /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
# API 和其他请求
location / {
proxy_pass http://backend;
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;
}
}
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;
}
```
### 部署步骤
### 日志与备份
```bash
# 1. 创建 .env 文件
cat > .env << EOF
DB_PASSWORD=your-secure-db-password
JWT_SECRET=your-very-long-random-jwt-secret-at-least-32-chars
STRIPE_SECRET_KEY=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
EOF
docker compose --env-file .env.production -f docker/docker-compose.prod.yml logs -f api worker
# 2. 获取 SSL 证书
sudo certbot certonly --standalone -d your-domain.com
# 3. 构建并启动
docker-compose -f docker/docker-compose.prod.yml up -d --build
# 4. 查看日志
docker-compose -f docker/docker-compose.prod.yml logs -f api
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避免数据库记录与文件卷产生时间差。
### 方案二Kubernetes大规模
`k8s/deployment.yaml`:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: imageforge
spec:
replicas: 3
selector:
matchLabels:
app: imageforge
template:
metadata:
labels:
app: imageforge
spec:
containers:
- name: imageforge
image: your-registry/imageforge:latest
ports:
- containerPort: 8080
env:
- name: IMAGEFORGE_ROLE
value: api
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: imageforge-secrets
key: database-url
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: imageforge-secrets
key: redis-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: imageforge-secrets
key: jwt-secret
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: imageforge
spec:
selector:
app: imageforge
ports:
- port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: imageforge
annotations:
kubernetes.io/ingress.class: nginx
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- your-domain.com
secretName: imageforge-tls
rules:
- host: your-domain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: imageforge
port:
number: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: imageforge-worker
spec:
replicas: 2
selector:
matchLabels:
app: imageforge-worker
template:
metadata:
labels:
app: imageforge-worker
spec:
containers:
- name: imageforge-worker
image: your-registry/imageforge:latest
env:
- name: IMAGEFORGE_ROLE
value: worker
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: imageforge-secrets
key: database-url
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: imageforge-secrets
key: redis-url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: imageforge-secrets
key: jwt-secret
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "4Gi"
cpu: "4000m"
```
---
## 监控与日志
### Prometheus 指标
应用暴露 `/metrics` 端点:
```rust
// 在代码中添加指标
use prometheus::{Counter, Histogram};
lazy_static! {
static ref COMPRESSION_REQUESTS: Counter = Counter::new(
"imageforge_compression_requests_total",
"Total number of compression requests"
).unwrap();
static ref COMPRESSION_DURATION: Histogram = Histogram::with_opts(
HistogramOpts::new(
"imageforge_compression_duration_seconds",
"Time spent compressing images"
)
).unwrap();
}
```
### Grafana 仪表板
监控项目:
- 请求量 / QPS
- 响应时间 P50/P95/P99
- 错误率
- 压缩任务队列长度
- CPU / 内存使用率
- 磁盘使用率
### 日志聚合
使用 ELK Stack 或 Loki
```yaml
# docker-compose 添加 Loki
loki:
image: grafana/loki:2.9.0
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
promtail:
image: grafana/promtail:2.9.0
volumes:
- /var/log:/var/log
- ./promtail-config.yml:/etc/promtail/config.yml
command: -config.file=/etc/promtail/config.yml
```
---
## 备份策略
### 数据库备份
## 本地开发
```bash
#!/bin/bash
# backup.sh
cp .env.example .env
docker compose -f docker/docker-compose.dev.yml up -d
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR=/backups
# 终端 1
IMAGEFORGE_ROLE=api cargo run
# PostgreSQL 备份
docker exec postgres pg_dump -U imageforge imageforge | gzip > $BACKUP_DIR/db_$DATE.sql.gz
# 终端 2
IMAGEFORGE_ROLE=worker cargo run
# 保留最近 7 天的备份
find $BACKUP_DIR -name "db_*.sql.gz" -mtime +7 -delete
# 可选:上传到 S3
# aws s3 cp $BACKUP_DIR/db_$DATE.sql.gz s3://your-bucket/backups/
# 终端 3
cd frontend
npm ci
npm run dev
```
添加到 crontab
API 或 Worker 启动时会通过 SQLx 顺序执行尚未应用的迁移。迁移失败时进程退出,不会在不完整的数据库结构上继续提供服务。
## 压缩质量回归
基准工具会下载固定 Picsum 照片,分别生成 JPEG、PNG、WebP、AVIF 输入,再通过真实 HTTP API 测量目标达标率、格式签名、SSIM、PSNR、分辨率和耗时。
```bash
0 3 * * * /path/to/backup.sh
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
```
### 上传文件备份
如果使用本地存储,定期同步到 S3
```bash
aws s3 sync /app/uploads s3://your-bucket/uploads --delete
```
---
`--strict` 会在请求失败、输出格式不匹配或目标体积未达标时返回非零退出码。测试素材与结果位于 `.bench/`,不会提交到 Git。
## 故障排查
### 常见问题
**1. 数据库连接失败**
```bash
# 检查 PostgreSQL 状态
docker-compose logs postgres
# 容器与健康状态
docker compose --env-file .env.production -f docker/docker-compose.prod.yml ps
# 测试连接
docker exec -it postgres psql -U imageforge -d imageforge -c "SELECT 1"
```
# 最近日志
docker compose --env-file .env.production -f docker/docker-compose.prod.yml logs --tail=200 api worker
**2. 压缩失败**
```bash
# 检查应用日志
docker-compose logs api | grep ERROR
docker-compose logs worker | grep ERROR
# 数据库迁移状态
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
```
**3. 内存不足**
```bash
# 查看内存使用
docker stats
# 调整容器内存限制
```
**4. 上传超时**
```bash
# 检查 Nginx 配置
# client_max_body_size 和 proxy_read_timeout
```
### 健康检查端点
```
GET /health
{
"status": "healthy",
"database": "connected",
"redis": "connected",
"storage": "available",
"uptime": 3600
}
```
若图片压缩长时间排队,先检查 CPU再下调 `IMAGE_PROCESSING_CONCURRENCY`;若 API 健康但批量任务不推进,检查 Worker 日志和 Redis 状态。