主頁 > 知識庫 > Golang學(xué)習(xí)之平滑重啟

Golang學(xué)習(xí)之平滑重啟

熱門標(biāo)簽:江西轉(zhuǎn)化率高的羿智云外呼系統(tǒng) 中國地圖標(biāo)注省會高清 高德地圖標(biāo)注口訣 廣州呼叫中心外呼系統(tǒng) 地圖標(biāo)注的汽車標(biāo) 南通如皋申請開通400電話 浙江高速公路地圖標(biāo)注 學(xué)海導(dǎo)航地圖標(biāo)注 西部云谷一期地圖標(biāo)注

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

與重載配置相同的是我們也需要通過信號來通知server重啟,但關(guān)鍵在于平滑重啟,如果只是簡單的重啟,只需要kill掉,然后再拉起即可。平滑重啟意味著server升級的時候可以不用停止業(yè)務(wù)。

我們先來看下Github上有沒有相應(yīng)的庫解決這個問題,然后找到了如下三個庫:

  • 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)

我們分別來學(xué)習(xí)一下,下面只講解http server的重啟。

使用方式

我們來分別使用這三個庫來做平滑重啟的事情,之后來對比其優(yōu)缺點。

這三個庫的官方都給了相應(yīng)的例子,例子如下:

但三個庫官方的例子不太一致,我們來統(tǒng)一一下:

  • grace例子 https://github.com/facebookgo/grace/blob/master/gracedemo/demo.go
  • endless例子 https://github.com/fvbock/endless/tree/master/examples
  • overseer例子 https://github.com/jpillora/overseer/tree/master/example

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

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
  })
}

對比

對比示例的操作步驟

  • 分別構(gòu)建上面的示例,并記錄pid
  • 調(diào)用API,在其未返回時,修改內(nèi)容(Hello World -> Hello Harry),重新構(gòu)建。查看舊API是否返回舊的內(nèi)容
  • 調(diào)用新API,查看返回的內(nèi)容是否是新的內(nèi)容
  • 查看當(dāng)前運(yùn)行的pid,是否與之前一致

下面給一下操作命令

# 第一次構(gòu)建項目
go build grace.go
# 運(yùn)行項目,這時就可以做內(nèi)容修改了
./grace 
# 請求項目,60s后返回
curl "http://127.0.0.1:5001/sleep?duration=60s" 
# 再次構(gòu)建項目,這里是新內(nèi)容
go build grace.go
# 重啟,2096為pid
kill -USR2 2096
# 新API請求
curl "http://127.0.0.1:5001/sleep?duration=1s"


# 第一次構(gòu)建項目
go build endless.go
# 運(yùn)行項目,這時就可以做內(nèi)容修改了
./endless 
# 請求項目,60s后返回
curl "http://127.0.0.1:5003/sleep?duration=60s" 
# 再次構(gòu)建項目,這里是新內(nèi)容
go build endless.go
# 重啟,22072為pid
kill -1 22072
# 新API請求
curl "http://127.0.0.1:5003/sleep?duration=1s"


# 第一次構(gòu)建項目
go build -ldflags '-X main.BuildID=1' overseer.go
# 運(yùn)行項目,這時就可以做內(nèi)容修改了
./overseer 
# 請求項目,60s后返回
curl "http://127.0.0.1:5005/sleep?duration=60s" 
# 再次構(gòu)建項目,這里是新內(nèi)容,注意版本號不同了
go build -ldflags '-X main.BuildID=2' overseer.go
# 重啟,28300為主進(jìn)程pid
kill -USR2 28300
# 新API請求
curl http://127.0.0.1:5005/sleep?duration=1s

對比結(jié)果

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

原理分析

可以看出grace和endless是比較像的。

  • 監(jiān)聽信號
  • 收到信號時fork子進(jìn)程(使用相同的啟動命令),將服務(wù)監(jiān)聽的socket文件描述符傳遞給子進(jìn)程
  • 子進(jìn)程監(jiān)聽父進(jìn)程的socket,這個時候父進(jìn)程和子進(jìn)程都可以接收請求
  • 子進(jìn)程啟動成功之后,父進(jìn)程停止接收新的連接,等待舊連接處理完成(或超時)
  • 父進(jìn)程退出,升級完成

overseer是不同的,主要是overseer加了一個主進(jìn)程管理平滑重啟,子進(jìn)程處理鏈接,能夠保持主進(jìn)程pid不變。

如下圖表示的很形象

自己實現(xiàn)

我們下面來嘗試自己實現(xiàn)下第一種處理,代碼如下,代碼來自《熱重啟golang服務(wù)器》:

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

關(guān)于這一部分,個人的理解也不是特別深入,如果又不正確的地方請大家指正。

參考文章

熱重啟golang服務(wù)器

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

您可能感興趣的文章:
  • golang flag簡單用法
  • Golang中定時器的陷阱詳解
  • 在golang中操作mysql數(shù)據(jù)庫的實現(xiàn)代碼
  • golang設(shè)置http response響應(yīng)頭與填坑記錄
  • 詳解Golang實現(xiàn)http重定向https的方式
  • Golang編譯器介紹

標(biāo)簽:吐魯番 德宏 常州 貴州 曲靖 東營 保定 許昌

巨人網(wǎng)絡(luò)通訊聲明:本文標(biāo)題《Golang學(xué)習(xí)之平滑重啟》,本文關(guān)鍵詞  Golang,學(xué),習(xí)之,平滑,重啟,;如發(fā)現(xiàn)本文內(nèi)容存在版權(quán)問題,煩請?zhí)峁┫嚓P(guān)信息告之我們,我們將及時溝通與處理。本站內(nèi)容系統(tǒng)采集于網(wǎng)絡(luò),涉及言論、版權(quán)與本站無關(guān)。
  • 相關(guān)文章
  • 下面列出與本文章《Golang學(xué)習(xí)之平滑重啟》相關(guān)的同類信息!
  • 本頁收集關(guān)于Golang學(xué)習(xí)之平滑重啟的相關(guān)信息資訊供網(wǎng)民參考!
  • 推薦文章