讀 "優雅關閉的 Go Web 伺服器"

stayfoo發表於2019-08-31

讀 "優雅關閉的 Go Web 伺服器"

GitHub 倉庫:https://github.com/stayfoo/stayfoo-hub

文章《優雅的關閉 Go Web 伺服器》( Go 語言中文網 ) 寫到:可以通過開啟一個單獨的 goroutine 攔截關閉訊號,這樣,當伺服器真正關閉之前,可以做一些任務,做完任務再發出執行完畢的關閉號。

一些任務比如:清理某些資源;完成資料庫事務;一些其他長時間的操作;退出服務的那一刻,剛好收到一個響應,為了保證所有請求完成,就可以在這裡,在最大超時時間內去處理這個響應;dump 程式當前狀態;記錄日誌的動作。

啟動應用,Ctrl + C 中斷程式,中斷訊號被攔截,do something.....

go run main.go -listen-addr=5001
http: 2019/08/31 11:34:30 Server is ready to handle requests at :5001
^Chttp: 2019/08/31 11:34:32 Server is shutting down...
do something start .....  2019-08-31 11:34:32.594668 +0800 CST m=+2.337451148
do something end .....  2019-08-31 11:34:37.598248 +0800 CST m=+7.340881516
http: 2019/08/31 11:34:37 Server stopped

對文中程式碼做了改造,程式碼如下:

var listenAddr string

func init() {
    //接收埠號,預設埠號:5000
    flag.StringVar(&listenAddr, "listen-addr", ":5000", "server listen address")
}
func main() {
    flag.Parse() //外部引數解析
    listenAddr = fmt.Sprintf(":%s",listenAddr)

    logger := log.New(os.Stdout, "http: ", log.LstdFlags)

    //建立 server:
    server := newWebServer(logger)

    done := make(chan struct{}, 1)
    quit := make(chan  os.Signal, 1)

    //os.Interrupt: syscall.SIGINT
    signal.Notify(quit, os.Interrupt)
    //啟動另一個 goroutine ,監聽將要關閉訊號:
    go shutdown(server, logger, quit, done)

    //啟動 server:
    logger.Println("Server is ready to handle requests at",listenAddr)
    err := server.ListenAndServe()
    if err != nil && err != http.ErrServerClosed {
        logger.Fatalf("Could not listen on %s: %v \n", listenAddr, err)
    }

    //等待已經關閉的訊號:
    <-done
    logger.Println("Server stopped")
}

//初始化 server
func newWebServer(logger *log.Logger) *http.Server {
    //路由:
    router := http.NewServeMux()
    router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })

    //http 服務配置:
    server := &http.Server{
        Addr: listenAddr,
        Handler: router,
        ErrorLog: logger,
        ReadTimeout: 5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout: 15 * time.Second,
    }

    return server
}

//關閉 server
//quit: 接收關閉訊號
//done: 發出已經關閉訊號
func shutdown(server *http.Server, logger *log.Logger, quit <-chan os.Signal, done chan<- struct{}) {
    //等待接收到退出訊號:
    <-quit
    logger.Println("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 {
        logger.Fatalf("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)
}

改造的地方

原文程式碼:

done := make(chan bool, 1)

改造後程式碼:

done := make(chan struct{}, 1)

可以看到,把關閉訊號從 chan bool 改成了 chan struct{}

好處:空 struct{} 不佔用空間,chan bool 佔 1 位元組。空 struct{} 更節省資源。

由下面測試程式碼可以知道:

s := struct {}{}
b := true
fmt.Println("struct: ", unsafe.Sizeof(s), " bool: ", unsafe.Sizeof(b)) 
//列印結果:struct:  0  bool:  1

一些其他

Go 中的訊號

os/signal 包對訊號處理。

  • 監聽收到的訊號:
func Notify(c chan<- os.Signal, sig ...os.Signal)
  • 取消監聽:
func Stop(c chan<- os.Signal)
  • 監聽 Interrupt 訊號,使用者 Ctrl+C 觸發:
quit := make(chan  os.Signal, 1)
//os.Interrupt: syscall.SIGINT
signal.Notify(quit, os.Interrupt)   
  • 監聽所有訊號:
c := make(chan os.Signal)
signal.Notify(c)
  • 監聽多個指定訊號:
c := make(chan os.Signal)
signal.Notify(c,os.Interrupt, os.Kill, syscall.SIGQUIT)
  • 一些訊號:
//作業系統收到訊號後的動作:
//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)
)
更多原創文章乾貨分享,請關注公眾號
  • 讀 "優雅關閉的 Go Web 伺服器"
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章