詳解Go語言如何熱重載和優(yōu)雅地關(guān)閉程序
我們有時會因不同的目的去關(guān)閉服務(wù),一種關(guān)閉服務(wù)是終止操作系統(tǒng),一種關(guān)閉服務(wù)是用來更新配置。
我們希望優(yōu)雅地關(guān)閉服務(wù)和通過熱重載重新加載配置,而這兩種方式可以通過信號包來完成。
1、代碼實現(xiàn)
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 結(jié)構(gòu)并聲明了一個 conf 變量。
type Config struct {
Message string
}
var conf = &Config{Message: "Before hot reload"}這里的代碼只是一個簡單的配置樣本,你可以根據(jù)自己的需要定義一個復(fù)雜的結(jié)構(gòu)。
其次,定義一個路由器函數(shù),用來綁定和監(jiān)聽 8080 端口。在熱重載配置完成后,它也被用來顯示結(jié)果。
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))
}()
}下一步是服務(wù)器關(guān)機和熱重載配置,當(dāng)一個服務(wù)器關(guān)閉時,它應(yīng)該停止接收新的請求,同時完成正在進行的請求,
返回其響應(yīng),然后關(guān)閉,在這里使用信號包實現(xiàn)。
sigCh := make(chan os.Signal, 1)
之后,使用 signal.Notify() 一起發(fā)送更多的信號。
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
當(dāng)程序被中斷時,signal.Notify 將向 sigCh 通道發(fā)送一個信號。
syscall.SIGHUP、syscall.SIGINT 和 syscall.SIGTERM 是什么意思?
- syscall.SIGINT 是用來在 Ctrl+C 時優(yōu)雅地關(guān)閉的,它也相當(dāng)于 os.Interrupt。
- syscall.SIGTERM 是常用的終止信號,也是 docker 容器的默認(rèn)信號,Kubernetes 也使用它。
- syscall.SIGHUP 用于熱重載配置。
如何優(yōu)雅地關(guān)閉,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、測試優(yōu)雅關(guān)閉
首先,運行服務(wù)器。
$ go run main.go
2023/06/25 09:57:05 starting up....
發(fā)送一個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、熱重載
首先,運行服務(wù)器。
$ go run main.go
2023/06/24 22:03:17 starting up....
然后,發(fā)送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
在官方介紹中,這個庫主要封裝信號實現(xiàn)對輸入信號的訪問,信號主要用于類 Unix 系統(tǒng)。
信號是事件發(fā)生時對進程的通知機制,有時也稱之為軟件中斷。信號與硬件中斷的相似之處在于打斷了程序執(zhí)行的正常流程,大多數(shù)情況下,無法預(yù)測信號到達的精確時間。
因為一個具有合適權(quán)限的進程可以向另一個進程發(fā)送信號,這可以稱為進程間的一種同步技術(shù)。當(dāng)然,進程也可以向自身發(fā)送信號。然而,發(fā)往進程的諸多信號,通常都是源于內(nèi)核。引發(fā)內(nèi)核為進程產(chǎn)生信號的各類事件如下:
硬件發(fā)生異常,即硬件檢測到一個錯誤條件并通知內(nèi)核,隨即再由內(nèi)核發(fā)送相應(yīng)信號給相關(guān)進程。比如執(zhí)行一條異常的機器語言指令(除0,引用無法訪問的內(nèi)存區(qū)域)。
用戶鍵入了能夠產(chǎn)生信號的終端特殊字符。如中斷字符 (通常是 Control-C)、暫停字符(通常是 Control-Z)。
發(fā)生了軟件事件。如調(diào)整了終端窗口大小,定時器到期等。
4.1 舉例說明
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
// os.Signal是一個系統(tǒng)信號接收channel
c := make(chan os.Signal, 1)
// syscall都是一些系統(tǒng)信號
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,我們必須使用緩沖通道,否則在信號發(fā)送時如果還沒有準(zhǔn)備好接收信號,就有丟失信號的風(fēng)險。
2、signal.notify 用于監(jiān)聽信號,參數(shù)1表示接收信號的 channel,參數(shù)2及后面的表示要監(jiān)聽的信號:
- syscall.SIGHUP 表示終端控制進程結(jié)束
- syscall.SIGQUIT 表示用戶發(fā)送QUIT字符 (Ctrl+/) 觸發(fā)
- syscall.SIGTERM 表示結(jié)束進程
- syscall.SIGINT 表示用戶發(fā)送INTR字符 (Ctrl+C) 觸發(fā)
3、<-c 一直阻塞直到接收到信號退出。
對于上面的的程序是優(yōu)雅的退出守護進程,接下來就是一些釋放資源或dump進程當(dāng)前狀態(tài)或記錄日志的動作,完成這些后,主進程退出。
4.2 GO的信號類型
4.2.1 POSIX.1-1990標(biāo)準(zhǔn)中定義的信號列表

