Files
hair/docs/系统架构-网关与高性能后端.md
T
xslandClaude Opus 4.8 147fef6ca6 docs(网关): 补全 base64→url 映射表(接口1/2/3/5) + 嵌套数组/超时提醒
接口 1/2/3/5 已真实实现,补齐网关需要的字段映射,供网关开发参考:
- 网关任务书 §6:完整映射表(annotated_image / results[].image / results[].grown_image /
  best_hairline_image / hair_growth_image / hairline_images[].image);推荐"凡 *_base64 递归改写"
  通用实现;可空字段(生发图)保留 null;gender 等入参网关透传无需改造
- 网关任务书 §9:接口4 仍 mock;生发接口(2/3) ComfyUI 同步出图慢,request_timeout 调大≥120s
- 架构 §9:标注两个易漏点(数组内字段需递归、生发图可空)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 00:29:33 +08:00

266 lines
15 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.
# 系统架构:外网网关 + 高性能后端(worker)
> 本文档描述「旷视五接口」的部署架构拆分。**接口文档(对外契约)完全不变**——客户端看到的 URL、请求/响应结构、错误码 1001–1008 全部保持原样。本文只改变内部如何处理这些请求。
---
## 1. 背景与目标
外网服务器(`hair.xiangsilian.com`)性能不足以跑 MediaPipe + BiSeNet + 标注图生成等重逻辑。因此拆分为两层:
- **外网网关(gateway)**:保持 HTTPS 对外接口不变,**自身不跑算法**,只做反向代理、健康检查、负载分发、鉴权、标注图托管。资源占用极小,可继续跑在现有外网机。
- **高性能后端(worker**:独立机器,**GPU + 32G 内存**,跑真正的算法逻辑。通过 `http://hair.xiangsilian.com:28187` 这类 `host:port` 暴露。
**核心诉求**
1. 对外接口与文档零变化。
2. 网关可配置**多个 worker URL**,周期探测可用性,只把请求发给健康的 worker。
3. **每个 worker 并发 = 1**(一次处理一个请求);多 worker 即可并发,**总并发 = 健康 worker 数**。
---
## 2. 拓扑总览
```
HTTPS (对外,接口文档不变)
┌────────┐ :443 ┌───────────────────────────┐
│ 客户端 │ ───────▶ │ 外网网关 gateway │
└────────┘ │ hair.xiangsilian.com │
│ - 反向代理 5 个接口 │
│ - 健康检查 worker 池 │
│ - 空闲 worker 派发(并发=worker数)│
│ - 共享密码鉴权 │
│ - 标注图 base64→落盘→URL │
│ - 无可用后端→1007 │
└───────┬───────────┬─────────┘
HTTP + 密码头 │ │
┌───────────────────────┘ └─────────────┐
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ worker #1 │ ...(可配置多个)... │ worker #N │
│ :28187 GPU/32G │ │ :xxxxx GPU/32G │
│ 跑完整 app.py │ │ 跑完整 app.py │
│ + face_analysis │ │ + face_analysis │
│ 并发=1 │ │ 并发=1 │
└──────────────────┘ └──────────────────┘
```
---
## 3. 职责划分
| 能力 | 网关 gateway | worker |
|------|:---:|:---:|
| 对外 HTTPS、接口契约 | ✅ | ✗ |
| 反向代理 5 个接口 | ✅ | ✗ |
| 算法(MediaPipe/BiSeNet/测量/标注图) | ✗ | ✅ |
| 入参校验(大小/格式/分辨率/人脸) | ✗(透传) | ✅ |
| worker 健康检查 + 池管理 | ✅ | 提供 `/health` |
| 负载分发(挑空闲 worker) | ✅ | ✗ |
| 鉴权(共享密码) | 发送密码 | 校验密码 |
| 标注 PNG 落盘 + 对外 URL | ✅ | 返回 base64 |
| `/static/*` 静态托管 | ✅ | ✗ |
| GPU | 不需要 | ✅ |
> **原则**:所有业务逻辑只在 worker 实现一份(worker 跑的就是完整 `app.py` + `face_analysis`)。网关是无状态的薄层,除了"健康池 + worker 忙闲状态"外不持有业务状态。
---
## 4. 网关配置文件
后端 URL 列表、共享密码、各项参数集中在一个配置文件,运维手动维护(密码定期轮换)。
`gateway/config.json`(示例):
```json
{
"workers": [
"http://hair.xiangsilian.com:28187",
"http://10.0.0.12:28187"
],
"shared_password": "REPLACE_ME_ROTATE_PERIODICALLY",
"accept_passwords": ["REPLACE_ME_ROTATE_PERIODICALLY"],
"health_check": {
"path": "/health",
"interval_seconds": 8,
"timeout_seconds": 3,
"unhealthy_threshold": 2,
"healthy_threshold": 1
},
"dispatch": {
"per_worker_concurrency": 1,
"queue_wait_seconds": 30,
"request_timeout_seconds": 60,
"retry_on_failure": true,
"max_retries": 1
}
}
```
字段说明:
- `workers`:worker 基址列表,手动增删。**增加一个就多一路并发**。
- `shared_password`:网关调用 worker 时发送的密码。
- `accept_passwords`:worker 端用(见 §7)——允许的密码列表,**轮换期可同时放新旧两个**,实现不停机改密码。网关侧也可放在 worker 的独立配置里。
- `health_check`:探测周期、超时、连续失败几次判定下线、连续成功几次判定上线。
- `dispatch.per_worker_concurrency`**固定为 1**(当前约束)。
- `dispatch.queue_wait_seconds`:所有 worker 都忙时请求最多排队多久,超时返回 1007。
- `request_timeout_seconds`:单次转发到 worker 的超时。
- `retry_on_failure` / `max_retries`:worker 转发失败时是否换一个 worker 重试。
> 配置变更后网关需 reload(可做成监听文件变更热加载,或重启网关进程)。
---
## 5. 健康检查
网关后台**周期轮询**每个 worker 的 `/health`(已存在于 `app.py`,排除在 OpenAPI 之外):
- 间隔 `interval_seconds`,每次超时 `timeout_seconds`
- 连续失败 `unhealthy_threshold` 次 → 标记**下线**,停止派发。
- 重新连续成功 `healthy_threshold` 次 → 标记**上线**,恢复派发。
- 健康池 = 当前在线的 worker 集合。
**主动检查 + 被动剔除**双保险:除周期探测外,转发请求时若 worker 连接失败/超时,立即把它标记为不健康并(按配置)换一个 worker 重试。
> worker 的 `/health` 建议返回 **模型就绪状态**:只有 MediaPipe / BiSeNet 权重都加载完成才返回 200,否则返回 503——避免请求被派发到尚未热好的 worker。
---
## 6. 负载分发与并发
**每个 worker 并发 = 1**,所以分发逻辑很简单:
1. 网关维护每个健康 worker 的**忙/闲**状态。
2. 新请求到来:从健康池里选**一个空闲 worker**,标记为忙,转发;收到响应(或失败)后标记为闲。
3. 若当前**无空闲 worker**(全忙):请求进入**队列等待**,直到有 worker 空闲或等待超过 `queue_wait_seconds`(超时返回 1007)。
4. 若**健康池为空**(无可用后端):直接返回 1007。
- **总并发能力 = 健康 worker 数量**。加机器即扩并发。
- 选空闲 worker 的策略:任意(如先到先得 / 轮转空闲),并发=1 下无需最少连接数算法。
---
## 7. 鉴权(共享密码)
`:28187` 在公网可直接访问,必须鉴权防止他人直接打 worker。
- **网关 → worker**:每个转发请求带密码头,如 `X-Internal-Token: <shared_password>`
- **worker 校验**worker 侧配置文件持有 `accept_passwords` 列表,收到请求校验 `X-Internal-Token` 是否在列表内;不匹配返回 HTTP 401,**不进入业务逻辑**。
- **轮换**:运维改密码时,先把新密码加进 worker 的 `accept_passwords`(此时新旧都接受)→ 再把网关 `shared_password` 切到新值 → 确认无旧密码流量后从 worker 移除旧密码。全程不停机。
- 密码**仅存配置文件**,不写日志、不进 git(配置文件加入 `.gitignore`,仓库只放 `config.example.json`)。
> 建议叠加防火墙:worker 防火墙只放行网关来源 IP(纵深防御)。密码是应用层兜底。
---
## 8. 单次请求处理流程
```
客户端 ──(HTTPS, multipart/json, 接口文档原样)──▶ 网关
├─ 1. 选一个空闲健康 worker(无则排队/1007
├─ 2. 原样转发请求体 + 加 X-Internal-Token 头
│ ──(HTTP)──▶ worker
│ ├─ 校验密码(失败→401,网关视为该 worker 异常)
│ ├─ 跑完整业务(校验/MediaPipe/BiSeNet/测量/标注图)
│ └─ 返回标准信封;图片字段以 base64 形式(见 §9)
├─ 3. 收到 worker 响应,标记该 worker 空闲
├─ 4. 若响应含 base64 图片 → 落盘到网关 /static/annotations/{uuid}.png
│ → 把字段改写成对外 URL(见 §9)
└─ 5. 把最终(符合接口文档的)响应返回客户端
```
- 网关**不解析也不校验**业务入参,原样透传(worker 负责全部校验与错误码)。worker 返回的 1001–1008 由网关**透传**给客户端。
- 网关只在「无后端/排队超时」时**自行**返回 1007。
---
## 9. 标注图处理(base64 → 落盘 → URL 改写)★关键
接口文档里多个接口返回图片 URL(接口1 标注图、接口3 标记图、接口5 发际线图)。拆分后:
1. **worker 不落盘、不拼 URL**,而是把生成的 PNG 以 **base64** 放进响应的约定字段返回给网关。
- 约定:worker 用 `*_base64` 字段承载图片,例如接口1 返回 `annotated_image_base64` 而非 `annotated_image_url`
2. **网关收到后**:对每个 `*_base64` 字段——
- 解码 → 保存到网关本地 `static/annotations/{uuid}.png`
- 删除该 base64 字段,新增对应的 `*_url` 字段,值为 `https://hair.xiangsilian.com/static/annotations/{uuid}.png`
3. 客户端最终看到的字段名/URL **与接口文档完全一致**(如 `annotated_image_url`)。
> **完整字段映射表见 [`网关-开发任务书.md`](网关-开发任务书.md) §6**(接口1/2/3/5 全部 `*_base64`→`*_url`,以 `接口文档.md` 为准)。
> ⚠️ 两个易漏点:
> 1. **数组里的图片字段**:接口2 `results[].image_base64`/`results[].grown_image_base64`、接口5
> `hairline_images[].image_base64` 在数组元素内——改写逻辑要**递归进数组**(建议「凡 key 以 `_base64`
> 结尾就改写」的通用递归,自动覆盖嵌套与未来新增字段)。
> 2. **可空字段**:接口2/3 的生发图(ComfyUI 未起/失败时)`*_base64` 为 **null** → 保留 null,不落盘。
>
> 代价:内部 HTTP 多传一份 base64(图片放大约 1.33×)。worker↔网关若跨网络,单图 ~100KB–1MB 量级,可接受。
> ⚠️ 生发接口(2/3)经 ComfyUI 同步出图较慢(接口2 ~18s、接口3 ~6s),网关转发**超时要调大(≥120s)**。
>
> 静态文件清理:网关 `static/annotations/` 会持续增长,需加定期清理(按时间或容量,定时任务),与拆分前同样的问题,由网关侧负责。
---
## 10. 错误处理
| 场景 | 返回 | 由谁 |
|------|------|------|
| 业务错误(无人脸/分辨率/非正面/超大/格式…) | 10011008(原样) | worker 产生,网关透传 |
| **无可用 worker / 全忙排队超时** | **1007 系统错误** | 网关 |
| worker 转发失败(连接/超时/5xx/401) | 按配置换 worker 重试;重试耗尽 → 1007 | 网关 |
- 复用 **1007**(系统错误)表示「基础设施层不可用」,**不新增错误码**,接口文档不动。
- 局限:调用方无法从错误码区分"算法失败"与"后端全挂"(都是 1007)。如需区分,可在 `message` 文案上体现(如"后端服务暂不可用,请稍后重试"),但 `code` 保持 1007。
---
## 11. worker 侧相对单机方案的变化
worker 跑的就是「接口1 技术实现方案」里描述的完整逻辑,但有几处因拆分/硬件而变:
1. **GPU 加速**worker 有独立 GPUBiSeNet 改用 **CUDA** 推理(torch GPU 版),比 CPU 快很多;MediaPipe 仍 CPUPython solutions API 仅 CPU)。
2. **不落盘、返回 base64**:标注图相关接口的 handler 改为返回 `*_base64`,不再保存到本地 `/static`、不拼 URL(改由网关做,见 §9)。
3. **`/health` 反映就绪**:模型加载完才返回 200。
4. **鉴权中间件**worker 增加一个校验 `X-Internal-Token` 的中间件/依赖(见 §7)。
5. **资源宽裕**:32G 内存 + GPU,无需单机方案里 2核4G 的并发限制与降级开关;但 worker 自身仍是**并发=1**(由网关保证,不向 worker 发并发请求;worker 可不做内部并发控制,但建议 uvicorn 单 worker 进程以省显存)。
> 单机方案文档(接口1 技术实现方案)描述的「方案A/B、虹膜标定、标注图、误差验证」全部不变,只是运行位置从外网机挪到 GPU worker,并启用 GPU。
---
## 12. 部署
### 网关(外网机,hair.xiangsilian.com
- 新增 gateway 应用(轻量 FastAPI/asgi 代理)。
- nginx 把 443 → 网关进程;网关再转发到 worker 池。
- 托管 `/static/*`(标注图落盘目录)。
- 配置文件 `gateway/config.json`(含 worker 列表 + 密码)。
- systemd 管理网关进程。
### workerGPU 机,:28187
- 部署完整 `app.py` + `face_analysis` + 模型权重(见 `OFFLINE_ASSETS.md`)。
- torch 用 **GPU 版**CUDA),其余依赖同单机方案。
- uvicorn 监听 `0.0.0.0:28187`(仅经防火墙放行网关)。
- 配置文件持有 `accept_passwords`
- systemd 管理 worker 进程;`/health` 供网关探测。
---
## 13. 安全注意
- `:28187` **公网可达 + HTTP 明文**:密码头会明文走公网。**强烈建议**给 worker 也套一层 TLSworker 前置 nginx 终止 HTTPS,或网关↔worker 走内网/VPN/IP 白名单),否则密码可被中间人嗅探。
- 若短期内只能 HTTP,务必靠**防火墙 IP 白名单**把 worker 限制为只接受网关来源,密码作为应用层兜底。
- 密码不入 git、不写日志。
- 网关对转发的请求体大小设上限(如 ≤2MB),防止被超大 body 拖垮。
---
## 14. 待确认 / 后续
1. **worker↔网关传输是否加 TLS**:当前 §13 标为强烈建议。若运维能给 worker 配 HTTPS 或限定内网,安全性更好。请确认部署条件。
2. **worker host:port 形态**`hair.xiangsilian.com:28187` 是端口转发到 GPU 机,还是 GPU 机直接持有该域名?影响防火墙与 TLS 方案。
3. **多 worker 的物理分布**:是否都在同一内网?若跨公网,base64 图片传输与密码明文风险都需重新评估。
4. **静态图清理策略**:网关 `static/annotations/` 的保留时长 / 清理触发条件。
---
> **文档版本**: v1.0 **创建日期**: 2026-06-14 配套:接口1 技术方案 v2.0 / 开发任务书 / OFFLINE_ASSETS.md
> **关键约束**: 接口文档不变;每 worker 并发=1;无后端→1007;标注图 base64→网关落盘→URL