1. 程式人生 > 實用技巧 >優雅地關機或重啟

優雅地關機或重啟

文章轉自

優雅地關機或重啟

我們編寫的Web專案部署之後,經常會因為需要進行配置變更或功能迭代而重啟服務,單純的kill -9 pid的方式會強制關閉程序,這樣就會導致服務端當前正在處理的請求失敗,那有沒有更優雅的方式來實現關機或重啟呢?

閱讀本文需要了解一些UNIX系統中訊號的概念,請提前查閱資料預習。

優雅地關機

什麼是優雅關機?

優雅關機就是服務端關機命令發出後不是立即關機,而是等待當前還在處理的請求全部處理完畢後再退出程式,是一種對客戶端友好的關機方式。而執行Ctrl+C關閉服務端時,會強制結束程序導致正在訪問的請求出現問題。

如何實現優雅關機?

Go 1.8版本之後, http.Server 內建的

Shutdown() 方法就支援優雅地關機,具體示例如下:

// +build go1.8

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		c.String(http.StatusOK, "Welcome Gin Server")
	})

	srv := &http.Server{
		Addr:    ":8080",
		Handler: router,
	}

	go func() {
		// 開啟一個goroutine啟動服務
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("listen: %s\n", err)
		}
	}()

	// 等待中斷訊號來優雅地關閉伺服器,為關閉伺服器操作設定一個5秒的超時
	quit := make(chan os.Signal, 1) // 建立一個接收訊號的通道
	// kill 預設會發送 syscall.SIGTERM 訊號
	// kill -2 傳送 syscall.SIGINT 訊號,我們常用的Ctrl+C就是觸發系統SIGINT訊號
	// kill -9 傳送 syscall.SIGKILL 訊號,但是不能被捕獲,所以不需要新增它
	// signal.Notify把收到的 syscall.SIGINT或syscall.SIGTERM 訊號轉發給quit
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)  // 此處不會阻塞
	<-quit  // 阻塞在此,當接收到上述兩種訊號時才會往下執行
	log.Println("Shutdown Server ...")
	// 建立一個5秒超時的context
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	// 5秒內優雅關閉服務(將未處理完的請求處理完再關閉服務),超過5秒就超時退出
	if err := srv.Shutdown(ctx); err != nil {
		log.Fatal("Server Shutdown: ", err)
	}

	log.Println("Server exiting")
}

  

如何驗證優雅關機的效果呢?

上面的程式碼執行後會在本地的8080埠開啟一個web服務,它只註冊了一條路由/,後端服務會先sleep 5秒鐘然後才返回響應資訊。

我們按下Ctrl+C時會發送syscall.SIGINT來通知程式優雅關機,具體做法如下:

  1. 開啟終端,編譯並執行上面的程式碼
  2. 開啟一個瀏覽器,訪問127.0.0.1:8080/,此時瀏覽器白屏等待服務端返回響應。
  3. 在終端迅速執行Ctrl+C命令給程式傳送syscall.SIGINT訊號
  4. 此時程式並不立即退出而是等我們第2步的響應返回之後再退出,從而實現優雅關機。

優雅地重啟

優雅關機實現了,那麼該如何實現優雅重啟呢?

我們可以使用 fvbock/endless

來替換預設的 ListenAndServe啟動服務來實現, 示例程式碼如下:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/fvbock/endless"
	"github.com/gin-gonic/gin"
)

func main() {
	router := gin.Default()
	router.GET("/", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		c.String(http.StatusOK, "hello gin!")
	})
	// 預設endless伺服器會監聽下列訊號:
	// syscall.SIGHUP,syscall.SIGUSR1,syscall.SIGUSR2,syscall.SIGINT,syscall.SIGTERM和syscall.SIGTSTP
	// 接收到 SIGHUP 訊號將觸發`fork/restart` 實現優雅重啟(kill -1 pid會發送SIGHUP訊號)
	// 接收到 syscall.SIGINT或syscall.SIGTERM 訊號將觸發優雅關機
	// 接收到 SIGUSR2 訊號將觸發HammerTime
	// SIGUSR1 和 SIGTSTP 被用來觸發一些使用者自定義的hook函式
	if err := endless.ListenAndServe(":8080", router); err!=nil{
		log.Fatalf("listen: %s\n", err)
	}

	log.Println("Server exiting")
}

  

如何驗證優雅重啟的效果呢?

我們通過執行kill -1 pid命令傳送syscall.SIGINT來通知程式優雅重啟,具體做法如下:

  1. 開啟終端,go build -o graceful_restart編譯並執行./graceful_restart,終端輸出當前pid(假設為43682)
  2. 將程式碼中處理請求函式返回的hello gin!修改為hello q1mi!,再次編譯go build -o graceful_restart
  3. 開啟一個瀏覽器,訪問127.0.0.1:8080/,此時瀏覽器白屏等待服務端返回響應。
  4. 在終端迅速執行kill -1 43682命令給程式傳送syscall.SIGHUP訊號
  5. 等第3步瀏覽器收到響應資訊hello gin!後再次訪問127.0.0.1:8080/會收到hello q1mi!的響應。
  6. 在不影響當前未處理完請求的同時完成了程式程式碼的替換,實現了優雅重啟。

但是需要注意的是,此時程式的PID變化了,因為endless 是通過fork子程序處理新請求,待原程序處理完當前請求後再退出的方式實現優雅重啟的。所以當你的專案是使用類似supervisor的軟體管理程序時就不適用這種方式了。

總結

無論是優雅關機還是優雅重啟歸根結底都是通過監聽特定系統訊號,然後執行一定的邏輯處理保障當前系統正在處理的請求被正常處理後再關閉當前程序。使用優雅關機還是使用優雅重啟以及怎麼實現,這就需要根據專案實際情況來決定了。