hyapp-server/docs/graceful-shutdown.md
2026-04-25 01:18:30 +08:00

12 KiB
Raw Blame History

Graceful Shutdown Development Guide

本文档定义服务下线、重启、滚动发布时必须遵守的进程生命周期规则。目标是让实例退出时不再接新流量,不截断关键请求,不留下错误 lease不制造重复消费。

Scope

适用范围:

  • gateway-service
  • room-service
  • im-service
  • wallet-service
  • user-service
  • activity-service
  • room-service outbox worker
  • 后续所有 consumer worker

不在本文档内解决:

  • 不设计业务补偿逻辑。
  • 不定义房间命令语义。
  • 不替代健康检查文档。
  • 不依赖人工在发布时手动清理状态。

健康检查规则见:

docs/health-checks.md

Shutdown State Machine

所有服务必须实现相同状态机:

starting -> running -> draining -> stopped

状态含义:

State Meaning
starting 进程启动中,不应该接正式流量
running 进程 ready可以接新请求或新连接
draining 进程正在下线,必须拒绝新流量并处理已有任务
stopped listener、worker、连接池已经关闭

进入 draining 后必须立即让 ready=falselive 是否继续成功取决于进程是否仍健康;正常下线期间 live 可以继续成功。

Signal Handling

所有服务必须监听:

SIGTERM
SIGINT

处理要求:

  1. 第一次收到信号,进入 draining
  2. 第二次收到信号,可以加速退出,但必须打印未完成任务数量。
  3. 超过 shutdown_timeout 后允许强制关闭,但必须记录被截断的资源类型。

推荐默认值:

Config Default
shutdown_timeout 30s
http_drain_timeout 20s
grpc_drain_timeout 20s
ws_drain_timeout 30s
worker_drain_timeout 30s
room_snapshot_flush_timeout 10s
outbox_flush_timeout 10s

这些配置必须允许线上按服务覆盖。room-serviceim-service 不建议使用过短 timeout。

Required Order

标准下线顺序:

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 下线顺序:

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 校验和接管协议。

im-service

im-service 持有真实 WebSocket 连接,下线时重点是停止接新连接,并给已有连接重连窗口。

进入 draining 后必须:

  • ready=false
  • HTTP/WebSocket listener 停止 accept 新连接。
  • 新的 WS upgrade 请求返回失败或服务重启语义。
  • 已有 WS 连接发送关闭帧,建议 close code 1012
  • 关闭帧原因使用稳定机器可读值,例如 service_restart
  • 给客户端留出重连时间窗口。
  • 已进入投递流程的消息在 deadline 内完成当前发送或安全放弃。
  • 如果消息需要 ACK未 ACK 的 peer_messagesystem_notice 不能标记已完成。

建议 WS 关闭流程:

1. mark draining
2. reject new websocket upgrades
3. broadcast service_restart close intent to local connections
4. wait for clients to close or timeout
5. close remaining sockets
6. stop session manager

多机部署注意:

  • 如果在线状态在 Redis关闭本机连接时必须删除或过期本机 session 记录。
  • 如果房间订阅在共享存储,必须释放本节点订阅记录。
  • 不允许让已关闭连接继续被其他节点当作在线目标。

禁止行为:

  • 直接杀掉进程导致客户端只看到 TCP reset。
  • 下线期间继续接受新的 subscribe_room
  • 对未投递成功的 ACK 消息写成已送达。

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、成功数、失败数、未知数。

推荐事件状态:

pending -> delivering -> delivered
pending -> delivering -> retryable
pending -> delivering -> failed

状态更新要求:

  • delivered 只能在下游确认成功后写入。
  • retryable 必须保留下一次投递所需 payload。
  • failed 只用于不可重试错误,并且要有人工或补偿路径。

禁止行为:

  • 进程退出时把 delivering 全部改成 delivered
  • 未确认成功就删除 outbox 记录。
  • 同一 worker 关闭时继续抢新批次。

Database And Redis Closing

依赖关闭顺序必须靠后。

推荐顺序:

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 本地建议:

stop_signal: SIGTERM
stop_grace_period: 45s

线上建议:

terminationGracePeriodSeconds >= shutdown_timeout + 10s
preStop: mark instance not ready, wait load balancer drain window

发布系统顺序:

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

下线日志只记录状态变化和关键资源结果,不要刷每秒心跳。

必须记录:

service
node_id
signal
state
ready
inflight_requests
active_ws_connections
active_room_cells
outbox_batch_size
shutdown_deadline_ms
duration_ms
result

room-service 额外记录:

room_id
lease_token
snapshot_flush_result
outbox_flush_result
command_inflight

im-service 额外记录:

conn_id
user_id
close_code
close_reason

Metrics

建议指标:

service_draining
service_shutdown_duration_ms
service_shutdown_forced_total
inflight_requests
grpc_inflight_requests
http_inflight_requests

room-service

room_cells_draining
room_snapshot_flush_total
room_snapshot_flush_failed_total
room_lease_release_total
room_lease_release_failed_total
outbox_shutdown_pending

im-service

ws_connections_draining
ws_shutdown_close_total
ws_shutdown_force_close_total

Worker

worker_inflight_messages
worker_shutdown_released_messages
worker_shutdown_committed_messages

Test Requirements

每个服务至少覆盖:

  • 收到 SIGTERMready=false
  • draining 后不再接受新请求或新连接。
  • in-flight 请求能在 deadline 内完成。
  • 超过 deadline 后会强制退出并记录日志。
  • 依赖关闭顺序正确。

gateway-service

  • 构造一个慢 HTTP handler确认 shutdown 等待已有请求。

room-service

  • 构造持有 Room Cell 的进程,发送 shutdown确认停止获取新 lease。
  • 确认 snapshot flush 被调用。
  • 确认 outbox 当前批次不会被错误删除。

im-service

  • 建立 WS 连接后发送 shutdown确认收到 1012 service_restart
  • 确认 shutdown 后新的 WS upgrade 被拒绝。

Worker

  • 构造投递中消息shutdown 后状态必须是 deliveredretryable 或保持可重试,不能丢失。

Do Not

  • 不要用 kill -9 做正常发布。
  • 不要在 SIGTERM 后继续接新请求。
  • 不要在 ready=false 后继续接管新 room lease。
  • 不要先关闭依赖再 drain 请求。
  • 不要在 shutdown 里做大范围数据修复。
  • 不要删除状态未知的 outbox 事件。
  • 不要把消费者位点提交放在业务处理之前。