Golang學習--平滑重啓

在上一篇博客介紹TOML配置的時候,講到了經過信號通知重載配置。咱們在這一篇中介紹下如何的平滑重啓server。git

與重載配置相同的是咱們也須要經過信號來通知server重啓,但關鍵在於平滑重啓,若是隻是簡單的重啓,只須要kill掉,而後再拉起便可。平滑重啓意味着server升級的時候能夠不用中止業務。github

咱們先來看下Github上有沒有相應的庫解決這個問題,而後找到了以下三個庫:golang

  • facebookgo/grace - Graceful restart & zero downtime deploy for Go servers.
  • fvbock/endless - Zero downtime restarts for go servers (Drop in replacement for http.ListenAndServe)
  • jpillora/overseer - Monitorable, gracefully restarting, self-upgrading binaries in Go (golang)

咱們分別來學習一下,下面只講解http server的重啓。shell

使用方式

咱們來分別使用這三個庫來作平滑重啓的事情,以後來對比其優缺點。服務器

這三個庫的官方都給了相應的例子,例子以下:app

但三個庫官方的例子不太一致,咱們來統一一下:less

咱們參考官方的例子分別來寫下用來對比的例子:curl

grace

package main

import (
    "time"
    "net/http"
    "github.com/facebookgo/grace/gracehttp"
)

func main() {
    gracehttp.Serve(
        &http.Server{Addr: ":5001", Handler: newGraceHandler()},
        &http.Server{Addr: ":5002", Handler: newGraceHandler()},
    )
}

func newGraceHandler() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/sleep", func(w http.ResponseWriter, r *http.Request) {
        duration, err := time.ParseDuration(r.FormValue("duration"))
        if err != nil {
            http.Error(w, err.Error(), 400)
            return
        }
        time.Sleep(duration)
        w.Write([]byte("Hello World"))
    })
    return mux
}

endless

package main

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

    "github.com/fvbock/endless"
    "github.com/gorilla/mux"
)

func handler(w http.ResponseWriter, r *http.Request) {
    duration, err := time.ParseDuration(r.FormValue("duration"))
    if err != nil {
        http.Error(w, err.Error(), 400)
        return
    }
    time.Sleep(duration)
    w.Write([]byte("Hello World"))
}

func main() {
    mux1 := mux.NewRouter()
    mux1.HandleFunc("/sleep", handler)

    w := sync.WaitGroup{}
    w.Add(2)
    go func() {
        err := endless.ListenAndServe(":5003", mux1)
        if err != nil {
            log.Println(err)
        }
        log.Println("Server on 5003 stopped")
        w.Done()
    }()
    go func() {
        err := endless.ListenAndServe(":5004", mux1)
        if err != nil {
            log.Println(err)
        }
        log.Println("Server on 5004 stopped")
        w.Done()
    }()
    w.Wait()
    log.Println("All servers stopped. Exiting.")

    os.Exit(0)
}

overseer

package main

import (
    "fmt"
    "net/http"
    "time"

    "github.com/jpillora/overseer"
)

//see example.sh for the use-case

// BuildID is compile-time variable
var BuildID = "0"

//convert your 'main()' into a 'prog(state)'
//'prog()' is run in a child process
func prog(state overseer.State) {
    fmt.Printf("app#%s (%s) listening...\n", BuildID, state.ID)
    http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        duration, err := time.ParseDuration(r.FormValue("duration"))
        if err != nil {
            http.Error(w, err.Error(), 400)
            return
        }
        time.Sleep(duration)
        w.Write([]byte("Hello World"))
        fmt.Fprintf(w, "app#%s (%s) says hello\n", BuildID, state.ID)
    }))
    http.Serve(state.Listener, nil)
    fmt.Printf("app#%s (%s) exiting...\n", BuildID, state.ID)
}

//then create another 'main' which runs the upgrades
//'main()' is run in the initial process
func main() {
    overseer.Run(overseer.Config{
        Program: prog,
        Addresses: []string{":5005", ":5006"},
        //Fetcher: &fetcher.File{Path: "my_app_next"},
        Debug:   false, //display log of overseer actions
    })
}

對比

對比示例的操做步驟

  • 分別構建上面的示例,並記錄pid
  • 調用API,在其未返回時,修改內容(Hello World -> Hello Harry),從新構建。查看舊API是否返回舊的內容
  • 調用新API,查看返回的內容是不是新的內容
  • 查看當前運行的pid,是否與以前一致

下面給一下操做命令socket

# 第一次構建項目
go build grace.go
# 運行項目,這時就能夠作內容修改了
./grace &
# 請求項目,60s後返回
curl "http://127.0.0.1:5001/sleep?duration=60s" &
# 再次構建項目,這裏是新內容
go build grace.go
# 重啓,2096爲pid
kill -USR2 2096
# 新API請求
curl "http://127.0.0.1:5001/sleep?duration=1s"


# 第一次構建項目
go build endless.go
# 運行項目,這時就能夠作內容修改了
./endless &
# 請求項目,60s後返回
curl "http://127.0.0.1:5003/sleep?duration=60s" &
# 再次構建項目,這裏是新內容
go build endless.go
# 重啓,22072爲pid
kill -1 22072
# 新API請求
curl "http://127.0.0.1:5003/sleep?duration=1s"


