单机部署(小型 / POC)

适用:日均订单 < 1k、模板 < 100、用户素材 < 100GB。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────── 单机 ───────────────────────────┐
│ │
│ Nginx (80/443) │
│ ├── / → pf-app (静态) │
│ ├── /editor/ → pf-editor (静态) │
│ ├── /manage/ → pf-manage (静态) │
│ ├── /api/ → pf-service:3000 │
│ └── /face/ → pf-face-service:3010 │
│ │
│ pf-service:3000 (Bun process, systemd unit) │
│ pf-face-service:3010 (uvicorn, systemd unit) │
│ │
│ MySQL:3306 │
│ Redis:6379 │
│ MinIO:1280 (S3) + 9001 (Console) │
│ │
└────────────────────────────────────────────────────────────┘

3 个 systemd 单元 + 3 个 docker compose 服务,部署成本最低。

分布式部署(生产推荐)

适用:日均订单 ≥ 10k、需要稳定的合成吞吐与高可用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
               ┌────────────────────┐
│ CDN / 对象存储 │ ← 用户素材 / 成品对外回源
└─────────┬──────────┘

┌─────────▼──────────┐
│ 反向代理 (LB) │ Nginx / Caddy / ALB
│ TLS 终止 + WAF │
└─────────┬──────────┘

┌───────────┬───────┼─────────┬──────────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────────┐ ┌─────────────────────┐
│ pf-app │ │pf-editor│ │ pf-manage │ │ pf-service (API) │
│ 静态托管│ │ 静态托管│ │ 静态托管 │ │ N 个 Bun 进程 │
└─────────┘ └─────────┘ └─────────────┘ │ - 无状态 │
│ - 水平扩展 │
└─────────┬───────────┘

┌───────────────────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────────┐ ┌────────────────────┐ ┌──────────────────────┐
│ pf-service (Worker) │ │ pf-face-service │ │ MinIO (S3 集群) │
│ 合成专用进程组 │ │ N 个副本 │ │ 多节点 / Erasure │
│ + Puppeteer 实例池 │ └────────────────────┘ └──────────────────────┘
└─────────┬───────────┘

┌─────────▼──────────┐ ┌──────────────────┐
│ MySQL (主从) │ │ Redis (Cluster)│
└────────────────────┘ └──────────────────┘

关键拆分点

1. API 进程 vs 合成 Worker

pf-service 是同一份代码,但部署时建议拆成两组进程

进程组 作用 config.server.jobs.composition.enabled
API 组 服务 HTTP 请求,水平扩展 false
Worker 组 orderTemplateCompositionJob,独占 Puppeteer true

避免合成进程的 CPU/IO 抖动影响 HTTP 响应延迟。

2. Puppeteer 进程隔离

Worker 进程内 Puppeteer 容易出 OOM / 僵尸进程。建议:

  • 每个 Worker 持有 1–2 个 Browser 长连接
  • 每条合成任务用独立 Page,处理完即关
  • 进程内累计渲染 N 次后 Browser 重启(避免内存泄漏累积)
  • 容器化时给 Worker 单独的 cgroup(CPU/内存上限),互不挤占

3. 对象存储

MinIO 单节点适合开发;生产建议:

  • 多节点 MinIO(Erasure Code 4+2 起)或直接用 S3 / OSS / COS
  • endpoint 改成对外 CDN 域名时,注意分预签名(内部直传)和对外读取(带签名 URL 或公开桶)两条路径
  • 冷热分离:30 天后的成品迁到 STANDARD_IA / 归档存储

4. 数据库

  • 主写从读:模板列表、订单列表都偏读
  • 备份:mysqldump 每日 + binlog 实时
  • 长尾大表:order_template_submission_items 增长最快,按 createdAt 月份分表/归档

5. Redis

  • 缓存:字体/分类/模板元数据(命中率高、变更频率低)
  • 任务队列(如果你把合成调度切到队列模式):Streams / List
  • 会话:JWT 黑名单
  • 部署成 Cluster 或 Sentinel,单点 Redis 是生产隐患

静态前端发布

三个 Quasar/Vite 工程都是构建产物:

1
2
3
bun run --filter @wangijun/pf-app build      # apps/pf-app/dist/spa
bun run --filter @wangijun/pf-editor build # apps/pf-editor/dist/spa
bun run --filter soybean-admin build # apps/pf-manage/dist

把对应 dist/ 目录推到 Nginx / 对象存储 + CDN 即可。注意:

  • 配置 pf-service 域名注入到前端构建(一般通过 .env.production 或 build-time 替换)
  • SPA 路由:Nginx try_files $uri /index.html;
  • 上 CDN 时区分版本目录或带 hash 文件名

反向代理示例(Nginx)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
server {
listen 443 ssl http2;
server_name pf.example.com;

# 用户端 H5
location / {
root /var/www/pf-app;
try_files $uri /index.html;
}

# 后端 API
location /api/ {
proxy_pass http://pf_service_upstream/;
proxy_set_header Host $host;
proxy_read_timeout 120s;
client_max_body_size 220m; # 单图 ≤ 200MB
}

# 人脸检测(也可只走内网,不暴露外网)
location /face/ {
proxy_pass http://pf_face_service_upstream/;
proxy_read_timeout 30s;
}
}

upstream pf_service_upstream {
server pf-service-1:3000;
server pf-service-2:3000;
keepalive 64;
}

systemd 单元示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# /etc/systemd/system/pf-service.service
[Unit]
Description=Printing Factory Service
After=network.target

[Service]
Type=simple
User=pf
WorkingDirectory=/opt/printing-factory/apps/pf-service
ExecStart=/usr/local/bin/bun run src/index.ts
Restart=on-failure
RestartSec=5
Environment="NODE_ENV=production"

[Install]
WantedBy=multi-user.target
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# /etc/systemd/system/pf-face-service.service
[Unit]
Description=Printing Factory Face Service
After=network.target

[Service]
Type=simple
User=pf
WorkingDirectory=/opt/printing-factory/apps/pf-face-service
ExecStart=/usr/local/bin/uv run uvicorn app.main:app --host 0.0.0.0 --port 3010
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

容器化要点

  • 镜像分层:依赖层(bun install / uv sync)单独打 → 业务层只拷源码
  • Puppeteer 依赖 Chromium 系统库(libnss3 / libatk 等),用官方 node/oven/bun 镜像时要补
  • 人脸服务镜像需要 OpenCV 系统库(libgl1libglib2.0-0
  • 健康检查走 /health,K8s readinessProbe + livenessProbe

监控与告警

最少接入这些指标,再考虑后续观测能力:

指标 来源 告警阈值参考
HTTP 5xx 率 反向代理 > 1% 持续 5min
API P95 延迟 pf-service > 1s
合成 pending 队列长度 DB query / 指标埋点 > 100 持续 10min
合成失败率 DB query > 5%
合成单条耗时 P95 指标埋点 > 30s
Puppeteer 进程内存 容器指标 > 限额 80%
MinIO 可用空间 exporter < 20%
face-service /health 探针 非 ok 任何时长

安全 checklist

  • pf-service 与 pf-face-service 只暴露给内部代理
  • MinIO 私有桶 + 预签名 URL,禁止公开列举
  • JWT 密钥 / S3 密钥放 secret manager,不入仓
  • CORS 白名单生产收紧
  • 上传白名单:MIME + 文件头双重校验
  • Puppeteer 不渲染外网任意 URL,只渲染受信内嵌页

下一步