Golang学习之平滑重启
在上一篇博客介绍toml配置的时候,讲到了通过信号通知重载配置。我们在这一篇中介绍下如何的平滑重启server。
与重载配置相同的是我们也需要通过信号来通知server重启,但关键在于平滑重启,如果只是简单的重启,只需要kill掉,然后再拉起即可。平滑重启意味着server升级的时候可以不用停止业务。
我们先来看下github上有没有相应的库解决这个问题,然后找到了如下三个库:
- - graceful restart & zero downtime deploy for go servers.
- - zero downtime restarts for go servers (drop in replacement for http.listenandserve)
- - monitorable, gracefully restarting, self-upgrading binaries in go (golang)
我们分别来学习一下,下面只讲解http server的重启。
使用方式
我们来分别使用这三个库来做平滑重启的事情,之后来对比其优缺点。
这三个库的官方都给了相应的例子,例子如下:
但三个库官方的例子不太一致,我们来统一一下:
- grace例子
- endless例子
- overseer例子
我们参考官方的例子分别来写下用来对比的例子:
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,是否与之前一致
下面给一下操作命令
# 第一次构建项目 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是比较像的。
- 监听信号
- 收到信号时fork子进程(使用相同的启动命令),将服务监听的socket文件描述符传递给子进程
- 子进程监听父进程的socket,这个时候父进程和子进程都可以接收请求
- 子进程启动成功之后,父进程停止接收新的连接,等待旧连接处理完成(或超时)
- 父进程退出,升级完成
overseer是不同的,主要是overseer加了一个主进程管理平滑重启,子进程处理链接,能够保持主进程pid不变。
如下图表示的很形象
自己实现
我们下面来尝试自己实现下第一种处理,代码如下,代码来自《》:
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
关于这一部分,个人的理解也不是特别深入,如果又不正确的地方请大家指正。
参考文章
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 刚毕业的时候学校组织去佛山实习
下一篇: # VsCode 配置C++调试运行