4.2.2 在SUSv2和POSIX.1-2001標(biāo)準(zhǔn)中的信號列表

信號 SIGKILL 和 SIGSTOP 可能不會被程序捕獲,因此不會受此軟件包影響。
同步信號是由程序執(zhí)行中的錯誤觸發(fā)的信號:SIGBUS,SIGFPE 和 SIGSEGV。這些只在程序執(zhí)行時才被認(rèn)為是同步的,而不是在使用 os.Process.Kill 或 kill 程序或類似的機制發(fā)送時。一般來說,除了如下所述,Go 程序會將同步信號轉(zhuǎn)換為運行時異常。其余信號是異步信號,它們不是由程序錯誤觸發(fā)的,而是從內(nèi)核或其他程序發(fā)送的。
在異步信號中,SIGHUP 信號在程序失去其控制終端時發(fā)送。當(dāng)控制終端的用戶按下中斷字符(默認(rèn)為^ C
(Control-C))時,發(fā)送 SIGINT 信號。當(dāng)控制終端的用戶按下退出字符時發(fā)送 SIGQUIT 信號,默認(rèn)為^ \
(Control-Backslash)。一般情況下,您可以通過按^ C來使程序簡單地退出,并且可以通過按^使堆棧轉(zhuǎn)儲退出。
4.3 Kill命令的原理
我們平時在 Linux 系統(tǒng)會 kill 命令來殺死進程,那其中的原理是什么呢。
4.3.1 kill pid
kill pid 的作用是向進程號為 pid 的進程發(fā)送 SIGTERM (這是 kill 默認(rèn)發(fā)送的信號),該信號是一個結(jié)束進程的信號且可以被應(yīng)用程序捕獲。若應(yīng)用程序沒有捕獲并響應(yīng)該信號的邏輯代碼,則該信號的默認(rèn)動作是 kill 掉進程。這是終止指定進程的推薦做法。
4.3.2 kill -9 pid
kill -9 pid 則是向進程號為 pid 的進程發(fā)送 SIGKILL (該信號的編號為9),從本文上面的說明可知,SIGKILL 既不能被應(yīng)用程序捕獲,也不能被阻塞或忽略,其動作是立即結(jié)束指定進程。通俗地說,應(yīng)用程序根本無法感知 SIGKILL信號,它在完全無準(zhǔn)備的情況下,就被收到 SIGKILL 信號的操作系統(tǒng)給干掉了,顯然,在這種暴力情況下,應(yīng)用程序完全沒有釋放當(dāng)前占用資源的機會。事實上,SIGKILL 信號是直接發(fā)給 init 進程的,它收到該信號后,負(fù)責(zé)終止 pid 指定的進程。在某些情況下(如進程已經(jīng) hang 死,無響應(yīng)正常信號),就可以使用 kill -9 來結(jié)束進程。
到此這篇關(guān)于詳解Go語言如何熱重載和優(yōu)雅地關(guān)閉程序的文章就介紹到這了,更多相關(guān)Go語言優(yōu)雅關(guān)閉程序內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

