Golang学习

在上一篇博客介绍TOML配置的时候,讲到了通过信号通知重载配置。我们在这一篇中介绍下如何的平滑重启server。

与重载配置相同的是我们也需要通过信号来通知server重启,但关键在于平滑重启,如果只是简单的重启,只需要kill掉,然后再拉起即可。平滑重启意味着server升级的时候可以不用停止业务。

我们先来看下Github上有没有相应的库解决这个问题,然后找到了如下三个库:

  • 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的重启。

使用方式

我们来分别使用这三个库来做平滑重启的事情,之后来对比其优缺点。

这三个库的官方都给了相应的例子,例子如下:

但三个库官方的例子不太一致,我们来统一一下:

我们参考官方的例子分别来写下用来对比的例子:

grace

1package main 2 3import ( 4 "time" 5 "net/http" 6 "github.com/facebookgo/grace/gracehttp" 7) 8 9func main() { 10 gracehttp.Serve( 11 &http.Server{Addr: ":5001", Handler: newGraceHandler()}, 12 &http.Server{Addr: ":5002", Handler: newGraceHandler()}, 13 ) 14} 15 16func newGraceHandler() http.Handler { 17 mux := http.NewServeMux() 18 mux.HandleFunc("/sleep", func(w http.ResponseWriter, r *http.Request) { 19 duration, err := time.ParseDuration(r.FormValue("duration")) 20 if err != nil { 21 http.Error(w, err.Error(), 400) 22 return 23 } 24 time.Sleep(duration) 25 w.Write([]byte("Hello World")) 26 }) 27 return mux 28}

endless

1package main 2 3import ( 4 "log" 5 "net/http" 6 "os" 7 "sync" 8 "time" 9 10 "github.com/fvbock/endless" 11 "github.com/gorilla/mux" 12) 13 14func handler(w http.ResponseWriter, r *http.Request) { 15 duration, err := time.ParseDuration(r.FormValue("duration")) 16 if err != nil { 17 http.Error(w, err.Error(), 400) 18 return 19 } 20 time.Sleep(duration) 21 w.Write([]byte("Hello World")) 22} 23 24func main() { 25 mux1 := mux.NewRouter() 26 mux1.HandleFunc("/sleep", handler) 27 28 w := sync.WaitGroup{} 29 w.Add(2) 30 go func() { 31 err := endless.ListenAndServe(":5003", mux1) 32 if err != nil { 33 log.Println(err) 34 } 35 log.Println("Server on 5003 stopped") 36 w.Done() 37 }() 38 go func() { 39 err := endless.ListenAndServe(":5004", mux1) 40 if err != nil { 41 log.Println(err) 42 } 43 log.Println("Server on 5004 stopped") 44 w.Done() 45 }() 46 w.Wait() 47 log.Println("All servers stopped. Exiting.") 48 49 os.Exit(0) 50}

overseer

1package main 2 3import ( 4 "fmt" 5 "net/http" 6 "time" 7 8 "github.com/jpillora/overseer" 9) 10 11//see example.sh for the use-case 12 13// BuildID is compile-time variable 14var BuildID = "0" 15 16//convert your 'main()' into a 'prog(state)' 17//'prog()' is run in a child process 18func prog(state overseer.State) { 19 fmt.Printf("app#%s (%s) listening...\n", BuildID, state.ID) 20 http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 duration, err := time.ParseDuration(r.FormValue("duration")) 22 if err != nil { 23 http.Error(w, err.Error(), 400) 24 return 25 } 26 time.Sleep(duration) 27 w.Write([]byte("Hello World")) 28 fmt.Fprintf(w, "app#%s (%s) says hello\n", BuildID, state.ID) 29 })) 30 http.Serve(state.Listener, nil) 31 fmt.Printf("app#%s (%s) exiting...\n", BuildID, state.ID) 32} 33 34//then create another 'main' which runs the upgrades 35//'main()' is run in the initial process 36func main() { 37 overseer.Run(overseer.Config{ 38 Program: prog, 39 Addresses: []string{":5005", ":5006"}, 40 //Fetcher: &fetcher.File{Path: "my_app_next"}, 41 Debug: false, //display log of overseer actions 42 }) 43}

对比

对比示例的操作步骤

  • 分别构建上面的示例,并记录pid
  • 调用API,在其未返回时,修改内容(Hello World -> Hello Harry),重新构建。查看旧API是否返回旧的内容
  • 调用新API,查看返回的内容是否是新的内容
  • 查看当前运行的pid,是否与之前一致

下面给一下操作命令

