Appearance
28|优雅退出与信号
go-gateway 有两个 HTTP 服务在运行::8080 的代理层和 :8081 的管理后台。进程收到停止信号(如 systemd 的 systemctl stop 或 Kubernetes 的 kubectl delete pod)时,直接退出会导致正在处理的请求被中断,客户端收到连接重置错误。优雅退出的目标是:停止接收新请求,等待正在处理的请求完成,然后才退出进程。
一、信号处理
操作系统通过信号通知进程。Go 的 os/signal 包可以监听信号:
go
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
done := make(chan os.Signal, 1)
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
fmt.Println("waiting for signal...")
<-done
fmt.Println("signal received, shutting down")
}| 信号 | 触发场景 |
|---|---|
SIGINT | 按 Ctrl+C |
SIGTERM | kill 命令、systemd stop、K8s 删除 Pod |
SIGKILL | kill -9,无法捕获,直接终止 |
SIGKILL 不能被捕获或忽略,进程没有机会做清理。优雅退出依赖 SIGTERM,所以部署配置里要用正常的停止方式,不要用 kill -9。
二、http.Server.Shutdown
Go 1.8 引入的 Shutdown 方法实现了优雅关闭:
go
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
srv := &http.Server{Addr: ":8080"}
// 在 goroutine 里启动服务
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Fprintln(os.Stderr, "server error:", err)
}
}()
// 等待停止信号
done := make(chan os.Signal, 1)
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
<-done
// 优雅关闭
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
fmt.Fprintln(os.Stderr, "shutdown error:", err)
}
fmt.Println("server stopped")
}Shutdown 的工作流程:
- 关闭 Listener,停止接收新连接
- 等待所有正在处理的请求完成
- 如果超时时间到了还有请求没完成,强制关闭
- 返回
http.ErrServerClosed
三、双服务优雅退出
go-gateway 有代理层和管理后台两个服务,需要同时优雅关闭:
go
func main() {
proxySrv := &http.Server{Addr: ":8080", Handler: proxyHandler}
adminSrv := &http.Server{Addr: ":8081", Handler: adminHandler}
go proxySrv.ListenAndServe()
go adminSrv.ListenAndServe()
done := make(chan os.Signal, 1)
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
<-done
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// 同时关闭两个服务
go proxySrv.Shutdown(ctx)
go adminSrv.Shutdown(ctx)
<-ctx.Done()
fmt.Println("shutdown complete")
}超时时间要合理。太短(如 1 秒),长请求没完成就被强制关闭;太长(如 5 分钟),Pod 删除或滚动更新时卡住。通常设 10-30 秒,和 K8s 的 terminationGracePeriodSeconds 对齐。
四、context 传递
Shutdown 的 context 不仅控制超时,还可以传递给 handler,让 handler 知道自己该尽快结束:
go
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
select {
case <-time.After(5 * time.Second):
w.Write([]byte("done"))
case <-ctx.Done():
// 收到关闭信号,提前返回
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("server shutting down"))
}
}r.Context() 在 Shutdown 被调用时会触发 Done(),handler 可以监听这个信号,提前释放资源或返回错误。
五、资源清理
除了关闭 HTTP 服务,还要清理其他资源:
go
func shutdown() {
// 1. 关闭 HTTP 服务
proxySrv.Shutdown(ctx)
adminSrv.Shutdown(ctx)
// 2. 关闭数据库连接
db.Close()
// 3. 关闭 Redis 连接
redisClient.Close()
// 4. 刷新日志缓冲
logger.Sync()
}资源清理顺序很重要:先停止接收流量,再关闭下游连接,最后释放本地资源。顺序反了可能导致正在处理的请求失败。
六、常见错误
不处理 Shutdown 错误
go
srv.Shutdown(ctx) // 忽略了返回值Shutdown 返回错误说明超时时间内没关闭完,应该记录日志或触发告警。
用 kill -9 停止
bash
kill -9 <pid>SIGKILL 不给进程任何清理机会,直接终止。测试优雅退出时要用 kill(默认发 SIGTERM)或 Ctrl+C。
主 goroutine 先退出
go
go srv.ListenAndServe()
// 主 goroutine 直接退出,进程结束如果主 goroutine 在 ListenAndServe 启动后就退出,进程会直接结束。需要阻塞主 goroutine,等待信号或某个 channel。