本文介绍如何优雅的关闭服务,以及k8s集群相关的配置
问题
当我们部署的服务被关闭时,如果正在运行一些操作(读写数据等),则可能会导致数据的丢失。通常我们希望服务被关闭之前,能处理好正在进行的操作之后再退出。
如何解决?
linux系统下,当需要关闭服务时,通常会发送一个信号给服务,这个信号可以被阻塞和处理。可以通过 kill 指令来获取信号列表:
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
关闭服务相关的信号通常有如下几个:
- 2) SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
- 3) SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-/)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
- 9) SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
- 15) SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
通过对服务增加对退出信号的处理,可以在接收到退出信号时做一些收尾和清理工作,从而优化的关闭服务
golang gin示例
通常情况下,我们的服务会在main函数中阻塞:
import (
"os/signal"
"syscall"
"github.com/gin-gonic/gin"
"github.com/prometheus/common/log"
)
func main() {
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "hello world")
})
router.Run(":5000")
}
如果想要响应系统发出的signal,则需要做一些调整:
- 响应http请求的部分放入goruntine中
- 通过信道阻塞主线程,监听信号
- 接收到退出信号时,做清理工作
- 退出
// 1. 响应http请求的部分放入goruntine中
srv := &http.Server{
Addr: ":5000",
Handler: router,
}
go func() {
// service connections
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s
", err)
}
}()
// 2. 通过信道阻塞主线程,监听信号
sigs := make(chan os.Signal)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigs
log.Info("Shutdown Server by sig: %v", sig)
// 3. 接收到退出信号时,做清理工作
GracefulCleanAndClose()
log.Info("Server exiting")
// 4. 退出
k8s相关配置
Kubernetes终止生命周期的步骤:
- Pod设置为 terminating 状态,并从所有服务的 endpoint 列表中删除
此时,广告连播停止获取新流量。在容器中运行的容器不会受到影响。
- 执行preStop Hook
prestop Hook 可以发送给 pod 以执行命令或HTTP请求。
如果您使用的是第三方代码或正在管理无法控制的系统,则preStop挂钩是触发正常关闭而不修改应用程序的好方法。
-
发送SIGTERM信号到pod
-
Kubernetes等待宽限期
此时,Kubernetes等待指定的时间,称为终止宽限期。默认情况下,这是30秒,这与preStop挂钩和SIGTERM信号并行发生。Kubernetes不会等待preStop挂钩完成。
如果您的应用程序完成关闭并在TerminationGracePeriod完成之前退出,则Kubernetes会立即移至下一步。
您可以通过在Pod YAML中设置TerminationGracePeriodSeconds选项来实现:
containers:
- image:
imagePullPolicy: Always
terminationGracePeriodSeconds: 120
- 发送SIGKILL信号到pod
如果在宽限期之后容器仍在运行,则会向其发送SIGKILL信号并强行将其删除。此时,所有Kubernetes对象也将被清理。