# 第一次構建項目
go build -ldflags '-X main.BuildID=1' overseer.go
# 運行項目,這時就能夠作內容修改了
./overseer &
# 請求項目,60s後返回
curl "http://127.0.0.1:5005/sleep?duration=60s" &
# 再次構建項目,這裏是新內容,注意版本號不一樣了
go build -ldflags '-X main.BuildID=2' overseer.go
# 重啓,28300爲主進程pid
kill -USR2 28300
# 新API請求
curl "http://127.0.0.1:5005/sleep?duration=1s"

對比結果

示例 舊API返回值 新API返回值 舊pid 新pid 結論
grace Hello world Hello Harry 2096 3100 舊API不會斷掉,會執行原來的邏輯,pid會變化
endless Hello world Hello Harry 22072 22365 舊API不會斷掉,會執行原來的邏輯,pid會變化
overseer Hello world Hello Harry 28300 28300 舊API不會斷掉,會執行原來的邏輯,主進程pid不會變化

原理分析

能夠看出grace和endless是比較像的。tcp

  • 監聽信號
  • 收到信號時fork子進程(使用相同的啓動命令),將服務監聽的socket文件描述符傳遞給子進程
  • 子進程監聽父進程的socket,這個時候父進程和子進程均可以接收請求
  • 子進程啓動成功以後,父進程中止接收新的鏈接,等待舊鏈接處理完成(或超時)
  • 父進程退出,升級完成

overseer是與grace和endless有些不一樣,主要是兩點:

  1. overseer添加了Fetcher,當Fetcher返回有效的二進位流(io.Reader) 時,主進程會將它保存到臨時位置並驗證它,替換當前的二進制文件並啓動。
    Fetcher運行在一個goroutine中,預先會配置好檢查的間隔時間。Fetcher支持File、GitHub、HTTP和S3的方式。詳細可查看包package fetcher
  2. overseer添加了一個主進程管理平滑重啓。子進程處理鏈接,可以保持主進程pid不變。

以下圖表示的很形象

本身實現

咱們下面來嘗試本身實現下第一種處理,代碼以下,代碼來自《熱重啓golang服務器》:

package main
import (
    "context"
    "errors"
    "flag"
    "log"
    "net"
    "net/http"
    "os"
    "os/exec"
    "os/signal"
    "syscall"
    "time"
)

var (
    server   *http.Server
    listener net.Listener
    graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
)

func sleep(w http.ResponseWriter, r *http.Request) {
    duration, err := time.ParseDuration(r.FormValue("duration"))
    if err != nil {
        http.Error(w, err.Error(), 400)
        return
    }
    time.Sleep(duration)
    w.Write([]byte("Hello World"))
}

func main() {
    flag.Parse()

    http.HandleFunc("/sleep", sleep)
    server = &http.Server{Addr: ":5007"}

    var err error
    if *graceful {
        log.Print("main: Listening to existing file descriptor 3.")
        // cmd.ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
        // when we put socket FD at the first entry, it will always be 3(0+3)
        f := os.NewFile(3, "")
        listener, err = net.FileListener(f)
    } else {
        log.Print("main: Listening on a new file descriptor.")
        listener, err = net.Listen("tcp", server.Addr)
    }

    if err != nil {
        log.Fatalf("listener error: %v", err)
    }

    go func() {
        // server.Shutdown() stops Serve() immediately, thus server.Serve() should not be in main goroutine
        err = server.Serve(listener)
        log.Printf("server.Serve err: %v\n", err)
    }()
    signalHandler()
    log.Printf("signal end")
}

func reload() error {
    tl, ok := listener.(*net.TCPListener)
    if !ok {
        return errors.New("listener is not tcp listener")
    }

    f, err := tl.File()
    if err != nil {
        return err
    }

    args := []string{"-graceful"}
    cmd := exec.Command(os.Args[0], args...)
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    // put socket FD at the first entry
    cmd.ExtraFiles = []*os.File{f}
    return cmd.Start()
}

func signalHandler() {
    ch := make(chan os.Signal, 1)
    signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
    for {
        sig := <-ch
        log.Printf("signal: %v", sig)

        // timeout context for shutdown
        ctx, _ := context.WithTimeout(context.Background(), 100*time.Second)
        switch sig {
        case syscall.SIGINT, syscall.SIGTERM:
            // stop
            log.Printf("stop")
            signal.Stop(ch)
            server.Shutdown(ctx)
            log.Printf("graceful shutdown")
            return
        case syscall.SIGUSR2:
            // reload
            log.Printf("reload")
            err := reload()
            if err != nil {
                log.Fatalf("graceful restart error: %v", err)
            }
            server.Shutdown(ctx)
            log.Printf("graceful reload")
            return
        }
    }
}

代碼可參考:https://github.com/CraryPrimitiveMan/go-in-action/tree/master/ch4

關於這一部分,我的的理解也不是特別深刻,若是又不正確的地方請你們指正。

參考文章

熱重啓golang服務器
overseer, 進入(golang),Monitorable,正常從新啟動,自升級二進位文件

相關文章
相關標籤/搜索