优雅重启和停止 原创
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
中断进程,观察日志输出。
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 "优雅重启和停止"