Files
hair/docs/监控探活接口.md
2026-10-15 23:04:07 +08:00

282 lines
13 KiB
Markdown
Raw Permalink 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.
# 监控探活接口
外网监控机器用这个接口判断**当前对外服务能不能接业务**。不正常就报警。监控侧每 30 秒调用一次,网关不做主动推送。
对外地址:
```text
GET https://hair.xiangsilian.com/monitor
```
本机等价地址是 `GET http://127.0.0.1:8080/monitor`。nginx 对 `hair.xiangsilian.com` 的 `location /` 整段反代到网关 8080,所以这条路径不用单独改 nginx。
---
## 1. 为什么不复用原来的 `/health`
网关上原来就有两个探活,都不能直接拿来报警:
| 路径 | 谁在用 | HTTP 状态 | 实际含义 |
|------|--------|-----------|----------|
| `GET /health` | 旧路径,和下面这条相同 | 永远 200 | 网关进程还活着 |
| `GET /gateway-health` | 人看池子概况 | 永远 200 | 同上,body 里带 worker 计数 |
| worker 的 `GET /health` | 网关自己每 8 秒打各台 GPU 机 | 200 或 503 | 那一台 worker 自己是否就绪 |
`/health` 和 `/gateway-health` 只要 uvicorn 进程在,就返回 `"status": "ok"`。五台 worker 全部下线时,HTTP 仍然是 200,只是 `workers_healthy` 变成 0。只看状态码的监控不会报警。
这次加的 `/monitor` 专供外部监控:
- 至少一台 worker 处于在线(`online`)→ **HTTP 200**
- 一台在线的都没有 → **HTTP 503**
- 报警演练打开时,即使还有在线 worker,也返回 **HTTP 503**(见第 9 节)
监控只需要判断状态码。200 正常,非 200(503、连接失败、超时)报警。
---
## 2. 调用约定
| 项目 | 约定 |
|------|------|
| 方法 | `GET` |
| 路径 | `/monitor` |
| 鉴权 | 不需要。不校验 `X-Internal-Token` |
| 请求体 | 无 |
| 建议间隔 | 30 秒 |
| 超时 | 建议 5 秒。接口本身只读内存,正常几十毫秒内返回 |
| 请求日志 | 不记录。网关请求日志只记 `/api/`,这条不会进 `request_log.jsonl`,也不会出现在 `/admin/stats` |
30 秒一次不会给 worker 增加探测。`/monitor` 不访问 GPU 机,只读网关内存里已经维护好的池状态。真正打 worker `/health` 的是网关后台循环,间隔见 `gateway/config.json` 的 `health_check.interval_seconds`(当前 8 秒)。
---
## 3. 怎样算「正常」
正常的定义是:**网关当前还能把业务请求派到至少一台 worker**。
池里每台 worker 有一个 `online` 标志。`workers_healthy` 就是 `online == true` 的台数。这个标志由后台健康检查维护,规则和派发用的是同一份状态:
1. 启动时立刻对配置里的每台 worker 打一次 `GET {worker}/health`(带 `X-Internal-Token`,超时 `health_check.timeout_seconds`,当前 3 秒)。返回 200 且 `healthy_threshold <= 1`(当前就是 1)则标记在线。
2. 之后每 8 秒再探一轮。
3. 连续失败达到 `unhealthy_threshold`(当前 2 次)才从在线改成下线。单次抖动不会下线。
4. 业务转发失败(连不上、超时、HTTP ≥ 500、401)会立刻把那台 worker 标下线,并进入隔离期 `cooldown_seconds`(当前 60 秒)。隔离期内即使 `/health` 恢复 200 也不重新上线。
5. 隔离期结束后,再有一次探测 200 才重新上线。
因此 `/monitor` 看到的「在线」,就是下一笔生发/测量请求实际可以派过去的机器,不是「进程还在」这么宽的概念。
### 会返回 200 的情况
- 主 worker 里还有任意一台在线。当前配置有 3 台主 worker。
- 主 worker 全挂,但备用 worker 还有在线的。主备分组下,主组全下线之后流量会打到备用,服务仍然能接,所以不报警。body 里 `workers_primary_healthy` 会是 0,需要看主组是否掉光时看这个字段,不要用状态码。
- 在线的 worker 全部正在忙。忙只说明队列里有请求,机器没有被摘掉。等某台 worker 空闲最多 `queue_wait_seconds`(当前 30 秒);全局并发槽排满时最多再等 `max_queue_wait_seconds`(当前 590 秒)。这不是宕机。
### 会返回 503 的情况
- 配置里的 worker 全部不在线。包括:探测连续失败被摘除、转发失败进入隔离期、启动后第一轮探测就没有一台返回 200。
- 池还没初始化好(进程刚起来、`_workers` 仍为空)。这种情况窗口很短,发生在 uvicorn 完成启动之前;启动完成前端口还不会接受连接,监控更多会看到连接失败,而不是这条 503。
网关进程本身挂了、nginx 挂了、机器网络不通,监控连不上,同样应当按失败报警。那不是这个接口返回的 503,而是连接错误。
---
## 4. 响应
`Content-Type: application/json`。
### 200 正常
```json
{
"status": "ok",
"service": "gateway",
"workers_total": 5,
"workers_healthy": 3,
"workers_primary_total": 3,
"workers_primary_healthy": 3
}
```
### 503 不正常
```json
{
"status": "unhealthy",
"service": "gateway",
"workers_total": 5,
"workers_healthy": 0,
"workers_primary_total": 3,
"workers_primary_healthy": 0,
"message": "没有可用的 worker"
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| status | string | `ok` 或 `unhealthy`。与 HTTP 状态码一致:`ok` 对应 200,`unhealthy` 对应 503 |
| service | string | 固定 `gateway` |
| workers_total | int | 配置里的 worker 总数 |
| workers_healthy | int | 当前在线台数。未开演练时,大于 0 返回 200,等于 0 返回 503 |
| workers_primary_total | int | 主 worker 台数。未配置 `primary_workers` 时,全部 worker 都视为主 worker |
| workers_primary_healthy | int | 主 worker 里在线的台数。为 0 且 `workers_healthy > 0` 表示只剩备用在撑 |
| message | string | 仅 HTTP 503 时出现。真故障文案是「没有可用的 worker」;演练文案以「模拟故障」开头 |
| simulated | bool | 仅演练打开时出现,值为 `true`。用来区分演练 503 和真故障 |
不返回 worker 地址、密码、队列深度。地址和鉴权信息留在网关日志和 `gateway/config.json` 里。
---
## 5. 监控侧建议
按 **HTTP 状态码** 报警,不要只解析 body。
| 结果 | 处理 |
|------|------|
| 200 | 正常 |
| 503 | 报警。body 的 `message` 可放进告警内容 |
| 连接失败、超时,以及其他 5xx | 报警。网关或 nginx 不可达 |
建议连续 **2 次**失败再发告警(约 60 秒)。网关重启时端口会有几秒不可用(`hair-gateway.service` 的 `RestartSec=3`,启动时还要等第一轮 worker 探测完成)。只看单次失败会在重启时误报。
不需要带任何请求头。不要把这条 URL 配成 POST,也不要把它当成业务接口去看 `code` 字段——这里没有业务信封 `{code, message, request_id, data}`。
报警延迟,从「最后一台可用 worker 真的挂了」到监控看到 503,大致是两段相加:
- 网关摘除:后台每 8 秒探一次,要连续失败 2 次才下线,大约 8~16 秒。若是转发失败触发的被动摘除,则立刻下线。
- 监控拉取:最多再等一个 30 秒周期。
所以典型告警延迟大约半分钟到一分钟,不是秒级。
---
## 6. 和业务接口的关系
worker 全下线时,走 GPU 的接口(测量、生发、发际线、遮罩等)都会失败,`/monitor` 返回 503。这是要报警的情况。
有两个接口在网关本机执行,不经过 worker 池:
- 接口 4 `POST /api/v1/face/features`(豆包)
- 接口 99 `POST /api/v1/face/special-points`(本机 CPU)
这两条在 worker 全挂时仍可能成功。`/monitor` 不管它们。它表示的是「依赖 worker 池的对外服务是否还能接」,不是「网关进程里每一个路由都活着」。
---
## 7. 已知边界
这些情况 `/monitor` **不会**变成 503,和派发逻辑一致,避免把「还能干活」报成故障:
- 只挂了一部分 worker,还剩至少一台在线。
- 主 worker 全挂,备用仍在线。
- 在线 worker 都在忙,请求在网关排队。排队超时是业务返回「后端服务繁忙」,不是这个探活的失败。
- worker 的 `/health` 仍返回 200,但机器实际上不能生图。当前仓库里 worker 的 `/health` 只看模型是否加载完成(未就绪才 503),不检查磁盘水位,也不检查 ComfyUI、change_hair 是否真的能出图。2026-09-19 那次磁盘写满时,浅探活会一直 200,网关就不会把机器摘掉,`/monitor` 也会一直 200。worker 侧更深的探活不在本接口范围内。
另外,备用 worker 如果从启动起就探不通,日志里不一定有「下线」警告。下线警告只在「原本在线、随后连续失败」时打印。一直没上线的机器只体现在 `workers_total` 和 `workers_healthy` 的差额里。
---
## 8. 实现与生效
| 项 | 位置 |
|----|------|
| 路由 | `gateway/app.py` 的 `GET /monitor`、`POST /monitor/fault` |
| 判定 | 同文件 `monitor_payload()`:未开演练时 `workers_healthy > 0` 为正常;演练打开则一律 503 |
| 池状态 | `gateway/pool.py` 的 `get_pool_status()`,统计 `online` 台数 |
| 单测 | `tests/test_gateway_monitor.py` |
改的是网关代码。worker 不用发版。改完后要重启网关进程才会加载新代码:
```bash
sudo systemctl restart hair-gateway
systemctl is-active hair-gateway
curl -sS -D - http://127.0.0.1:8080/monitor
curl -sS -D - https://hair.xiangsilian.com/monitor
```
配置(worker 列表、探测间隔、下线阈值、隔离期)仍在 `gateway/config.json`,启动时加载一次。只改配置、不改代码,同样要重启 `hair-gateway`。
`/health` 与 `/gateway-health` 保持原样,继续永远返回 200,给「进程是否存活」这类检查用。外部报警请只挂 `/monitor`。
---
## 9. 故障模拟(报警演练)
`POST /monitor/fault` 用来演练监控会不会报警。打开之后,`GET /monitor` 固定返回 503;worker 池、派发、业务接口都不变。测完必须关掉,否则监控会一直报。
开关只活在网关进程内存里。重启 `hair-gateway` 后自动回到关闭。
### 请求
```bash
curl -sS -X POST https://hair.xiangsilian.com/monitor/fault \
-H "Content-Type: application/json" \
-H "X-Internal-Token: <gateway/config.json 里的 shared_password>" \
-d '{"enabled": true}'
```
| 项目 | 约定 |
|------|------|
| 方法 | `POST` |
| 路径 | `/monitor/fault` |
| 鉴权 | 请求头 `X-Internal-Token`,值与 `gateway/config.json` 的 `shared_password` 相同。缺失或错误返回 401 |
| Body | JSON 布尔。`{"enabled": true}` 打开,`{"enabled": false}` 关闭。字符串 `"true"`、数字 `1` 都算参数错误,返回 400 |
打开成功:
```json
{"status": "ok", "simulated": true, "message": "已开启模拟故障,GET /monitor 将返回 503"}
```
关闭成功:
```json
{"status": "ok", "simulated": false, "message": "已关闭模拟故障,GET /monitor 恢复真实状态"}
```
### 演练期间 `/monitor` 的 503
计数字段仍是真实池状态,多一个 `simulated: true`,用来和「真的没有 worker」区分。若演练期间在线数本身已经是 0,文案会同时写明「当前没有可用的 worker」,避免把真故障误读成纯演练。
在线数仍大于 0 时:
```json
{
"status": "unhealthy",
"service": "gateway",
"workers_total": 5,
"workers_healthy": 3,
"workers_primary_total": 3,
"workers_primary_healthy": 3,
"simulated": true,
"message": "模拟故障:监控测试中,真实 worker 未摘除"
}
```
监控若只看 HTTP 状态码,这次 503 会报警,和真故障同一条链路。告警内容里如果带了 body,可以看到 `simulated` 和「模拟故障」。
### 建议步骤
1. 确认当前 `GET /monitor` 是 200。
2. `POST /monitor/fault`,`enabled` 设为 `true`。
3. 再拉一次 `/monitor`,应为 503,且 `workers_healthy` 没有变成 0。
4. 等监控的 30 秒周期(若配了连续 2 次失败才告警,就等两次)。确认告警发出。
5. `enabled` 设为 `false`。再拉 `/monitor`,应回到 200。确认告警恢复。
演练时业务请求仍会打到在线 worker。这个接口不能用来模拟磁盘满、ComfyUI 挂掉或某台机器被摘除。
---
## 10. 上线核对(2026-10-15)
`hair-gateway.service` 已重启并完成启动。启动日志:`Worker 池初始化完成 | 总数=5 | 在线=3 | 主worker=3`。三台主 worker 的 `/health` 均为 200;两台备用在这轮启动探测里没有上线,所以健康数是 3 不是 5。按上面的规则这仍是正常。
当时公网实查:
```text
HTTP/1.1 200 OK
{"status":"ok","service":"gateway","workers_total":5,"workers_healthy":3,"workers_primary_total":3,"workers_primary_healthy":3}
```
旧的 `GET /health` 仍是 200。重启前 8080 上没有已建立连接。
故障模拟在同一次上线里用本机请求核对过:不带令牌或令牌错误返回 401,且开关保持关闭;`enabled: true` 之后 `GET /monitor` 为 503,`simulated` 为 true,`workers_healthy` 仍是 3;`enabled: false` 之后回到 200。核对结束时开关是关闭的。开关的开、关会在 `journalctl -u hair-gateway` 里各留一条 warning,不进业务请求日志。