--- name: 优雅停机方案 overview: 在 HoTime 框架层实现优雅停机,跨平台(Windows/Linux)支持。收到信号后新请求立即返回 503 让 nginx 切流,等排空后调用 http.Server.Shutdown 等在途请求完成,exit(0) 退出,run_app.cmd 检测正常退出不重启。 todos: - id: framework-core content: hotimev1.5/application.go:新增 shuttingDown/shutdownOnce/DrainTimeout/ShutdownTimeout 字段,改 ServeHTTP,改 Run() 信号 goroutine,改 recover 保护,提取 initiateGracefulShutdown() status: completed - id: framework-windows content: hotimev1.5/signal_windows.go(新建):SetConsoleCtrlHandler 处理 CTRL_CLOSE_EVENT(关窗口) status: completed - id: framework-other content: hotimev1.5/signal_other.go(新建):非 Windows 平台 registerWindowsCloseHandler() 空实现 status: completed - id: nginx-config content: Nginx location 块加 proxy_next_upstream error timeout http_503 和 proxy_next_upstream_tries 2 status: completed - id: cmd-exit-check content: run_app.cmd:检测 EXIT_CODE == 0 时不重启直接退出 status: completed - id: docs content: 新建 hotimev1.5/docs/graceful-shutdown.md 文档 status: completed isProject: false --- # 优雅停机方案 ## 整体流程 ```mermaid sequenceDiagram participant Op as 运维 participant OS as 操作系统 participant App as Go 进程 participant Nginx as Nginx Op->>OS: Ctrl+C / kill / taskkill / 关窗口 OS->>App: SIGINT / SIGTERM / CTRL_CLOSE_EVENT App->>App: shuttingDown = true(sync.Once 保证只执行一次) Note over App,Nginx: 新请求立即返回 503 + Connection:close Nginx->>App: 新请求 App-->>Nginx: 503 Service Unavailable Nginx->>Nginx: proxy_next_upstream http_503 → 转另一台 Note over App: 等 DrainTimeout(默认5s,让nginx完成切流) App->>App: server.Shutdown(ShutdownTimeout=30s) Note over App: 等所有在途请求跑完 App->>OS: os.Exit(0) Note over Op: run_app.cmd 检测 exit 0 → 不重启 ``` ## 信号触发场景对照表 | 操作方式 | 平台 | 信号/事件 | 是否可拦截 | 备注 | |---|---|---|---|---| | Ctrl+C | Windows/Linux | `os.Interrupt` | ✅ | 标准中断 | | `kill ` 或 `kill -15 ` | Linux | `syscall.SIGTERM` | ✅ | 推荐生产关闭方式 | | `taskkill /PID `(无 /F) | Windows | `os.Interrupt` | ✅ | 等价于 Ctrl+C | | 关闭控制台窗口 X 按钮 | Windows | `CTRL_CLOSE_EVENT` | ✅(≤5s 限制)| Windows 给 ~5s 后强杀 | | `taskkill /F /PID ` | Windows | 强制 SIGKILL | ❌ | 无法拦截,强杀 | | `kill -9 ` | Linux | SIGKILL | ❌ | 无法拦截,强杀 | > **关窗口说明**:Windows 在触发 `CTRL_CLOSE_EVENT` 后约 5 秒会强制结束进程。框架对此事件使用更短的 drain(默认 2s)+ shutdown(默认 3s),总计 <5s,可完成正在跑的短接口;长接口(>3s)会被截断。推荐用 `taskkill /PID` 代替关窗口做计划性停机。 --- ## 需要改动/新建的文件 ### 1. [`hotimev1.5/application.go`](d:/work/hotimev1.5/application.go) **`Application` struct** 新增字段: ```go type Application struct { // ...原有字段... shuttingDown atomic.Bool shutdownOnce sync.Once // 保证多路触发只执行一次 DrainTimeout time.Duration // 等 nginx 切流,默认 5s ShutdownTimeout time.Duration // 等在途请求,默认 30s } ``` **`ServeHTTP`** 新增停机检测: ```go func (that *Application) ServeHTTP(w http.ResponseWriter, req *http.Request) { if that.shuttingDown.Load() { w.Header().Set("Connection", "close") http.Error(w, "Service Unavailable", http.StatusServiceUnavailable) return } that.handler(w, req) } ``` **提取 `initiateGracefulShutdown(drain, shutdown time.Duration)` 方法**(被信号 goroutine 和 Windows 关窗 handler 共同调用): ```go func (that *Application) initiateGracefulShutdown(drain, shutdown time.Duration) { that.shutdownOnce.Do(func() { that.shuttingDown.Store(true) that.Log.Infof("优雅停机:等 %v 让 nginx 切流...", drain) time.Sleep(drain) ctx, cancel := context.WithTimeout(context.Background(), shutdown) defer cancel() that.Log.Infof("等进行中请求完成(最多 %v)...", shutdown) if err := that.Server.Shutdown(ctx); err != nil { that.Log.Errorf("优雅停机超时: %v", err) } that.Log.Infof("服务已安全关闭") os.Exit(0) }) } ``` **`Run()`** 加信号监听 goroutine(在启动 ListenAndServe 之后): ```go // 注册 Windows 关窗口处理(Linux 上是空实现) that.registerWindowsCloseHandler() // 跨平台:Ctrl+C / SIGTERM / kill go func() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) <-sigCh drain := that.DrainTimeout if drain == 0 { drain = 5 * time.Second } shutdown := that.ShutdownTimeout if shutdown == 0 { shutdown = 30 * time.Second } that.initiateGracefulShutdown(drain, shutdown) }() ``` **`defer recover`** 加停机保护: ```go defer func() { if err := recover(); err != nil { if that.shuttingDown.Load() { return // 停机中的 panic 不触发重启 } that.Log.Warnf("%v", err) that.Run(router) } }() ``` 新增 import:`"context"`, `"os/signal"`, `"sync"`, `"sync/atomic"`, `"syscall"` --- ### 2. `hotimev1.5/signal_windows.go`(新建) ```go //go:build windows package hotime import ( "time" "golang.org/x/sys/windows" ) func (that *Application) registerWindowsCloseHandler() { windows.SetConsoleCtrlHandler(func(ctrl uint32) bool { // CTRL_CLOSE_EVENT=2 CTRL_LOGOFF_EVENT=5 CTRL_SHUTDOWN_EVENT=6 if ctrl == 2 || ctrl == 5 || ctrl == 6 { that.initiateGracefulShutdown(2*time.Second, 3*time.Second) // 阻塞此回调,让 initiateGracefulShutdown 内的 os.Exit(0) 执行 // Windows 约 5s 后强杀,这里最多等 6s 兜底 time.Sleep(6 * time.Second) return true } return false }, true) } ``` > `golang.org/x/sys` 已在 `xbc/go.mod` 中作为间接依赖存在(v0.29.0),无需新增依赖。 --- ### 3. `hotimev1.5/signal_other.go`(新建) ```go //go:build !windows package hotime func (that *Application) registerWindowsCloseHandler() {} ``` --- ### 4. Nginx 配置(在已有 location 块中追加) ```nginx # 让后端返回 503 时自动切换到另一台(优雅停机核心) proxy_next_upstream error timeout http_503; proxy_next_upstream_tries 2; ``` 已有的 `max_fails=1 fail_timeout=3s` 负责连接级失败(强杀后的端口拒绝),两者互补。 --- ### 5. [`run_app.cmd`](d:/work/your-app/run_app.cmd) 在 `goto run_app` 前插入 exit code 判断: ```cmd :: 优雅退出 (exit 0) 不重启 if %EXIT_CODE% EQU 0 ( echo [%date% %time%] Graceful shutdown. Not restarting. >> "%LOG_FILE%" echo. echo [优雅停机] 服务已安全关闭,不再自动重启。 pause exit /b 0 ) ``` --- ### 6. `hotimev1.5/docs/graceful-shutdown.md`(新建) 包含:原理说明、场景对照表、nginx 配置、滚动重启步骤、参数说明(DrainTimeout/ShutdownTimeout)。 --- ## 滚动重启操作步骤(Windows) ``` # 1. 找到端口 9998 的进程 PID netstat -ano | findstr 9998 # 2. 优雅关闭该实例(不加 /F) taskkill /PID # 3. 等日志出现 "服务已安全关闭" 后再部署新版并启动 # 4. 再对 9999 执行同样操作 ``` ## 滚动重启操作步骤(Linux) ```bash # 找 PID pgrep xbc # 或 ps aux | grep xbc # 优雅关闭 kill # 发 SIGTERM,触发优雅停机 # 或 kill -2 # 发 SIGINT,等同 Ctrl+C # 等日志出现 "服务已安全关闭" 再部署新版 ```