我们因不同的目的去关闭服务。有时,关闭服务器的主要目的是用来更新配置。在Golang中,有很多关于优雅关机的帖子。然而,我发现他们并没有提到热重载配置。一般来说,shutdown graceful和hot reload是由信号控制的。这就是我把它们放在一个帖子里的原因。
起初,一个服务器由于各种原因需要关闭,常见的是操作系统的中断,我们希望能优雅地关闭服务器。最后也是最基本的是如果一个应用程序需要更新,只需要更新配置。这种情况总是发生。那个时候,如果你不需要重启服务器,那将是非常惊人的。你只需要更新你的配置,然后发送一个信号。之后,服务器将热重新加载,你的新配置就会工作。上述场景可以在Golang中使用信号包来完成。让我们动手吧。
代码实现
首先,我定义了一个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
用于热重载配置。
这一部分的全部代码如下:
func main() { router() sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) for { multiSignalHandler(<-sigCh) } }
由于这个帖子也包括优雅地关闭,multiSignalHandler(<-sigCh)
被用来接收chan值,然后,它将决定运行代码的哪一部分。multiSignalHandler
的代码如下:
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") } }
以上所有的代码将使优雅地关机和热重载配置工作。我把整个代码粘贴在下面,你可以很容易地把它粘贴到你的环境中。
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") } }
测试优雅关闭和热重载配置
这与之前使用Golang的自动测试不同,此时我手动进行测试。这个测试需要打开两个终端窗口。
测试优雅关闭
首先,在终端一中运行服务器。如果你按上述步骤操作,你会看到与运行服务器后的截图类似的结果:
第二,在第二终端发送一个curl请求。
curl localhost:8080
如果你按上述步骤操作,运行服务器后,你会看到与截图类似的结果。
上面的截图意味着服务器在工作。
让我们先用Ctrl+C测试一下中断。
第三,回到第一终端,然后,用键盘输入Ctrl+C。你会看到下面类似的屏幕截图。
从下面的代码中输出 "通过Ctrl+C中断",你可以在函数multiSignalHandler()中轻松找到它们。
热重载
首先,由于服务器已经被优雅地关闭了,我们应该在终端一中再次运行服务器。
第二,让我们到第二终端。发送curl请求,以确保服务器工作。
第三,在第二终端,运行
lsof -i tcp:port-number
上面的命令是用来检查当前端口号在本地的PID。
第四,在第二终端,运行
kill -SIGHUP 43587
43587与截图中的PID相同。
第五,回到第一终端,如果你按部就班,你会看到类似下面的截图。
现在你会看到 "After hot reload "已经在终端一打印出来了。让我们回到终端二,发送一个 curl 请求来检查结果。
如截图所示,我们知道配置已经被热重载。
在完成上述所有工作后,我们可以用两种方式来终止程序。一种方式,在终端一中使用 "Ctrl+C "来做。另一种方式,使用
kill -9 port-number
在二号终端中,终止程序。