golang优雅退出

优雅退出

graceful shutdown,优雅退出。

指HTTP服务接受到用户的退出指令后停止接收新请求,在处理和回复当前正在处理的这批请求后主动退出服务。

区别于SIGKILL(kill -9 or CTRL + C),安全退出可以最小化程序在滚动更新时的服务抖动

用户的退出指令一般是SIGTERM(k8s的实现)或SIGINT(常常对应bash的Ctrl + C

一、 涉及模块

1、 监听信号

使用标准库os/exec.go中Signal即可完成信息监听

go 复制代码
  // 至少设置数量为1的缓存区
  quitSignal := make(chan os.Signal, 1)
  signal.Notify(quitSignal, []os.Signal{syscall.SIGINT, syscall.SIGTERM}...)

  // 阻塞直至有信号写入
	<-quitSignal
  • SIGINT:当你在终端按下ctrl+c时,则会触发这个信号
  • SIGTERM:当我们给程序发送kill或者killall指令时,则会触发这个信号

值得注意的是,在没有使用signal.Notify()时,Go默认有一套信号处理规则,比如 SIGHUP, SIGINTSIGTERM会让程序直接退出。

2、 停止HTTP服务

调用运行中的Server实例的Shutdown()方法可以让服务安全退出:

go 复制代码
// ListenAndServe listens on the TCP network address srv.Addr and then
// calls Serve to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// If srv.Addr is blank, ":http" is used.
//
// ListenAndServe always returns a non-nil error. After Shutdown or Close,
// the returned error is ErrServerClosed.
func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(ln)
}

// When Shutdown is called, Serve, ListenAndServe, and
// ListenAndServeTLS immediately return ErrServerClosed. Make sure the
// program doesn't exit and waits instead for Shutdown to return.

// Once Shutdown has been called on a server, it may not be reused;
// future calls to methods such as Serve will return ErrServerClosed.
func (srv *Server) Shutdown(ctx context.Context) error {
  xxx
}

这里能看到标准库的注释

  • ListenAndServe会在ShutdownClose立即 返回 ErrServerClosed
  • Shutdown执行完成时,确保程序不会被退出而是等待以返回

3、 超时处理

server的Shutdown方法需要接收一个context对象,因此我们可以定义一个设置超时的context,如果超过这个时间请求还没完成处理,则会强制退出,避免程序长时间等待无法退出

当然也可以传入一个没有超时的context(context.Background())

二、 代码实现

handler方法,通过num参数进行短暂休眠,并打印休眠持续时间。

简单Demo,未加数据校验。

go 复制代码
func handler(w http.ResponseWriter, r *http.Request) {
	numStr := r.URL.Query().Get("num")
	num, err := strconv.Atoi(numStr)
	if err != nil {
		return
	}
	delay := time.Duration(num) * time.Second
	startAt := time.Now()
	fmt.Println("req received, delay", delay)
	defer func() {
		fmt.Println("req completed, latency", time.Since(startAt))
	}()

	time.Sleep(delay)
	w.WriteHeader(http.StatusOK)
	_, _ = io.WriteString(w, numStr)
}

main函数,创建http服务、启动服务、监听系统退出信号、超时处理。

go 复制代码
func main() {
	// 创建一个新的HTTP服务器
	mux := http.NewServeMux()
	mux.HandleFunc("/process", handler)
	server := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}

	// 在一个新的goroutine中启动服务器
	go func() {
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			fmt.Printf("listen: %s\n", err)
		}
	}()

	// 监听系统退出信号
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, []os.Signal{syscall.SIGINT, syscall.SIGTERM}...)
	fmt.Println(fmt.Sprintf("\n exit: %v", <-quit))

	// 创建一个带有超时的context,以便在服务器关闭时有一个限制时间
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := server.Shutdown(ctx); err != nil {
		fmt.Printf("Server Shutdown: %s, time out\n", err)
	}
	fmt.Println("Server exiting")
}
  • 启动服务ListenAndServe()会阻塞程序,为了避免后续的信号监测被阻塞,因此需要把服务启动放到协程执行。
  • 根据设置超时时间和handler传参的大小,体验服务是否超时的返回结果有什么不同

三、 业务场景

代码实现只是一个简单的demo,在实际应用场景下,并不会简单地、草率地开启这样一个http服务,一方便程序要监听系统的退出信号,另一方面在程序拉起时创建routerGroup路由组、LoadConfig加载配置等出现错误时也应中断程序并给出对应的错误信息。

go 复制代码
func main() {

  	runtime.GOMAXPROCS(runtime.NumCPU())
  	...
  
  	errs := make(chan error, 2)
 	go func() {
		c := make(chan os.Signal)
		signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
		errs <- fmt.Errorf("%s", <-c)
	}()
	
 	hostAPI(errs)
	...
  
	fmt.Println(fmt.Sprintf("exit: %v", <-errs))
}

func hostAPI(errs chan error) {
	server := GetServer() // 表示获取Server对象,伪代码
	  router := gin.Default()
	if svr == nil {
		log.Warnf(" [%s] server config is nil", name)
		return
	}
	log.Infof("host [%s] server [%s,%d]", svr.Name, svr.Host, svr.Port)
	go func() {
		strPort := strconv.Itoa(svr.Port)
		listenAddr := svr.Host + ":" + strPort
		fmt.Println("hosts:", listenAddr)
		errs <- http.ListenAndServe(listenAddr, router) //打开监听端口
	}()
}
  • 在main函数中创建error管道,缓冲区大小设置为2(ERROR+SIGTERM)
  • 监听系统退出+监听API路由组创建是否异常
  • 错误均写入errs, 在main函数最后,阻塞等待errs管道后退出程序
相关推荐
Rust研习社2 小时前
组合真的优于继承吗?为什么 Rust 和 Go 都拥抱组合舍弃继承?
后端·rust·编程语言
IT_陈寒3 小时前
JavaScript的闭包把我坑惨了,说好的内存会自动回收呢?
前端·人工智能·后端
CaffeinePro3 小时前
Pydantic深度使用:数据校验、枚举、ORM映射
后端·fastapi
Chenyiax4 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH4 小时前
Koa和Express的区别
后端
MariaH4 小时前
Koa框架的使用
后端
luckdewei5 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某6 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy6 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom7 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github