在上一篇博客介紹TOML配置的時(shí)候,講到了通過信號(hào)通知重載配置。我們?cè)谶@一篇中介紹下如何的平滑重啟server。
與重載配置相同的是我們也需要通過信號(hào)來通知server重啟,但關(guān)鍵在于平滑重啟,如果只是簡單的重啟,只需要kill掉,然后再拉起即可。平滑重啟意味著server升級(jí)的時(shí)候可以不用停止業(yè)務(wù)。
我們先來看下Github上有沒有相應(yīng)的庫解決這個(gè)問題,然后找到了如下三個(gè)庫:
- 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的重啟。
使用方式
我們來分別使用這三個(gè)庫來做平滑重啟的事情,之后來對(duì)比其優(yōu)缺點(diǎn)。
這三個(gè)庫的官方都給了相應(yīng)的例子,例子如下:
但三個(gè)庫官方的例子不太一致,我們來統(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
我們參考官方的例子分別來寫下用來對(duì)比的例子:
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
})
}
對(duì)比
對(duì)比示例的操作步驟
- 分別構(gòu)建上面的示例,并記錄pid
- 調(diào)用API,在其未返回時(shí),修改內(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)建項(xiàng)目
go build grace.go
# 運(yùn)行項(xiàng)目,這時(shí)就可以做內(nèi)容修改了
./grace
# 請(qǐng)求項(xiàng)目,60s后返回
curl "http://127.0.0.1:5001/sleep?duration=60s"
# 再次構(gòu)建項(xiàng)目,這里是新內(nèi)容
go build grace.go
# 重啟,2096為pid
kill -USR2 2096
# 新API請(qǐng)求
curl "http://127.0.0.1:5001/sleep?duration=1s"
# 第一次構(gòu)建項(xiàng)目
go build endless.go
# 運(yùn)行項(xiàng)目,這時(shí)就可以做內(nèi)容修改了
./endless
# 請(qǐng)求項(xiàng)目,60s后返回
curl "http://127.0.0.1:5003/sleep?duration=60s"
# 再次構(gòu)建項(xiàng)目,這里是新內(nèi)容
go build endless.go
# 重啟,22072為pid
kill -1 22072
# 新API請(qǐng)求
curl "http://127.0.0.1:5003/sleep?duration=1s"
# 第一次構(gòu)建項(xiàng)目
go build -ldflags '-X main.BuildID=1' overseer.go
# 運(yùn)行項(xiàng)目,這時(shí)就可以做內(nèi)容修改了
./overseer
# 請(qǐng)求項(xiàng)目,60s后返回
curl "http://127.0.0.1:5005/sleep?duration=60s"
# 再次構(gòu)建項(xiàng)目,這里是新內(nèi)容,注意版本號(hào)不同了
go build -ldflags '-X main.BuildID=2' overseer.go
# 重啟,28300為主進(jìn)程pid
kill -USR2 28300
# 新API請(qǐng)求
curl http://127.0.0.1:5005/sleep?duration=1s
對(duì)比結(jié)果
示例 |
舊API返回值 |
新API返回值 |
舊pid |
新pid |
結(jié)論 |
grace |
Hello world |
Hello Harry |
2096 |
3100 |
舊API不會(huì)斷掉,會(huì)執(zhí)行原來的邏輯,pid會(huì)變化 |
endless |
Hello world |
Hello Harry |
22072 |
22365 |
舊API不會(huì)斷掉,會(huì)執(zhí)行原來的邏輯,pid會(huì)變化 |
overseer |
Hello world |
Hello Harry |
28300 |
28300 |
舊API不會(huì)斷掉,會(huì)執(zhí)行原來的邏輯,主進(jìn)程pid不會(huì)變化 |
原理分析
可以看出grace和endless是比較像的。
- 監(jiān)聽信號(hào)
- 收到信號(hào)時(shí)fork子進(jìn)程(使用相同的啟動(dòng)命令),將服務(wù)監(jiān)聽的socket文件描述符傳遞給子進(jìn)程
- 子進(jìn)程監(jiān)聽父進(jìn)程的socket,這個(gè)時(shí)候父進(jìn)程和子進(jìn)程都可以接收請(qǐng)求
- 子進(jìn)程啟動(dòng)成功之后,父進(jìn)程停止接收新的連接,等待舊連接處理完成(或超時(shí))
- 父進(jìn)程退出,升級(jí)完成
overseer是不同的,主要是overseer加了一個(gè)主進(jìn)程管理平滑重啟,子進(jìn)程處理鏈接,能夠保持主進(jìn)程pid不變。
如下圖表示的很形象
自己實(shí)現(xiàn)
我們下面來嘗試自己實(shí)現(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)于這一部分,個(gè)人的理解也不是特別深入,如果又不正確的地方請(qǐng)大家指正。
參考文章
熱重啟golang服務(wù)器
以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
您可能感興趣的文章:- golang flag簡單用法
- Golang中定時(shí)器的陷阱詳解
- 在golang中操作mysql數(shù)據(jù)庫的實(shí)現(xiàn)代碼
- golang設(shè)置http response響應(yīng)頭與填坑記錄
- 詳解Golang實(shí)現(xiàn)http重定向https的方式
- Golang編譯器介紹