hyapp-server/docs/graceful-shutdown.md
2026-04-29 23:07:50 +08:00

399 lines
11 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.

# Graceful Shutdown Development Guide
本文档定义服务下线、重启和重新部署时必须遵守的进程生命周期规则。目标是让实例退出时不再接新流量,不截断关键请求,不留下错误 lease不制造重复消费。
## Scope
适用范围:
- `gateway-service`
- `room-service`
- `wallet-service`
- `user-service`
- `activity-service`
- `room-service` outbox worker
- 后续所有 consumer worker
不在本文档内解决:
- 不设计业务补偿逻辑。
- 不定义房间命令语义。
- 不替代健康检查文档。
- 不依赖人工在发布时手动清理状态。
健康检查规则见:
```text
docs/health-checks.md
```
## Shutdown State Machine
所有服务必须实现相同状态机:
```text
starting -> running -> draining -> stopped
```
状态含义:
| State | Meaning |
| --- | --- |
| `starting` | 进程启动中,不应该接正式流量 |
| `running` | 进程 ready可以接新请求或新连接 |
| `draining` | 进程正在下线,必须拒绝新流量并处理已有任务 |
| `stopped` | listener、worker、连接池已经关闭 |
进入 `draining` 后必须立即让 `ready=false``live` 是否继续成功取决于进程是否仍健康;正常下线期间 `live` 可以继续成功。
## Signal Handling
所有服务必须监听:
```text
SIGTERM
SIGINT
```
处理要求:
1. 第一次收到信号,进入 `draining`
2. 第二次收到信号,可以加速退出,但必须打印未完成任务数量。
3. 超过 `shutdown_timeout` 后允许强制关闭,但必须记录被截断的资源类型。
推荐默认值:
| Config | Default |
| --- | ---: |
| `shutdown_timeout` | `30s` |
| `http_drain_timeout` | `20s` |
| `grpc_drain_timeout` | `20s` |
| `worker_drain_timeout` | `30s` |
| `room_snapshot_flush_timeout` | `10s` |
| `outbox_flush_timeout` | `10s` |
这些配置必须允许线上按服务覆盖。`room-service` 不建议使用过短 timeout因为它要完成 Room Cell snapshot、command log 和 outbox 安全收尾。
## Required Order
标准下线顺序:
```text
1. mark draining
2. readiness returns false
3. stop accepting new requests/connections/work
4. wait for in-flight work
5. flush or release service-owned state
6. close listeners and clients
7. exit
```
不能先关闭 MySQL、Redis、gRPC client再等待业务 drain。依赖提前关闭会导致正在处理的请求无法提交或无法释放状态。
## gateway-service
`gateway-service` 是 HTTP JSON 入口,优雅下线重点是停止接新请求并等待正在处理的 HTTP 请求结束。
进入 `draining` 后必须:
- `GET /healthz/ready` 返回 `503`
- 停止从负载均衡接新流量。
- HTTP server 调用 graceful shutdown不再 accept 新连接。
- 已经进入 handler 的请求继续执行,直到完成或 deadline 到期。
- 给下游 gRPC 调用保留原有请求 timeout不因为进程下线取消所有已开始请求。
允许行为:
- 对新连接返回连接关闭或 `503`
- 对超过 drain deadline 的请求返回 `503` 或上下文取消错误。
禁止行为:
- 收到 `SIGTERM` 后直接 `os.Exit`
- 在 handler 未完成时关闭 room-service gRPC client。
- 继续通过 readiness 接收新请求。
## room-service
`room-service` 是房间状态 owner下线时必须保护 Room Cell、Redis lease、snapshot、command log 和 outbox。
进入 `draining` 后必须:
- `ready=false`,让负载均衡摘流。
- 停止接管新房间。
- 停止创建新的 Room Cell。
- 停止获取新的 `room:route:{room_id}` lease。
- 对已经进入处理中的命令,允许在 deadline 内完成提交。
- 命令提交后必须完成必要 command log、snapshot dirty 标记和 outbox 写入。
- 对本节点持有的 Room Cell 执行 snapshot flush。
- 停止 lease 续约,或按实现安全释放 lease。
- outbox worker 处理完当前批次,未处理事件保持在 MySQL 中等待其他 worker 或下次启动补投。
Room Cell 下线顺序:
```text
1. cell stops accepting new commands
2. mailbox drains current command
3. command log/outbox transaction completes
4. snapshot flush runs with timeout
5. lease renew stops
6. cell is removed from local manager
```
如果 shutdown timeout 到期:
- 不能删除未安全释放的持久状态。
- 不能清空 outbox。
- 可以让 Redis lease 自然过期,由其他节点通过 snapshot + command log 恢复。
禁止行为:
- `ready=false` 后继续接管新 room lease。
- 还没刷 snapshot 就强制清空本地 Room Cell。
- outbox 投递失败后删除 outbox 记录。
- shutdown 时把 Redis route 直接改到另一个节点,除非有明确的 lease token 校验和接管协议。
## Tencent Cloud IM
客户端长连接、房间群消息、公屏、单聊、离线和漫游由腾讯云 IM 承载,本仓库不再部署自研 `im-service`,因此没有本地 WebSocket drain。
本仓库下线时只处理两个边界:
- `gateway-service` 不再接收 `/api/v1/im/usersig` 新请求,但已经进入 handler 的 UserSig 签发可以在 HTTP drain deadline 内完成。
- `room-service` 已提交的房间系统事件如果同步推腾讯云 IM 失败,必须保留在 `room_outbox`,由后续 worker 补投。
客户端重连策略由腾讯云 IM SDK 驱动。服务端不能假装能控制腾讯云连接关闭码,只能通过业务快照、房间系统事件和客户端重进群保证恢复。
## wallet-service
`wallet-service` 下线重点是不能截断扣费 RPC。
进入 `draining` 后必须:
- `ready=false`
- gRPC server 停止接新 RPC。
- 已开始的扣费 RPC 在 deadline 内完成。
- 如果未来接入账务数据库,必须先完成事务提交或回滚,再关闭 DB 连接。
禁止行为:
- 关闭数据库连接后再等待扣费 RPC。
- 对未知提交状态的扣费请求返回成功。
## user-service
`user-service` 当前是基础服务骨架,下线重点是保持 gRPC 生命周期一致。
进入 `draining` 后必须:
- `ready=false`
- 停止接新 gRPC 请求。
- 等待 in-flight 查询结束。
- 最后关闭外部依赖连接。
## activity-service
`activity-service` 未来会消费 room outbox 或 MQ 事件。下线重点是消费位点安全。
进入 `draining` 后必须:
- `ready=false`
- 停止拉取新消息。
- 当前正在处理的消息必须完成以下任一结果:
- 成功处理并提交消费位点。
- 失败并不提交消费位点,等待后续重试。
- 明确写入失败状态,交给补偿逻辑处理。
禁止行为:
- 消息还没处理完就提交消费位点。
- 处理成功后因进程退出丢失位点提交。
- 同一消息在本地并发处理多次。
## Outbox Worker
Outbox worker 的下线必须保证“不会丢事件,可以接受重复投递”。
进入 `draining` 后必须:
- 停止扫描新的 outbox 批次。
- 当前批次逐条完成投递或释放。
- 投递成功且确认后,才能更新 outbox 状态。
- 投递失败或状态未知时,保留 outbox 记录为可重试状态。
- 退出前记录当前 batch size、成功数、失败数、未知数。
推荐事件状态:
```text
pending -> delivering -> delivered
pending -> delivering -> retryable
pending -> delivering -> failed
```
状态更新要求:
- `delivered` 只能在下游确认成功后写入。
- `retryable` 必须保留下一次投递所需 payload。
- `failed` 只用于不可重试错误,并且要有人工或补偿路径。
禁止行为:
- 进程退出时把 `delivering` 全部改成 `delivered`
- 未确认成功就删除 outbox 记录。
- 同一 worker 关闭时继续抢新批次。
## Database And Redis Closing
依赖关闭顺序必须靠后。
推荐顺序:
```text
1. stop listeners
2. stop accepting new work
3. drain in-flight requests
4. flush snapshots/outbox/consumer offsets
5. close Redis clients
6. close MySQL pools
7. exit process
```
Redis 注意:
- lease 释放必须校验 `lease_token`
- 不能删除不属于自己的 `room:route:{room_id}`
- 无法确认 owner 时,让 lease 自然过期比错误删除更安全。
MySQL 注意:
- 不要在事务未提交时关闭连接池。
- shutdown 时不做 schema migration。
- shutdown 时不跑大范围清理 SQL。
## Docker And Deployment
Dockerfile 或部署系统必须给进程发送 `SIGTERM`,不能默认直接杀死。
Docker Compose 本地建议:
```yaml
stop_signal: SIGTERM
stop_grace_period: 45s
```
线上建议:
```text
terminationGracePeriodSeconds >= shutdown_timeout + 10s
preStop: mark instance not ready, wait load balancer drain window
```
发布系统顺序:
```text
1. start new instance
2. wait new instance ready=true
3. mark old instance draining
4. remove old instance from load balancer
5. wait drain timeout
6. stop old process
```
## Logging
下线日志只记录状态变化和关键资源结果,不要刷每秒心跳。
必须记录:
```text
service
node_id
signal
state
ready
inflight_requests
active_room_cells
outbox_batch_size
shutdown_deadline_ms
duration_ms
result
```
`room-service` 额外记录:
```text
room_id
lease_token
snapshot_flush_result
outbox_flush_result
command_inflight
```
不再记录本地 `conn_id``close_code``close_reason`。腾讯云 IM 连接诊断应通过腾讯云侧日志、客户端 SDK 回调和服务端 REST API 错误日志关联。
## Metrics
建议指标:
```text
service_draining
service_shutdown_duration_ms
service_shutdown_forced_total
inflight_requests
grpc_inflight_requests
http_inflight_requests
```
`room-service`
```text
room_cells_draining
room_snapshot_flush_total
room_snapshot_flush_failed_total
room_lease_release_total
room_lease_release_failed_total
outbox_shutdown_pending
```
Worker
```text
worker_inflight_messages
worker_shutdown_released_messages
worker_shutdown_committed_messages
```
## Test Requirements
每个服务至少覆盖:
- 收到 `SIGTERM``ready=false`
- `draining` 后不再接受新请求或新连接。
- in-flight 请求能在 deadline 内完成。
- 超过 deadline 后会强制退出并记录日志。
- 依赖关闭顺序正确。
`gateway-service`
- 构造一个慢 HTTP handler确认 shutdown 等待已有请求。
`room-service`
- 构造持有 Room Cell 的进程,发送 shutdown确认停止获取新 lease。
- 确认 snapshot flush 被调用。
- 确认 outbox 当前批次不会被错误删除。
Worker
- 构造投递中消息shutdown 后状态必须是 `delivered``retryable` 或保持可重试,不能丢失。
## Do Not
- 不要用 `kill -9` 做正常发布。
- 不要在 `SIGTERM` 后继续接新请求。
- 不要在 `ready=false` 后继续接管新 room lease。
- 不要先关闭依赖再 drain 请求。
- 不要在 shutdown 里做大范围数据修复。
- 不要删除状态未知的 outbox 事件。
- 不要把消费者位点提交放在业务处理之前。