Go语言热重载和优雅地关闭程序
我们有时会因不同的目的去关闭服务,一种关闭服务是终止操作系统,一种关闭服务是用来更新配置。
我们希望优雅地关闭服务和通过热重载重新加载配置,而这两种方式可以通过信号包来完成。
1、代码实现
package main
import (
"log"
"net/http"
"os"
"os/signal"
"syscall"
)
type Config struct {
Message string
}
var conf = &Config{Message: "Before hot reload"}
func router() {
log.Println("starting up....")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(conf.Message))
})
go func() {
log.Fatal(http.ListenAndServe(":8080", nil))
}()
}
func main() {
router()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
for {
multiSignalHandler(<-sigCh)
}
}
func multiSignalHandler(signal os.Signal) {
switch signal {
case syscall.SIGHUP:
log.Println("Signal:", signal.String())
log.Println("After hot reload")
conf.Message = "Hot reload has been finished."
case syscall.SIGINT:
log.Println("Signal:", signal.String())
log.Println("Interrupt by Ctrl+C")
os.Exit(0)
case syscall.SIGTERM:
log.Println("Signal:", signal.String())
log.Println("Process is killed.")
os.Exit(0)
default:
log.Println("Unhandled/unknown signal")
}
}
首先,定义了一个 Config 结构并声明了一个 conf 变量。
type Config struct {
Message string
}
var conf = &Config{Message: "Before hot reload"}
这里的代码只是一个简单的配置样本,你可以根据自己的需要定义一个复杂的结构。
其次,定义一个路由器函数,用来绑定和监听 8080 端口。在热重载配置完成后,它也被用来显示结果。
func router() {
log.Println("starting up....")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(conf.Message))
})
go func() {
log.Fatal(http.ListenAndServe(":8080", nil))
}()
}
下一步是服务器关机和热重载配置,当一个服务器关闭时,它应该停止接收新的请求,同时完成正在进行的请求,
返回其响应,然后关闭,在这里使用信号包实现。
sigCh := make(chan os.Signal, 1)
之后,使用 signal.Notify() 一起发送更多的信号。
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
当程序被中断时,signal.Notify 将向 sigCh 通道发送一个信号。
syscall.SIGHUP、syscall.SIGINT 和 syscall.SIGTERM 是什么意思?
- syscall.SIGINT 是用来在 Ctrl+C 时优雅地关闭的,它也相当于 os.Interrupt。
- syscall.SIGTERM 是常用的终止信号,也是 docker 容器的默认信号,Kubernetes 也使用它。
- syscall.SIGHUP 用于热重载配置。
如何优雅地关闭,multiSignalHandler(<-sigCh) 被用来接收 chan 值,然后它将决定运行代码的哪一部分。
func multiSignalHandler(signal os.Signal) {
switch signal {
case syscall.SIGHUP:
log.Println("Signal:", signal.String())
log.Println("After hot reload")
conf.Message = "Hot reload has been finished."
case syscall.SIGINT:
log.Println("Signal:", signal.String())
log.Println("Interrupt by Ctrl+C")
os.Exit(0)
case syscall.SIGTERM:
log.Println("Signal:", signal.String())
log.Println("Process is killed.")
os.Exit(0)
default:
log.Println("Unhandled/unknown signal")
}
}
2、测试优雅关闭
首先,运行服务器。
$ go run main.go
2023/06/25 09:57:05 starting up....
发送一个curl请求。
$ curl localhost:8080
Before hot reload
先用Ctrl+C测试一下中断。
$ go run main.go
2023/06/25 09:57:05 starting up....
2023/06/25 09:59:45 Signal: interrupt
2023/06/25 09:59:45 Interrupt by Ctrl+C
3、热重载
首先,运行服务器。
$ go run main.go
2023/06/24 22:03:17 starting up....
然后,发送curl请求。
$ curl localhost:8080
Before hot reload
查看进程:
$ lsof -i tcp:8080
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
main 85193 root 3u IPv6 13642377 0t0 TCP *:webcache (LISTEN)
使用 kill 杀死进程:
$ kill -SIGHUP 85193
$ go run main.go
2023/06/24 22:03:17 starting up....
2023/06/24 22:06:05 Signal: hangup
2023/06/24 22:06:05 After hot reload
如果直接使用 kill 命令杀死程序:
$ go run main.go
2023/06/24 22:14:11 starting up....
$ lsof -i tcp:8080
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
main 89619 root 3u IPv6 13669401 0t0 TCP *:webcache (LISTEN)
$ kill -9 89619
$ go run main.go
2023/06/24 22:14:11 starting up....
2023/06/24 22:14:50 Signal: terminated
2023/06/24 22:14:50 Process is killed.
4、Go信号库os/signal
在官方介绍中,这个库主要封装信号实现对输入信号的访问,信号主要用于类 Unix 系统。
信号是事件发生时对进程的通知机制,有时也称之为软件中断。信号与硬件中断的相似之处在于打断了程序执行的
正常流程,大多数情况下,无法预测信号到达的精确时间。
因为一个具有合适权限的进程可以向另一个进程发送信号,这可以称为进程间的一种同步技术。当然,进程也可以
向自身发送信号。然而,发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
-
硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一
条异常的机器语言指令(除0,引用无法访问的内存区域)。
-
用户键入了能够产生信号的终端特殊字符。如中断字符 (通常是 Control-C)、暂停字符(通常是 Control-Z)。
-
发生了软件事件。如调整了终端窗口大小,定时器到期等。
4.1 举例说明
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
// os.Signal是一个系统信号接收channel
c := make(chan os.Signal, 1)
// syscall都是一些系统信号
signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
for {
s := <-c
fmt.Printf("get a signal %s", s.String())
switch s {
case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
fmt.Printf("exit")
os.Exit(0)
case syscall.SIGHUP:
fmt.Printf("reload")
default:
fmt.Printf("nothing")
}
}
}
1、首先初始化一个 os.Signal 类型的 channel,我们必须使用缓冲通道,否则在信号发送时如果还没有准备好接
收信号,就有丢失信号的风险。
2、signal.notify 用于监听信号,参数1表示接收信号的 channel,参数2及后面的表示要监听的信号:
-
syscall.SIGHUP 表示终端控制进程结束
-
syscall.SIGQUIT 表示用户发送QUIT字符 (Ctrl+/) 触发
-
syscall.SIGTERM 表示结束进程
-
syscall.SIGINT 表示用户发送INTR字符 (Ctrl+C) 触发
3、<-c 一直阻塞直到接收到信号退出。
对于上面的的程序是优雅的退出守护进程,接下来就是一些释放资源或dump进程当前状态或记录日志的动作,完
成这些后,主进程退出。
4.2 GO的信号类型
4.2.1 POSIX.1-1990标准中定义的信号列表
信号值 | 值 | 动作 | 说明 |
---|---|---|---|
SIGHUP | 1 | Term | 终端控制进程结束(终端连接断开) |
SIGINT | 2 | Term | 用户发送INTR字符(Ctrl+C)触发 |
SIGQUIT | 3 | Core | 用户发送QUIT字符(Ctrl+/)触发 |
SIGILL | 4 | Core | 非法指令(程序错误、试图执行数据段、栈溢出等) |
SIGABRT | 6 | Core | 调用abort函数触发 |
SIGFPE | 8 | Core | 算术运行错误(浮点运算错误、除数为零等) |
SIGKILL | 9 | Term | 无条件结束程序(不能被捕获、阻塞或忽略) |
SIGSEGV | 11 | Core | 无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作) |
SIGPIPE | 13 | Term | 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作) |
SIGALRM | 14 | Term | 时钟定时信号 |
SIGTERM | 15 | Term | 结束程序(可以被捕获、阻塞或忽略) |
SIGUSR1 | 30,10,16 | Term | 用户保留 |
SIGUSR2 | 31,12,17 | Term | 用户保留 |
SIGCHLD | 20,17,18 | Ign | 子进程结束(由父进程接收) |
SIGCONT | 19,18,25 | Cont | 继续执行已经停止的进程(不能被阻塞) |
SIGSTOP | 17,19,23 | Stop | 停止进程(不能被捕获、阻塞或忽略) |
SIGTSTP | 18,20,24 | Stop | 停止进程(可以被捕获、阻塞或忽略) |
SIGTTIN | 21,21,26 | Stop | 后台程序从终端中读取数据时触发 |
SIGTTOU | 22,22,27 | Stop | 后台程序向终端中写数据时触发 |
4.2.2 在SUSv2和POSIX.1-2001标准中的信号列表
信号 | 值 | 动作 | 说明 |
---|---|---|---|
SIGTRAP | 5 | Core | Trap指令触发(如断点,在调试器中使用) |
SIGBUS | 0,7,10 | Core | 非法地址(内存地址对齐错误) |
SIGPOLL | Term | Pollable event (Sys V). Synonym for SIGIO | |
SIGPROF | 27,27,29 | Term | 性能时钟信号(包含系统调用时间和进程占用CPU的时间) |
SIGSYS | 12,31,12 | Core | 无效的系统调用(SVr4) |
SIGURG | 16,23,21 | Ign | 有紧急数据到达Socket(4.2BSD) |
SIGVTALRM | 26,26,28 | Term | 虚拟时钟信号(进程占用CPU的时间)(4.2BSD) |
SIGXCPU | 24,24,30 | Core | 超过CPU时间资源限制(4.2BSD) |
SIGXFSZ | 25,25,31 | Core | 超过文件大小资源限制(4.2BSD) |
信号 SIGKILL 和 SIGSTOP 可能不会被程序捕获,因此不会受此软件包影响。
同步信号是由程序执行中的错误触发的信号:SIGBUS,SIGFPE 和 SIGSEGV。这些只在程序执行时才被认为是同
步的,而不是在使用 os.Process.Kill 或 kill 程序或类似的机制发送时。一般来说,除了如下所述,Go 程序会将同
步信号转换为运行时异常。其余信号是异步信号,它们不是由程序错误触发的,而是从内核或其他程序发送的。
在异步信号中,SIGHUP 信号在程序失去其控制终端时发送。当控制终端的用户按下中断字符(默认为^ C
(Control-C))时,发送 SIGINT 信号。当控制终端的用户按下退出字符时发送 SIGQUIT 信号,默认为^ \
(Control-Backslash)。一般情况下,您可以通过按^ C来使程序简单地退出,并且可以通过按^使堆栈转储退出。
4.3 Kill命令的原理
我们平时在 Linux 系统会 kill 命令来杀死进程,那其中的原理是什么呢。
4.3.1 kill pid
kill pid 的作用是向进程号为 pid 的进程发送 SIGTERM (这是 kill 默认发送的信号),该信号是一个结束进程的信号
且可以被应用程序捕获。若应用程序没有捕获并响应该信号的逻辑代码,则该信号的默认动作是 kill 掉进程。这是
终止指定进程的推荐做法。
4.3.2 kill -9 pid
kill -9 pid 则是向进程号为 pid 的进程发送 SIGKILL (该信号的编号为9),从本文上面的说明可知,SIGKILL 既不能
被应用程序捕获,也不能被阻塞或忽略,其动作是立即结束指定进程。通俗地说,应用程序根本无法感知 SIGKILL
信号,它在完全无准备的情况下,就被收到 SIGKILL 信号的操作系统给干掉了,显然,在这种暴力情况下,应用
程序完全没有释放当前占用资源的机会。事实上,SIGKILL 信号是直接发给 init 进程的,它收到该信号后,负责终文章来源:https://www.toymoban.com/news/detail-798464.html
止 pid 指定的进程。在某些情况下(如进程已经 hang 死,无响应正常信号),就可以使用 kill -9 来结束进程。文章来源地址https://www.toymoban.com/news/detail-798464.html
到了这里,关于Go语言热重载和优雅地关闭程序的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!