feat: optimize compression and production deployment
This commit is contained in:
@@ -272,9 +272,9 @@ Idempotency-Key: <key> # 建议
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|---|---|---:|---|
|
||||
| `file` | File | 是 | 图片文件 |
|
||||
| `compression_rate` | Integer | 否 | 压缩率 1-100(压缩后体积占原图比例,数值越小压缩越强,100 表示不压缩),优先级高于 `level` |
|
||||
| `compression_rate` | Integer | 否 | 压缩率 1-100;JPEG/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-100;JPEG/WebP/AVIF 以该比例为体积上限,无损格式为尽力优化,100 表示不压缩;优先级高于 `level` |
|
||||
| `level` | String | 否 | `high` / `medium` / `low`(兼容参数) |
|
||||
| `output_format` | String | 否 | 输出格式:`png/jpeg/webp/avif/gif/bmp/tiff/ico`(默认保持原格式) |
|
||||
| `preserve_metadata` | Boolean | 否 | 是否保留元数据(默认 `false`) |
|
||||
|
||||
@@ -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 状态。
|
||||
|
||||
Reference in New Issue
Block a user