2026-04-14 14:07:59 +08:00
|
|
|
|
# HoTime 框架优雅停机
|
|
|
|
|
|
|
|
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
|
|
HoTime 框架内置优雅停机支持,收到关闭信号后:
|
|
|
|
|
|
|
|
|
|
|
|
1. **立即**对所有新请求返回 `503 Service Unavailable`,触发 nginx 把流量切到其他实例
|
|
|
|
|
|
2. **等待** `DrainTimeout`(默认 5 秒)让 nginx 完成流量切换
|
|
|
|
|
|
3. **等待**所有正在处理的请求完成(最多 `ShutdownTimeout`,默认 30 秒)
|
2026-07-13 07:45:51 +08:00
|
|
|
|
4. 以 `exit(0)` 正常退出;若使用进程守护脚本(如 `run_app.cmd`),可检测正常退出后不自动重启
|
2026-04-14 14:07:59 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 触发方式对照表
|
|
|
|
|
|
|
|
|
|
|
|
| 操作 | 平台 | 是否可优雅停机 | 说明 |
|
|
|
|
|
|
|---|---|:---:|---|
|
|
|
|
|
|
| `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_EVENT,Windows 约 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;
|
2026-07-23 06:05:00 +08:00
|
|
|
|
# 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;
|
2026-04-14 14:07:59 +08:00
|
|
|
|
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. 观察日志,出现"服务已安全关闭"后,部署新版并启动
|
2026-07-13 07:45:51 +08:00
|
|
|
|
:: 若使用 run_app.cmd 守护,检测到 exit(0) 后不会自动重启
|
2026-04-14 14:07:59 +08:00
|
|
|
|
|
|
|
|
|
|
:: 4. 对端口 9999 重复以上步骤
|
|
|
|
|
|
netstat -ano | findstr :9999
|
|
|
|
|
|
taskkill /PID <pid2>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Linux
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-07-13 07:45:51 +08:00
|
|
|
|
# 1. 查找进程 PID(按你的二进制名替换)
|
|
|
|
|
|
pgrep -a <your-app> # 或
|
|
|
|
|
|
ps aux | grep <your-app>
|
2026-04-14 14:07:59 +08:00
|
|
|
|
|
|
|
|
|
|
# 2. 优雅关闭(发送 SIGTERM)
|
|
|
|
|
|
kill <pid>
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 观察日志确认关闭完成
|
2026-07-13 07:45:51 +08:00
|
|
|
|
tail -f logs/app.log # 等待"服务已安全关闭"
|
2026-04-14 14:07:59 +08:00
|
|
|
|
|
|
|
|
|
|
# 4. 部署新版并启动,再对另一个实例重复
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 工作原理
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
收到信号 / 关窗口
|
|
|
|
|
|
│
|
|
|
|
|
|
▼
|
|
|
|
|
|
shuttingDown = true
|
|
|
|
|
|
│
|
|
|
|
|
|
▼ DrainTimeout(默认 5s)
|
|
|
|
|
|
新请求 → 503 ──────────────────────► nginx 切流到另一台
|
|
|
|
|
|
│
|
|
|
|
|
|
▼
|
|
|
|
|
|
server.Shutdown(ShutdownTimeout=30s)
|
|
|
|
|
|
等待所有进行中请求跑完
|
|
|
|
|
|
│
|
|
|
|
|
|
▼
|
2026-07-13 07:45:51 +08:00
|
|
|
|
os.Exit(0) → 守护脚本(如 run_app.cmd)可检测 exit(0) 后不重启
|
2026-04-14 14:07:59 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 为何在框架层而非应用层实现
|
|
|
|
|
|
|
2026-07-13 07:45:51 +08:00
|
|
|
|
通过 `appIns.Run(Router{...})` 启动服务,信号处理与 `http.Server` 生命周期强绑定,必须在框架的 `Run()` 方法内才能正确管理。应用层无需任何修改,升级框架即自动获得优雅停机能力。
|
2026-04-14 14:07:59 +08:00
|
|
|
|
|
|
|
|
|
|
### `sync.Once` 保证幂等性
|
|
|
|
|
|
|
|
|
|
|
|
框架同时监听多路触发源(信号 goroutine + Windows 关窗口 handler),`sync.Once` 保证无论哪路先触发,`initiateGracefulShutdown` 只执行一次,不会重复调用 `Shutdown()`。
|