gracehttp: 優雅重啓 Go 程序(熱啓動 - Zero Downtime)

看完此篇你會知道,如何優雅的使用 HTTP Server

問題背景

http 應用程序重啓時,若是咱們直接 kill -9 使程序退出,而後在啓動,會有如下幾個問題:linux

  1. 舊的請求未處理完,若是服務端進程直接退出,會形成客戶端連接中斷(收到 RST);
  2. 新請求打過來,服務還沒重啓完畢,形成 connection refused
  3. 即便是要退出程序,直接 kill -9 仍然會讓正在處理的請求中斷;
  4. 面對海量請求,如何對連接數進行限制,並進行過載保護;
  5. 避免 open too many files 錯誤;

這些問題會形成很差的客戶體驗,嚴重的甚至影響客戶業務。因此,咱們須要以一種優雅的方式重啓/關閉咱們的應用,來達到熱啓動的效果,即:Zero Downtimegit

(Tips:名詞解釋)
熱啓動:新老程序(進程)無縫替換,同時能夠保持對client的服務。讓client端感受不到你的服務掛掉了;
Zero Downtime: 0 宕機時間,即不間斷的服務;

解決問題

Github: gracehttpgithub

平滑啓動

通常狀況下,咱們是退出舊版本,再啓動新版本,總會有時間間隔,時間間隔內的請求怎麼辦?並且舊版本正在處理請求怎麼辦?
那麼,針對這些問題,在升級應用過程當中,咱們須要達到以下目的:golang

  • 舊版本爲退出以前,須要先啓動新版本;
  • 舊版本繼續處理完已經接受的請求,而且再也不接受新請求;
  • 新版本接受並處理新請求的方式;

這樣,咱們就能實現 Zero Downtime 的升級效果。網絡

實現原理

首先,咱們須要用到如下基本知識:
1.linux 信號處理機制:在程序中,經過攔截 signal,並針對 signal 作出不一樣處理;
2.子進程繼承父進程的資源:一切皆文件,子進程會繼承父進程的資源句柄,網絡端口也是文件;
3.經過給子進程重啓標識(好比:重啓時帶着 -continue 參數),來實現子進程的初始化處理;併發

重啓時,咱們能夠在程序中捕獲 HUP 信號(經過 kill -HUP pid 能夠觸發),而後開啓新進程,退出舊進程。信號處理代碼示例以下:app

package gracehttp

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

var sig chan os.Signal
var notifySignals []os.Signal

func init() {
    sig = make(chan os.Signal)
    notifySignals = append(notifySignals, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGTSTP, syscall.SIGQUIT)
    signal.Notify(sig, notifySignals...) // 註冊須要攔截的信號
}

// 捕獲系統信號,並處理
func handleSignals() {
    capturedSig := <-sig
    srvLog.Info(fmt.Sprintf("Received SIG. [PID:%d, SIG:%v]", syscall.Getpid(), capturedSig))
    switch capturedSig {
    case syscall.SIGHUP: // 重啓信號
        startNewProcess() // 開啓新進程
        shutdown() // 退出舊進程
    case syscall.SIGINT:
        fallthrough
    case syscall.SIGTERM:
        fallthrough
    case syscall.SIGTSTP:
        fallthrough
    case syscall.SIGQUIT:
        shutdown()
    }
}

startNewProcess shutdown 具體實現能夠參考 Githubide

過載保護

經過限制 HTTP Serveraccept 數量實現連接數的限制,來達到若是併發量達到了最大值,客戶端超時時間內能夠等待,但不會消耗服務端文件句柄數(咱們知道 Linux 系統對用戶能夠打開的最大文件數有限制,網絡請求也是文件操做)ui

實現原理

  • 利用 channel 的緩衝機制實現,每一個請求都會獲取緩衝區的一個單元大小,知道緩衝區滿了,後邊的請求就會阻塞;
  • 若是客戶端請求被阻塞,達到了客戶端設置的超時時間,這時候連接會斷開,那咱們利用 goselect 機制,退出阻塞,並返回,再也不進行 accept

處理代碼以下:日誌

package gracehttp

// about limit @see: "golang.org/x/net/netutil"

import (
    "net"
    "sync"
    "time"
)

type Listener struct {
    *net.TCPListener
    sem       chan struct{}
    closeOnce sync.Once     // ensures the done chan is only closed once
    done      chan struct{} // no values sent; closed when Close is called
}

func newListener(tl *net.TCPListener, n int) net.Listener {
    return &Listener{
        TCPListener: tl,
        sem:         make(chan struct{}, n),
        done:        make(chan struct{}),
    }
}

func (l *Listener) Fd() (uintptr, error) {
    file, err := l.TCPListener.File()
    if err != nil {
        return 0, err
    }
    return file.Fd(), nil
}

// override
func (l *Listener) Accept() (net.Conn, error) {
    acquired := l.acquire()
    tc, err := l.AcceptTCP()
    if err != nil {
        if acquired {
            l.release()
        }
        return nil, err
    }
    tc.SetKeepAlive(true)
    tc.SetKeepAlivePeriod(time.Minute)

    return &ListenerConn{Conn: tc, release: l.release}, nil
}

// override
func (l *Listener) Close() error {
    err := l.TCPListener.Close()
    l.closeOnce.Do(func() { close(l.done) })
    return err
}

// acquire acquires the limiting semaphore. Returns true if successfully
// accquired, false if the listener is closed and the semaphore is not
// acquired.
func (l *Listener) acquire() bool {
    select {
    case <-l.done:
        return false
    case l.sem <- struct{}{}:
        return true
    }
}

func (l *Listener) release() { <-l.sem }

type ListenerConn struct {
    net.Conn
    releaseOnce sync.Once
    release     func()
}

func (l *ListenerConn) Close() error {
    err := l.Conn.Close()
    l.releaseOnce.Do(l.release)
    return err
}

參考:grace-http:listener.go

gracehttp

如今咱們把這個功能作得更優美有點,並提供一個開箱即用的代碼庫。
地址:Github-gracehttp

支持功能

  1. 平滑重啓(Zero-Downtime);
  2. 平滑關閉;
  3. Server 添加(支持 HTTPHTTPS);
  4. 自定義日誌組件;
  5. 支持單個端口 server 連接數限流,默認值爲:C100K。超過該限制以後,連接阻塞進入等待,可是不消耗系統文件句柄,避免發生雪崩,壓壞服務。

使用指南

添加服務

import "fevin/gracehttp"
    
    ....

    // http
    srv1 := &http.Server{
        Addr:    ":80",
        Handler: sc,
    }
    gracehttp.AddServer(srv1, false, "", "")

    // https

    srv2 := &http.Server{
        Addr:    ":443",
        Handler: sc,
    }
    gracehttp.AddServer(srv2, true, "../config/https.crt", "../config/https.key")

    gracehttp.Run() // 此方法會阻塞,直到進程收到退出信號,或者 panic

如上所示,只需建立好 Server 對象,調用 gracehttp.AddServer 添加便可。

退出或者重啓服務

  • 重啓:kill -HUP pid
  • 退出:kill -QUIT pid

添加自定義日誌組件

gracehttp.SetErrorLogCallback(logger.LogConfigLoadError)

此處提供了三個 Set* 方法,分別對應不一樣的日誌等級:

  • SetInfoLogCallback
  • SetNoticeLogCallback
  • SetErrorLogCallback

最後

實際中,不少狀況會用到這種方式,不妨點個 star 吧!歡迎一塊兒來完善這個小項目,共同貢獻代碼。

相關文章
相關標籤/搜索