Files
hotime/docs/graceful-shutdown.md
T
hoteas 66f18a084a feat(app): 实现优雅停机功能
- 新增 shuttingDown 原子布尔标志和 shutdownOnce 保证单次执行
- 添加 DrainTimeout 和 ShutdownTimeout 配置参数,默认 5s 和 30s
- 修改 ServeHTTP 拦截新请求返回 503 Service Unavailable
- 实现跨平台信号处理:Windows 关窗口和 Linux SIGTERM/SIGINT
- 添加 recover 保护避免停机过程中 panic 触发重启
- 优化 HTTP 服务退出判断,排除正常关闭情况
- 新增 log.LoggerNoCaller 用于访问日志等无意义调用方场景
- 更新 README 文档增加优雅停机特性说明和文档链接
- 创建详细的优雅停机文档和测试用例
- 添加 Windows 平台特定信号处理实现
- 新增 .cursor 计划文件记录开发方案
2026-04-14 14:07:59 +08:00

4.3 KiB
Raw Blame History

HoTime 框架优雅停机

概述

HoTime 框架内置优雅停机支持,收到关闭信号后:

  1. 立即对所有新请求返回 503 Service Unavailable,触发 nginx 把流量切到其他实例
  2. 等待 DrainTimeout(默认 5 秒)让 nginx 完成流量切换
  3. 等待所有正在处理的请求完成(最多 ShutdownTimeout,默认 30 秒)
  4. exit(0) 正常退出,run_xbc.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() 前配置):

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 自动转到其他实例,客户端无感知:

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;
        proxy_set_header X-Real-IP $remote_addr;
        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

:: 1. 查找端口 9998 的进程 PID
netstat -ano | findstr :9998

:: 2. 优雅关闭该实例(不加 /F,发送 Ctrl+C 信号)
taskkill /PID <pid>

:: 3. 观察日志,出现"服务已安全关闭"后,部署新版并启动
::    run_xbc.cmd 检测到 exit(0) 后不会自动重启

:: 4. 对端口 9999 重复以上步骤
netstat -ano | findstr :9999
taskkill /PID <pid2>

Linux

# 1. 查找进程 PID
pgrep -a xbc          # 或
ps aux | grep xbc

# 2. 优雅关闭(发送 SIGTERM
kill <pid>

# 3. 观察日志确认关闭完成
tail -f logs/xbc.log  # 等待"服务已安全关闭"

# 4. 部署新版并启动,再对另一个实例重复

工作原理

收到信号 / 关窗口
       │
       ▼
shuttingDown = true
       │
       ▼  DrainTimeout(默认 5s
新请求 → 503 ──────────────────────► nginx 切流到另一台
       │
       ▼
server.Shutdown(ShutdownTimeout=30s)
  等待所有进行中请求跑完
       │
       ▼
os.Exit(0) → run_xbc.cmd 检测到 exit(0),不重启

为何在框架层而非应用层实现

通过 appIns.Run(Router{...}) 启动服务,信号处理与 http.Server 生命周期强绑定,必须在框架的 Run() 方法内才能正确管理。应用层无需任何修改,升级框架即自动获得优雅停机能力。

sync.Once 保证幂等性

框架同时监听多路触发源(信号 goroutine + Windows 关窗口 handler),sync.Once 保证无论哪路先触发,initiateGracefulShutdown 只执行一次,不会重复调用 Shutdown()