Files
hotime/docs/GracefulShutdown_优雅停机.md
2026-07-23 06:05:00 +08:00

146 lines
4.6 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.
# HoTime 框架优雅停机
## 概述
HoTime 框架内置优雅停机支持,收到关闭信号后:
1. **立即**对所有新请求返回 `503 Service Unavailable`,触发 nginx 把流量切到其他实例
2. **等待** `DrainTimeout`(默认 5 秒)让 nginx 完成流量切换
3. **等待**所有正在处理的请求完成(最多 `ShutdownTimeout`,默认 30 秒)
4.`exit(0)` 正常退出;若使用进程守护脚本(如 `run_app.cmd`),可检测正常退出后不自动重启
---
## 触发方式对照表
| 操作 | 平台 | 是否可优雅停机 | 说明 |
|---|---|:---:|---|
| `Ctrl+C` | Windows / Linux | ✅ | 发送 SIGINT / os.Interrupt |
| `kill <pid>``kill -15 <pid>` | Linux | ✅ | **推荐**,发送 SIGTERM |
| `kill -2 <pid>` | Linux | ✅ | 等同 Ctrl+C,发送 SIGINT |
| `taskkill /PID <pid>`(不加 /F | Windows | ✅ | **推荐**,等同发送 Ctrl+C |
| 关闭控制台/exe 窗口 X 按钮 | Windows | ✅(≤5s | CTRL_CLOSE_EVENTWindows 约 5s 后强杀 |
| `taskkill /F /PID <pid>` | Windows | ❌ | 强制 SIGKILL,无法拦截 |
| `kill -9 <pid>` | Linux | ❌ | SIGKILL,无法拦截 |
> **关窗口的限制**Windows 系统在触发 CTRL_CLOSE_EVENT 后约 5 秒会强制杀死进程。
> 框架对此路径使用更短的 drain(2s) + shutdown(3s),总计 <5s。
> 如果有接口执行时间超过 3 秒,建议改用 `taskkill /PID` 做计划性停机,不受 5s 限制。
---
## 配置参数
`Application` 实例上设置(需在调用 `Run()` 前配置):
```go
appIns := Init("config/config.json")
// 可选:自定义超时,不设置则使用默认值
appIns.DrainTimeout = 5 * time.Second // 等 nginx 切流,默认 5s
appIns.ShutdownTimeout = 30 * time.Second // 等在途请求完成,默认 30s
appIns.Run(Router{ ... })
```
---
## Nginx 配置
`location` 块中添加以下两行,让后端返回 `503` 时 nginx 自动转到其他实例,客户端无感知:
```nginx
upstream api_backend {
least_conn;
keepalive 64;
server 127.0.0.1:9998 max_fails=1 fail_timeout=3s;
server 127.0.0.1:9999 max_fails=1 fail_timeout=3s;
}
server {
location /api/ {
proxy_pass http://api_backend;
# 优雅停机核心配置:后端返回 503 时自动切换到另一台
proxy_next_upstream error timeout http_503;
proxy_next_upstream_tries 2;
# 其他常规配置...
proxy_set_header Host $host;
# EdgeOne 场景优先 EO-Connecting-IP;空则框架再降级到 XFF / RemoteAddr
proxy_set_header X-Real-IP $http_eo_connecting_ip;
proxy_set_header EO-Connecting-IP $http_eo_connecting_ip;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
```
- `max_fails=1 fail_timeout=3s`:连接级失败(如强杀后端口拒绝)时标记下线 3s
- `proxy_next_upstream http_503`:应用级 503 时 nginx 自动重试下一台
---
## 滚动重启步骤
### Windows
```cmd
:: 1. 查找端口 9998 的进程 PID
netstat -ano | findstr :9998
:: 2. 优雅关闭该实例(不加 /F,发送 Ctrl+C 信号)
taskkill /PID <pid>
:: 3. 观察日志,出现"服务已安全关闭"后,部署新版并启动
:: 若使用 run_app.cmd 守护,检测到 exit(0) 后不会自动重启
:: 4. 对端口 9999 重复以上步骤
netstat -ano | findstr :9999
taskkill /PID <pid2>
```
### Linux
```bash
# 1. 查找进程 PID(按你的二进制名替换)
pgrep -a <your-app> # 或
ps aux | grep <your-app>
# 2. 优雅关闭(发送 SIGTERM
kill <pid>
# 3. 观察日志确认关闭完成
tail -f logs/app.log # 等待"服务已安全关闭"
# 4. 部署新版并启动,再对另一个实例重复
```
---
## 工作原理
```
收到信号 / 关窗口
shuttingDown = true
▼ DrainTimeout(默认 5s
新请求 → 503 ──────────────────────► nginx 切流到另一台
server.Shutdown(ShutdownTimeout=30s)
等待所有进行中请求跑完
os.Exit(0) → 守护脚本(如 run_app.cmd)可检测 exit(0) 后不重启
```
### 为何在框架层而非应用层实现
通过 `appIns.Run(Router{...})` 启动服务,信号处理与 `http.Server` 生命周期强绑定,必须在框架的 `Run()` 方法内才能正确管理。应用层无需任何修改,升级框架即自动获得优雅停机能力。
### `sync.Once` 保证幂等性
框架同时监听多路触发源(信号 goroutine + Windows 关窗口 handler),`sync.Once` 保证无论哪路先触发,`initiateGracefulShutdown` 只执行一次,不会重复调用 `Shutdown()`