Go 平滑重啟(優雅重啟)


問題背景

生產環境重要且復雜,許多的操作需要在任何場景都要保證正常運行。

如果我們對線上服務進行更新的步驟如下:

  1. kill -9服務
  2. 再啟動服務

那么將不可避免的出現以下兩個問題:

  • 未處理完的請求,被迫中斷,數據一致性被破壞
  • 新服務啟動期間,請求無法進來,導致一段時間的服務不可用現象

一般有三種方案處理以上問題:

  1. 生產環境會通過四層(lb)->七層(gateway)->服務,那么可以通過流量調度的方式實現平滑重啟
  2. k8s管理
  3. 程序自身完成平滑重啟(本章介紹)

什么事平滑重啟

進程在不關閉其所監聽端口的情況下進行重啟,並且重啟的整個過程保證所有請求都能被正確處理。

主要步驟:

  1. 原進程(父進程)先fork一個子進程,同時讓fork出來的子進程繼承父進程所監聽的socket
  2. 子進程完成初始化后,開始接收socket的請求。
  3. 父進程停止接收新的請求,並將當下的請求處理完,等待連接空閑后,平滑退出。

信號(Signal)

服務的平滑重啟,主要依賴進程接收的信號(實現進程間通信),這里簡單的介紹Golang中信號的處理:

發送信號

  • kill: 命令允許用戶發送一個特定的信號給進程
  • raise: 庫函數可以發送特定的信號給當前進程

在Linux下運行man kill可以查看此命令的介紹和用法。

kill -- terminate or signal a process
The kill utility sends a signal to the processes specified by the pid operands.
Only the super-user may send signals to other users' processes.

常用信號類型

信號的默認行為:

  • term:信號終止進程
  • core:產生核心轉儲文件並退出
  • ignore:忽略信號
  • stop:信號停止進程
  • cont:信號恢復一個已停止的進程
信號 默認動作 說明
SIGHUP 1 Term HUP (hang up):終端控制進程結束(終端連接斷開)
SIGINT 2 Term INT (interrupt):用戶發送INTR字符(Ctrl+C)觸發(強制進程結束)
SIGQUIT 3 Core QUIT (quit):用戶發送QUIT字符(Ctrl+/)觸發(進程結束)
SIGKILL 9 Term KILL (non-catchable, non-ignorable kill):無條件結束程序(不能被捕獲、阻塞或忽略)
SIGUSR1 30,10,16 Term 用戶自定義信號1
SIGUSR2 31,12,17 Term 用戶自定義信號2
SIGKILL 15 KILL (non-catchable, non-ignorable kill) TERM (software termination signal):程序終止信號

信號接收測試

package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    sigs := make(chan os.Signal)
    signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)

    // 監聽所有信號
    log.Println("listen sig")
    signal.Notify(sigs)

    // 打印進程id
    log.Println("PID:", os.Getppid())


    s := <-sigs
    log.Println("退出信號", s)
}

go run main.go
## --> listen sig ## --> PID: 4604 kill -s HUP 4604 # --> Hangup: 1

實現案例

demo:

func main() {
   sigs := make(chan os.Signal)
   signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)

   // 監聽所有信號
   log.Println("listen sig")
   signal.Notify(sigs)


   // 打印進程id
   log.Println("PID:", os.Getppid())
   go func() {
      for s := range sigs {
         switch s {
         case syscall.SIGHUP:
            log.Println("startNewProcess...")
            startNewProcess()
            log.Println("shutdownParentProcess...")
            shutdownParentProcess()
         case syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
            log.PrintLn("Program Exit...", s)
         case syscall.SIGUSR1:
            log.Println("usr1 signal", s)
         case syscall.SIGUSR2:
            log.Println("usr2 signal", s)
         default:
            log.Println("other signal", s)
         }
      }
   }()

   <-sigs
}

 

推薦組件

Facebookarchive/grace

shutdown優雅退出

go 1.8.x后,golang在http里加入了shutdown方法,用來控制優雅退出。

package main

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

func main() {
    s := http.NewServeMux()
    s.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(3 * time.Second)
        log.Println(w, "Hello world!")
    })
    server := &http.Server{
        Addr:    ":8090",
        Handler: s,
    }
    go server.ListenAndServe()

    listenSignal(context.Background(), server)
}

func listenSignal(ctx context.Context, httpSrv *http.Server) {
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)

    select {
    case <-sigs:
        log.Println("notify sigs")
        httpSrv.Shutdown(ctx)
        log.Println("http shutdown")
    }
}

 

 

小結

在平常的生產環境中,優雅的重啟是一個不可缺少的環節,無論是在go進程層間,或者上層的服務流量調度層面,都有許多的方案,選擇最適合團隊,保證項目穩定才是最重要的。

參考文章

https://github.com/facebookarchive/grace

https://mp.weixin.qq.com/s/UVZKFmv8p4ghm8ICdz85wQ


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM