Go语言实现服务优雅关机与重启

陈默
陈默
发布时间:2026-10-16 | 4人浏览过
原创

文章介绍了如何在 Go 中实现服务的优雅关闭和重启,以避免因强制杀死进程导致的请求中断问题。

为什么需要优雅操作

在部署 Web 项目时,配置变更或版本迭代往往意味着服务重启。如果使用 kill -9 这种暴力方式关闭进程,当前正在处理的请求会被强制终止,从而导致客户端请求失败。所谓“优雅”,是指在收到关机指令后,程序不再接受新请求,而是等待已有请求全部处理完毕后再退出。

优雅关机实现原理

核心方法

自 Go 1.8 起,net/http.Server 新增了 Shutdown() 方法。该方法会触发停止接收新连接,并等待所有活跃连接(即正在处理的请求)完成后才真正退出。实现的关键在于使用 os/signal 包监听系统信号。

操作步骤

  • 监听信号:创建一个通道并通过 signal.Notify 绑定 SYS.SIGINT (Ctrl+C) 和 SYS.SIGTERM 信号。
  • 异步启动:将服务器的监听放入 Goroutine 中运行,以免阻塞主程序对信号的监听。
  • 设置超时:调用 Shutdown(ctx) 时传入带有超时的 context,防止意外情况下无法退出。

Gin 服务优雅关机代码示例

// +build go1.8
package main
import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
	"github.com/gin-gonic/gin"
)
func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		c.String(http.StatusOK, "Welcome Gin Server")
	})
	srv := &http.Server{
		Addr:    ":8080",
		Handler: router,
	}
	go func() {
		// 开启一个goroutine启动服务
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()
	// 等待中断信号来优雅地关闭服务器,为关闭服务器操作设置一个5秒的超时
	quit := make(chan os.Signal, 1) // 创建一个接收信号的通道
	// kill 默认会发送 syscall.SIGTERM 信号
	// kill -2 发送 syscall.SIGINT 信号,我们常用的Ctrl+C就是触发系统SIGINT信号
	// kill -9 发送 syscall.SIGKILL 信号,但是不能被捕获,所以不需要添加它
	// signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 信号转发给quit
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)  // 此处不会阻塞
	<-quit  // 阻塞在此,当接收到上述两种信号时才会往下执行
	log.Println("Shutdown Server ...")
	// 创建一个5秒超时的context
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	// 5秒内优雅关闭服务(将未处理完的请求处理完再关闭服务),超过5秒就超时退出
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("Server Shutdown: ", err)
	}
	log.Println("Server exiting")
}

验证方法

可通过以下方式测试:
1. 编译并运行代码。
2. 浏览器访问 127.0.0.1:8080,此时由于代码中模拟了5秒延迟,页面会显示白屏。
3. 迅速按下 Ctrl+C。
4. 观察终端输出 Server exiting。此时会发现虽然发出了退出命令,但浏览器直到5秒后才显示出结果,证明服务等待了请求完成。

优雅重启实现方案

基本原理与工具

标准库没有直接的重启接口。通常使用第三方库如 fvbock/endless 来替代默认的 ListenAndServe。该库利用 fork/restart 机制,在接收到特定信号时会复制进程环境并执行新版本二进制,随后旧进程结束,从而实现平滑过渡。

代码示例

package main
import (
	"log"
	"net/http"
	"time"

	"github.com/fvbock/endless"
	"github.com/gin-gonic/gin"
)
func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		c.String(http.StatusOK, "hello gin!")
	})
	// 默认endless服务器会监听下列信号:
	// syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP
	// 接收到 SIGHUP 信号将触发`fork/restart` 实现优雅重启(kill -1 pid会发送SIGHUP信号)
	// 接收到 syscall.SIGINT或syscall.SIGTERM 信号将触发优雅关机
	// 接收到 SIGUSR2 信号将触发HammerTime
	// SIGUSR1 和 SIGTSTP 被用来触发一些用户自定义的hook函数
	if err := endless.ListenAndServe(":8080", router); err!=nil{
		log.Fatalf("listen: %s\n", err)
	}
	log.Println("Server exiting")
}

验证流程

  • 准备环境:假设原 PID 为 43682。
  • 发起请求:打开浏览器访问 127.0.0.1:8080。
  • 修改代码:将响应改为新的内容(如 hello q1mi!),重新编译生成新的二进制文件。
  • 触发重启:在终端执行 kill -1 43682,向进程发送 SIGHUP 信号。
  • 观察结果:旧的 hello gin! 请求会正常结束,而新发出的请求会自动由新进程处理并返回 hello q1mi!。

注意事项与局限性

在执行优雅重启时,必须注意程序的 PID 会发生变化。这是因为底层使用了 fork 机制派生子进程。如果你的进程是由 supervisor 或 systemd 等外部管理器监控的,这种方式可能会导致 PID 不匹配,进而使得外部管理器误认为旧进程已崩溃而再次尝试拉起,引发循环重启。在这种场景下,建议仅使用标准库的 Shutdown 逻辑进行重启前清理和退出。

总结而言,无论是关机还是重启,核心都在于利用系统信号机制接管控制流,确保数据与请求的安全流转。

以上就是使用golang如何优雅的关机或重启操作的详细内容,更多关于golang信号处理和优雅退出的资料请关注本站其它相关文章!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表