Skip to content

优雅重启和停止 原创

1、遇到的问题

在开发完成应用程序后,即可将其部署到测试、预发布或生产环境中,这时又涉及一个问题,即持续集成。简单来说,开发人员需要关注的是这个应用程序是不断地进行更新和发布的,也就是说,这个应用程序在发布时,很可能某客户正在使用这个应用。如果直接硬发布,就会造成客户的行为被中断。

为了避免这种情况的发生,我们希望在应用更新或发布时,现有正在处理既有连接的应用不要中断,要先处理完既有连接后再退出。而新发布的应用在部署上去后再开始接收新的请求并进行处理,这样即可避免原来正在处理的连接被中断的问题。

2、解决方案

想要解决这个问题,目前最经典的方案就是通过信号量的方式来解决。

当一个信号发送给一个进程时,操作系统中断了进程正常的控制流程。此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则执行默认的处理函数

3、具体流程

  • 替换可执行文件或修改配置文件。
  • 发送信号量 SIGHUP。
  • 拒绝新连接请求旧进程,保证正在处理的连接正常。
  • 启动新的子进程。
  • 新的子进程开始 Accept。
  • 系统将新的请求转交新的子进程。
  • 旧进程处理完所有旧连接后正常退出。

4、实现

pkg/shutdown 目录中创建 shutdown.go 文件,写入以下内容:

go
package shutdown

import (
	"context"
	"fmt"
	"net/http"
	"os"
	"time"

	"github.com/joker-bai/kubemana/global"
)

// 优雅退出
func Shutdown(server *http.Server, quit <-chan os.Signal, done chan<- struct{}) {
	//等待接收到退出信号:
	<-quit
	global.Logger.Info("Server is shutting down...")

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	server.SetKeepAlivesEnabled(false)
	err := server.Shutdown(ctx)
	if err != nil {
		global.Logger.Fatal(fmt.Sprintf("Could not gracefully shutdown the server: %v \n", err))
	}

	//do Something :
	fmt.Println("do something start ..... ", time.Now())
	time.Sleep(5 * time.Second)
	fmt.Println("do something end ..... ", time.Now())

	close(done)
}

然后在 main.go 中增加优雅重启的操作,如下:

go
func main() {
	rand.Seed(time.Now().UnixNano())
	// 初始化服务引擎
	engine := initialize.NewEngine()
	server := &http.Server{
		Addr:         ":" + global.ServerSetting.Port,
		WriteTimeout: global.ServerSetting.WriteTimeout,
		ReadTimeout:  global.ServerSetting.ReadTimeout,
		IdleTimeout:  global.ServerSetting.IdleTimeout,
		Handler:      engine,
	}
	global.Logger.Info("Server", zap.String("server", "begin start server ....."))

	// 优雅退出
	done := make(chan struct{}, 1)
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	go shutdown.Shutdown(server, quit, done)

	//启动 server:
	global.Logger.Info(fmt.Sprintf("Server is ready to handle requests at %s", global.ServerSetting.Port))
	err := server.ListenAndServe()
	if err != nil && err != http.ErrServerClosed {
		global.Logger.Fatal(fmt.Sprintf("Could not listen on %s: %v \n", global.ServerSetting.Port, err))
	}

	//等待已经关闭的信号:
	<-done
	global.Logger.Info("Server stopped")
}

5、测试一下

先用 go run main.go 启动服务,然后在终端使用 ctrl + c 中断进程,观察日志输出。

22a7f2cebd39d903619770f48987ed04 MD5

6、常用信号量

补充一些常用的信号量,如下:

go
//操作系统收到信号后的动作:
//Term: 表明默认动作为终止进程
//Ign: 表明默认动作为忽略该信号
//Core: 表明默认动作为终止进程同时输出core dump
//Stop: 表明默认动作为停止进程

// Signals
const (
    SIGABRT   = Signal(0x6) //调用abort函数触发,十进制值:6, Core
    SIGALRM   = Signal(0xe) //时钟定时信号,十进制值:14, Term
    SIGBUS    = Signal(0xa) //非法地址(内存地址对齐错误),十进制值:10 Core
    SIGCHLD   = Signal(0x14)//子进程结束(由父进程接收),十进制值:20  Ign
    SIGCONT   = Signal(0x13)//继续执行已经停止的进程(不能被阻塞),十进制:19 Cont
    SIGEMT    = Signal(0x7)
    SIGFPE    = Signal(0x8)//算术运行错误(浮点运算错误、除数为零等),十进制:8  Core
    SIGHUP    = Signal(0x1)//终端控制进程结束(终端连接断开),十进制:1  Term
    SIGILL    = Signal(0x4)//非法指令(程序错误、试图执行数据段、栈溢出等)  Core
    SIGINFO   = Signal(0x1d)
    SIGINT    = Signal(0x2)//用户发送INTR字符(Ctrl+C)触发,十进制值:2
    SIGIO     = Signal(0x17)
    SIGIOT    = Signal(0x6)
    SIGKILL   = Signal(0x9)//无条件结束程序(不能被捕获、阻塞或忽略)十进制:9
    SIGPIPE   = Signal(0xd)//消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)
    SIGPROF   = Signal(0x1b)
    SIGQUIT   = Signal(0x3)//用户发送QUIT字符(Ctrl+/)触发,十进制值:3
    SIGSEGV   = Signal(0xb)//无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作)
    SIGSTOP   = Signal(0x11)//停止进程(不能被捕获、阻塞或忽略)
    SIGSYS    = Signal(0xc)
    SIGTERM   = Signal(0xf)
    SIGTRAP   = Signal(0x5)
    SIGTSTP   = Signal(0x12)//停止进程(可以被捕获、阻塞或忽略)
    SIGTTIN   = Signal(0x15)//后台程序从终端中读取数据时触发
    SIGTTOU   = Signal(0x16)//后台程序向终端中写数据时触发
    SIGURG    = Signal(0x10)
    SIGUSR1   = Signal(0x1e)
    SIGUSR2   = Signal(0x1f)
    SIGVTALRM = Signal(0x1a)
    SIGWINCH  = Signal(0x1c)
    SIGXCPU   = Signal(0x18)//超过CPU时间资源限制(4.2BSD)
    SIGXFSZ   = Signal(0x19)//超过文件大小资源限制(4.2BSD)
)

7、代码版本

本节内容开发完成后,记得给代码做标记,如下:

go
$ git add .
$ git commit -m "优雅重启和停止"
最近更新