1# 第一次构建项目 2go build grace.go 3# 运行项目,这时就可以做内容修改了 4./grace & 5# 请求项目,60s后返回 6curl "http://127.0.0.1:5001/sleep?duration=60s" & 7# 再次构建项目,这里是新内容 8go build grace.go 9# 重启,2096为pid 10kill -USR2 2096 11# 新API请求 12curl "http://127.0.0.1:5001/sleep?duration=1s" 13 14 15# 第一次构建项目 16go build endless.go 17# 运行项目,这时就可以做内容修改了 18./endless & 19# 请求项目,60s后返回 20curl "http://127.0.0.1:5003/sleep?duration=60s" & 21# 再次构建项目,这里是新内容 22go build endless.go 23# 重启,22072为pid 24kill -1 22072 25# 新API请求 26curl "http://127.0.0.1:5003/sleep?duration=1s" 27 28 29# 第一次构建项目 30go build -ldflags '-X main.BuildID=1' overseer.go 31# 运行项目,这时就可以做内容修改了 32./overseer & 33# 请求项目,60s后返回 34curl "http://127.0.0.1:5005/sleep?duration=60s" & 35# 再次构建项目,这里是新内容,注意版本号不同了 36go build -ldflags '-X main.BuildID=2' overseer.go 37# 重启,28300为主进程pid 38kill -USR2 28300 39# 新API请求 40curl "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是与grace和endless有些不同,主要是两点:

  1. overseer添加了Fetcher,当Fetcher返回有效的二进位流(io.Reader) 时,主进程会将它保存到临时位置并验证它,替换当前的二进制文件并启动。 Fetcher运行在一个goroutine中,预先会配置好检查的间隔时间。Fetcher支持File、GitHub、HTTP和S3的方式。详细可查看包package fetcher
  2. overseer添加了一个主进程管理平滑重启。子进程处理连接,能够保持主进程pid不变。

如下图表示的很形象

自己实现

我们下面来尝试自己实现下第一种处理,代码如下,代码来自《热重启golang服务器》:

1package main 2import ( 3 "context" 4 "errors" 5 "flag" 6 "log" 7 "net" 8 "net/http" 9 "os" 10 "os/exec" 11 "os/signal" 12 "syscall" 13 "time" 14) 15 16var ( 17 server *http.Server 18 listener net.Listener 19 graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)") 20) 21 22func sleep(w http.ResponseWriter, r *http.Request) { 23 duration, err := time.ParseDuration(r.FormValue("duration")) 24 if err != nil { 25 http.Error(w, err.Error(), 400) 26 return 27 } 28 time.Sleep(duration) 29 w.Write([]byte("Hello World")) 30} 31 32func main() { 33 flag.Parse() 34 35 http.HandleFunc("/sleep", sleep) 36 server = &http.Server{Addr: ":5007"} 37 38 var err error 39 if *graceful { 40 log.Print("main: Listening to existing file descriptor 3.") 41 // cmd.ExtraFiles: If non-nil, entry i becomes file descriptor 3+i. 42 // when we put socket FD at the first entry, it will always be 3(0+3) 43 f := os.NewFile(3, "") 44 listener, err = net.FileListener(f) 45 } else { 46 log.Print("main: Listening on a new file descriptor.") 47 listener, err = net.Listen("tcp", server.Addr) 48 } 49 50 if err != nil { 51 log.Fatalf("listener error: %v", err) 52 } 53 54 go func() { 55 // server.Shutdown() stops Serve() immediately, thus server.Serve() should not be in main goroutine 56 err = server.Serve(listener) 57 log.Printf("server.Serve err: %v\n", err) 58 }() 59 signalHandler() 60 log.Printf("signal end") 61} 62 63func reload() error { 64 tl, ok := listener.(*net.TCPListener) 65 if !ok { 66 return errors.New("listener is not tcp listener") 67 } 68 69 f, err := tl.File() 70 if err != nil { 71 return err 72 } 73 74 args := []string{"-graceful"} 75 cmd := exec.Command(os.Args[0], args...) 76 cmd.Stdout = os.Stdout 77 cmd.Stderr = os.Stderr 78 // put socket FD at the first entry 79 cmd.ExtraFiles = []*os.File{f} 80 return cmd.Start() 81} 82 83func signalHandler() { 84 ch := make(chan os.Signal, 1) 85 signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2) 86 for { 87 sig := <-ch 88 log.Printf("signal: %v", sig) 89 90 // timeout context for shutdown 91 ctx, _ := context.WithTimeout(context.Background(), 100*time.Second) 92 switch sig { 93 case syscall.SIGINT, syscall.SIGTERM: 94 // stop 95 log.Printf("stop") 96 signal.Stop(ch) 97 server.Shutdown(ctx) 98 log.Printf("graceful shutdown") 99 return 100 case syscall.SIGUSR2: 101 // reload 102 log.Printf("reload") 103 err := reload() 104 if err != nil { 105 log.Fatalf("graceful restart error: %v", err) 106 } 107 server.Shutdown(ctx) 108 log.Printf("graceful reload") 109 return 110 } 111 } 112}

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

关于这一部分,个人的理解也不是特别深入,如果又不正确的地方请大家指正。

参考文章

热重启golang服务器 overseer, 進入(golang),Monitorable,正常重新啟動,自升級二進位文件

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

java将前端的json数组字符串转换为列表

记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang