381 lines
9.8 KiB
Markdown
381 lines
9.8 KiB
Markdown
# Health Check Development Guide
|
||
|
||
本文档只定义部署稳定性需要的健康检查能力。健康检查不承载业务判断,不替代监控告警,也不用于修复故障。进程下线和 drain 细节见 `docs/graceful-shutdown.md`。
|
||
|
||
## Goals
|
||
|
||
健康检查必须解决三个问题:
|
||
|
||
- 负载均衡是否可以把新流量打到这个进程。
|
||
- 发布或重启时,进程是否可以安全下线。
|
||
- 多机部署时,依赖异常是否会被及时摘流,而不是继续接收请求并放大故障。
|
||
|
||
健康检查不解决这些问题:
|
||
|
||
- 不判断房间是否存在。
|
||
- 不判断用户是否有权限。
|
||
- 不扫描全量 MySQL 表。
|
||
- 不做 Redis 修复、房间恢复、outbox 补偿。
|
||
- 不在检查路径里触发业务写入。
|
||
|
||
## Check Types
|
||
|
||
所有服务都必须区分 `live` 和 `ready`。
|
||
|
||
| Type | Meaning | Failure Action |
|
||
| --- | --- | --- |
|
||
| `live` | 进程还活着,事件循环或 gRPC/HTTP runtime 未卡死 | 由容器平台重启进程 |
|
||
| `ready` | 当前进程可以接新流量 | 从负载均衡摘流,不一定重启 |
|
||
|
||
不要把下游短暂不可用放进 `live`。例如 MySQL 抖动时,`room-service` 应该 `ready=false`,而不是 `live=false` 导致所有副本被重启。
|
||
|
||
## Required Interfaces
|
||
|
||
### HTTP Services
|
||
|
||
HTTP 服务暴露两个只读接口:
|
||
|
||
```text
|
||
GET /healthz/live
|
||
GET /healthz/ready
|
||
```
|
||
|
||
适用服务:
|
||
|
||
- `gateway-service`
|
||
- `im-service` 的 HTTP/WebSocket 入口
|
||
|
||
成功响应:
|
||
|
||
```json
|
||
{
|
||
"status": "ok",
|
||
"service": "gateway-service",
|
||
"node_id": "gateway-node-1",
|
||
"time_unix_ms": 1777000000000
|
||
}
|
||
```
|
||
|
||
失败响应:
|
||
|
||
```json
|
||
{
|
||
"status": "fail",
|
||
"service": "gateway-service",
|
||
"node_id": "gateway-node-1",
|
||
"time_unix_ms": 1777000000000,
|
||
"checks": [
|
||
{
|
||
"name": "room_grpc",
|
||
"status": "fail",
|
||
"message": "deadline exceeded"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
状态码规则:
|
||
|
||
- `live` 成功返回 `200`。
|
||
- `live` 失败返回 `500`。
|
||
- `ready` 成功返回 `200`。
|
||
- `ready` 失败返回 `503`。
|
||
|
||
### gRPC Services
|
||
|
||
gRPC 服务必须注册标准 gRPC Health Checking Protocol:
|
||
|
||
```protobuf
|
||
grpc.health.v1.Health
|
||
```
|
||
|
||
适用服务:
|
||
|
||
- `room-service`
|
||
- `wallet-service`
|
||
- `user-service`
|
||
- `activity-service`
|
||
- `im-service` 的内部 gRPC 入口
|
||
|
||
服务名使用完整服务边界名:
|
||
|
||
```text
|
||
gateway-service
|
||
room-service
|
||
im-service
|
||
wallet-service
|
||
user-service
|
||
activity-service
|
||
```
|
||
|
||
状态映射:
|
||
|
||
| gRPC Health Status | Meaning |
|
||
| --- | --- |
|
||
| `SERVING` | ready |
|
||
| `NOT_SERVING` | not ready |
|
||
| `UNKNOWN` | 启动中或检查器未初始化 |
|
||
|
||
本地 Docker 可以用 `grpc-health-probe` 或服务内置 HTTP admin 口做容器 healthcheck。线上优先使用云负载均衡、Kubernetes 或服务发现系统原生的 gRPC health check。
|
||
|
||
## Service Readiness Rules
|
||
|
||
### gateway-service
|
||
|
||
`live`:
|
||
|
||
- HTTP server runtime 正常。
|
||
- 进程没有进入不可恢复 panic 状态。
|
||
|
||
`ready`:
|
||
|
||
- JWT 配置已加载。
|
||
- HTTP listener 已启动。
|
||
- `room-service` gRPC 地址可连接。
|
||
|
||
`gateway-service` 不检查 MySQL、Redis、IM、wallet。它只关心自己能否把客户端 HTTP 命令转到 `room-service`。
|
||
|
||
### room-service
|
||
|
||
`live`:
|
||
|
||
- gRPC server runtime 正常。
|
||
- Room manager 主循环未停止。
|
||
- Outbox worker 主循环未停止。
|
||
|
||
`ready`:
|
||
|
||
- MySQL `PingContext` 在超时时间内成功。
|
||
- Redis `PING` 在超时时间内成功。
|
||
- 当前节点不处于 draining 状态。
|
||
- gRPC listener 已启动。
|
||
- Redis lease 操作能力正常,至少能执行一次轻量读写或 Lua 脚本探测。
|
||
|
||
`wallet-service` 建议作为 `SendGift` 链路的强依赖监控,但不要默认作为 `room-service` 整体 ready 的硬失败条件;否则 wallet 短暂异常会导致房间基础命令也被摘流。
|
||
|
||
`im-service` 不作为 `room-service` ready 的硬依赖。`PublishRoomEvent` 同步失败后由 outbox 补投,不能因为 IM 抖动把房间状态服务整体摘掉。
|
||
|
||
### im-service
|
||
|
||
`live`:
|
||
|
||
- HTTP/WebSocket server runtime 正常。
|
||
- gRPC bridge runtime 正常。
|
||
- Session manager 主循环未停止。
|
||
|
||
`ready`:
|
||
|
||
- HTTP listener 已启动。
|
||
- gRPC listener 已启动。
|
||
- 当前节点不处于 draining 状态。
|
||
- `room-service` gRPC 可连接,用于 `VerifyRoomPresence` 和 `CheckSpeakPermission`。
|
||
|
||
`im-service` 多机部署后,如果在线状态迁移到 Redis 或消息总线,Redis/MQ 必须进入 ready 检查。当前内存版不应假装具备跨节点投递能力。
|
||
|
||
### wallet-service
|
||
|
||
`live`:
|
||
|
||
- gRPC server runtime 正常。
|
||
|
||
`ready`:
|
||
|
||
- gRPC listener 已启动。
|
||
- 未来接入账务数据库后,账务数据库连接必须成功。
|
||
|
||
### user-service
|
||
|
||
`live`:
|
||
|
||
- gRPC server runtime 正常。
|
||
|
||
`ready`:
|
||
|
||
- gRPC listener 已启动。
|
||
- 未来接入用户数据库后,用户数据库连接必须成功。
|
||
|
||
### activity-service
|
||
|
||
`live`:
|
||
|
||
- gRPC server runtime 正常。
|
||
- Consumer 主循环未停止。
|
||
|
||
`ready`:
|
||
|
||
- gRPC listener 已启动。
|
||
- 未来接入 MQ/Redis Stream 后,消费位点存储可访问。
|
||
- 当前节点不处于 draining 状态。
|
||
|
||
## Timeout Budget
|
||
|
||
健康检查必须快失败,不能拖垮服务。
|
||
|
||
| Check | Timeout |
|
||
| --- | ---: |
|
||
| 本进程状态 | `50ms` |
|
||
| HTTP/gRPC listener 自检 | `100ms` |
|
||
| Redis `PING` | `200ms` |
|
||
| MySQL `PingContext` | `300ms` |
|
||
| 下游 gRPC health | `300ms` |
|
||
|
||
单次 `ready` 总耗时建议小于 `500ms`。如果检查项较多,使用并发检查并设置总 deadline。
|
||
|
||
## Docker Compose
|
||
|
||
本地 `docker-compose.yml` 应为每个服务配置 healthcheck。HTTP 服务使用 `curl`,gRPC 服务使用 `grpc-health-probe` 或服务提供的 admin health endpoint。
|
||
|
||
HTTP 示例:
|
||
|
||
```yaml
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:13000/healthz/ready"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 20
|
||
start_period: 10s
|
||
```
|
||
|
||
gRPC 示例:
|
||
|
||
```yaml
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "grpc-health-probe -addr=127.0.0.1:13001 -service=room-service"]
|
||
interval: 5s
|
||
timeout: 3s
|
||
retries: 20
|
||
start_period: 10s
|
||
```
|
||
|
||
`depends_on.condition=service_healthy` 只用于本地编排启动顺序。线上不能依赖 compose 的启动顺序保证服务稳定,必须依赖负载均衡 readiness 和服务自身重试。
|
||
|
||
## Online Deployment
|
||
|
||
多机部署时,每个实例必须有唯一 `node_id`。
|
||
|
||
推荐变量:
|
||
|
||
```text
|
||
SERVICE_NAME=room-service
|
||
NODE_ID=room-prod-az1-001
|
||
HEALTH_READY_TIMEOUT=500ms
|
||
DRAIN_TIMEOUT=30s
|
||
```
|
||
|
||
线上接入规则:
|
||
|
||
- SLB、Kubernetes Service、Nacos、Consul 只把 `ready=true` 的实例放进可用池。
|
||
- 发布新版本时先等待新实例 `ready=true`,再下线旧实例。
|
||
- 下线旧实例时先切 `ready=false`,再进入 drain,最后退出进程。
|
||
- 不允许直接 `kill -9` 正常发布中的服务。
|
||
|
||
## Graceful Shutdown
|
||
|
||
所有服务收到 `SIGTERM` 后执行相同状态机:
|
||
|
||
```text
|
||
running -> draining -> stopped
|
||
```
|
||
|
||
`draining` 要求:
|
||
|
||
- `ready` 立即返回失败。
|
||
- 停止接收新连接或新请求。
|
||
- 等待当前请求完成,超过 deadline 后强制关闭。
|
||
- 关闭前写清必要的部署状态日志。
|
||
|
||
各服务额外要求:
|
||
|
||
| Service | Drain Requirement |
|
||
| --- | --- |
|
||
| `gateway-service` | 停止接新 HTTP 请求,等待已有请求结束 |
|
||
| `room-service` | 停止接管新房间,停止或释放 lease,刷 snapshot,暂停 outbox 当前批次后退出 |
|
||
| `im-service` | 停止接新 WS,向连接发送服务重启关闭码,给客户端重连窗口 |
|
||
| `wallet-service` | 等待当前扣费 RPC 完成,禁止中途截断 |
|
||
| `activity-service` | 完成当前消费消息或安全提交失败状态 |
|
||
|
||
WebSocket 下线建议使用 close code `1012`,表示 service restart。客户端收到后应重连并重新 `auth`、`subscribe_room`。
|
||
|
||
## Room Service Lease Safety
|
||
|
||
`room-service` 的健康检查必须和 Redis lease 下线策略配合。
|
||
|
||
发布或故障时:
|
||
|
||
1. 节点进入 draining。
|
||
2. `ready=false`,负载均衡不再打新请求。
|
||
3. 节点停止获取新的 `room:route:{room_id}` lease。
|
||
4. 已持有 Room Cell 刷 snapshot。
|
||
5. lease 释放或自然过期。
|
||
6. 新节点接到请求后通过 snapshot + command log 恢复。
|
||
|
||
禁止在 `ready=false` 之后继续接管新房间。否则滚动发布时会出现旧节点持续获得新 room lease,导致下线时间不可控。
|
||
|
||
## Observability Fields
|
||
|
||
健康检查日志只在状态变化时输出,不能每 5 秒刷一条成功日志。
|
||
|
||
状态变化日志必须包含:
|
||
|
||
```text
|
||
service
|
||
node_id
|
||
old_status
|
||
new_status
|
||
failed_check
|
||
reason
|
||
duration_ms
|
||
```
|
||
|
||
建议指标:
|
||
|
||
```text
|
||
health_ready_status
|
||
health_live_status
|
||
health_check_duration_ms
|
||
health_dependency_failures_total
|
||
service_draining
|
||
service_shutdown_duration_ms
|
||
```
|
||
|
||
`room-service` 额外指标:
|
||
|
||
```text
|
||
room_lease_check_failures_total
|
||
room_snapshot_flush_failures_total
|
||
room_outbox_worker_alive
|
||
```
|
||
|
||
`im-service` 额外指标:
|
||
|
||
```text
|
||
ws_active_connections
|
||
ws_draining_connections
|
||
room_subscription_count
|
||
```
|
||
|
||
## Implementation Checklist
|
||
|
||
新增或修改服务时,必须检查:
|
||
|
||
- 是否提供 `live`。
|
||
- 是否提供 `ready`。
|
||
- `ready` 是否包含本服务真正的硬依赖。
|
||
- 下游依赖失败是否只影响 `ready`,没有误杀 `live`。
|
||
- 检查是否有总 timeout。
|
||
- 检查路径是否没有业务写入。
|
||
- `SIGTERM` 后是否立即让 `ready=false`。
|
||
- Dockerfile 是否包含 healthcheck 所需工具,或服务是否提供可被容器调用的 health endpoint。
|
||
- `docker-compose.yml` 是否配置 service healthcheck。
|
||
- 线上部署脚本是否等待新实例 ready 后再切流。
|
||
|
||
## Do Not
|
||
|
||
- 不要用业务接口当健康检查,例如 `CreateRoom`、`JoinRoom`、`SendGift`。
|
||
- 不要在健康检查里创建房间、写 command log、写 outbox。
|
||
- 不要把所有下游都塞进每个服务的 ready。
|
||
- 不要把 Redis/MySQL 抖动映射成 `live=false`。
|
||
- 不要让健康检查依赖外网。
|
||
- 不要把健康检查失败日志刷屏。
|
||
- 不要在滚动更新时跳过 drain。
|