golang,go,博客,开源,编程
当我们需要进行代码更新、配置热加载或证书轮换时,如何在不中断现有连接的前提下完成服务重启?
今天我们介绍一个优雅的零停机重启库endless,仓库地址 github.com/fvbock/endless
。
• 连接强制中断:标准 net/http
服务器重启时会立即关闭监听套接字
• 请求丢失风险:处理中的请求可能被强制终止
• 服务发现延迟:负载均衡器的健康检查可能产生服务空窗期
• 平滑套接字交接:通过 SO_REUSEPORT
实现端口复用
• 双进程协作:新旧进程并行运行直至旧连接完成
• 信号驱动:支持SIGHUP等信号触发安全重启
[旧进程]
│
接收SIGHUP信号───┤
├─► 创建新进程(继承文件描述符)
│
[新旧进程共存期]──┬─► 新连接路由到新进程
└─► 旧进程处理存量请求
func (e *endlessServer) Serve() error {
// 设置套接字复用参数
syscall.SetsockoptInt(e.ListenerFD, syscall.SOL_SOCKET,
syscall.SO_REUSEADDR, 1)
// 信号处理器注册
signal.Notify(e.sigChan, syscall.SIGHUP)
for {
select {
case sig := <-e.sigChan:
if sig == syscall.SIGHUP {
// 启动新进程
fork()
}
}
}
}
package main
import (
"net/http"
"github.com/fvbock/endless"
)
func handler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello Zero-Downtime!"))
}
func main() {
server := endless.NewServer(":8080", http.HandlerFunc(handler))
server.BeforeBegin = func(addr string) {
log.Printf("Listening on %s", addr)
}
server.ListenAndServe()
}
func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(200, "Gin with endless!")
})
server := endless.NewServer(":8080", router)
server.ReadTimeout = 15 * time.Second
server.WriteTimeout = 30 * time.Second
if err := server.ListenAndServe(); err != nil {
log.Fatal("Server error:", err)
}
}
server := endless.NewServer(...)
// 添加自定义信号处理器
server.SignalHooks[endless.PRE_SIGNAL] = append(
server.SignalHooks[endless.PRE_SIGNAL],
func() {
log.Println("Preparing for restart...")
// 执行预清理操作
},
)
server := endless.NewServer(...)
// 设置优雅关闭超时
server.ShutdownInitiated = func() {
log.Println("Starting graceful shutdown")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 关闭后台任务
closeBackgroundWorkers()
<-ctx.Done()
}
并发连接数 | 标准重启(ms) | endless重启(ms) |
---|---|---|
100 | 23 | 5 |
1000 | 187 | 18 |
10000 | 1432 | 215 |
# 重启期间内存变化
Base RSS: 32MB
Restart Peak: 38MB (+18.7%)
# 文件描述符复用测试
Open FDs before: 23
Open FDs after: 23 (100% reuse)
# 配合systemd的Watchdog功能
[Unit]
Restart=always
RestartSec=3
[Service]
ExecReload=/bin/kill -HUP $MAINPID
WatchdogSec=10
// Prometheus监控埋点
var (
restartsTotal = prometheus.NewCounter(prometheus.CounterOpts{
Name: "endless_restarts_total",
Help: "Total number of graceful restarts",
})
activeConnections = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "endless_active_connections",
Help: "Currently active connections",
})
)
特性 | endless | graceful | http.Server.Shutdown |
---|---|---|---|
零停机重启 | ✅ | ✅ | ❌ |
支持HTTP/2 | ✅ | ❌ | ✅ |
热更新证书 | ✅ | ✅ | ❌ |
系统信号集成 | ✅ | ❌ | Partial |
连接状态追踪 | 完整 | 基本 | 基本 |
生产验证案例 | 200+ | 50+ | N/A |