fiber 本身不提供开箱即用的优雅停机能力,必须手动接管 http.server 实例并配合信号监听、带超时的 shutdown() 和资源协调;其 app.listen() 是阻塞式启动且不暴露 *http.server,需改用 srv.serve(ln) 显式启动并按序关闭 http 层及 db、grpc、redis 等所有资源。

Fiber 框架本身不提供开箱即用的优雅停机能力,必须手动接管 http.Server 实例并配合信号监听、带超时的 Shutdown() 和资源协调 —— 这和 Gin、Echo 本质一致,不是 Fiber 的缺陷,而是 Go 标准库设计使然。
为什么 app.Listen() 不能直接用于生产停机
调用 app.Listen(":8080") 是阻塞式启动,内部封装了 http.Server.ListenAndServe(),它不接受 context.Context,也不暴露 *http.Server 实例。一旦进程收到 SIGTERM,Go 运行时立即退出,所有活跃连接被 RST 中断,数据库事务未提交、文件写入中断、WebSocket 连接突兀断开都是常见现象。
典型错误表现:
-
connection reset by peer(大文件上传中断) - 前端 SSE 连接丢失
last-event-id,重连后重复收消息 - 日志里出现
sql: transaction has already been committed or rolled back
如何从 fiber.App “抠出” *http.Server 并接管 shutdown
必须放弃 app.Listen(),改用显式构造 *http.Server + srv.Serve(ln) 方式启动:
ln, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
srv := &http.Server{
Handler: app.Handler(),
Addr: ":8080",
}
go func() {
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
关键点:
- 用
app.Handler()获取底层http.Handler,而非app本身 - 必须用
srv.Serve(ln)而非srv.ListenAndServe(),否则无法复用已创建的net.Listener(比如需要 TLS 或 Prefork 场景) - 启动必须放在 goroutine,否则主 goroutine 阻塞,收不到信号
- 保留
srv变量引用,后续Shutdown()全靠它
srv.Shutdown() 必须带超时 context,且 handler 要响应取消
srv.Shutdown(ctx) 只做两件事:关闭 listener 拒绝新连接 + 等待已有 handler 返回。它不设默认超时,也不中断卡住的 DB 查询或 HTTP client 调用。
错误写法:srv.Shutdown(context.Background()) → 进程 hang 住,K8s 在 terminationGracePeriodSeconds 后发 SIGKILL
正确写法:
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("shutdown error: %v", err)
}
同时,所有 handler 必须使用带 context 的下游调用:
- DB 查询用
db.QueryContext(ctx, ...),不用db.Query(...) - Redis 调用传
ctx,如rdb.Get(ctx, "key").Result() - 第三方 HTTP 请求用
http.Client.Do(req.WithContext(ctx)) - 避免在 handler 里用无超时的
time.Sleep()或select{}
HTTP 之外的资源必须手动关,顺序不能错
http.Server.Shutdown() 只管 HTTP 层。gRPC Server、数据库连接池、Redis 客户端、WebSocket manager、后台定时器、消息消费者……全得你亲手收尾,漏一个,main() 就卡住不退出。
推荐顺序:
- 先调
srv.Shutdown(ctx)拒绝新请求 - 再触发 gRPC
GracefulStop()(注意它不等流式 RPC 结束,需额外判断活跃 stream) - 对支持 context 的 DB 驱动(如
pgx/v5),用db.BeginTx(ctx, ...)和tx.Commit(ctx)确保超时 rollback - 最后调
db.Close()(它会阻塞直到连接归还,但不中断活跃事务) - Redis 客户端调
client.Close(),WebSocket manager 调Close()清理所有连接
真正容易被忽略的是:handler 内部新开的 goroutine 如果没传 ctx 或没监听 ctx.Done(),它们不会随 Shutdown() 自动结束 —— 这类 goroutine 会成为“幽灵协程”,让进程迟迟无法退出。











