与重载配置相同的是我们也需要通过信号来通知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例子 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 mainimport ("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 mainimport ("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 mainimport ("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是比较像的。
热重启的原理非常简单,但是涉及到一些系统调用以及父子进程之间文件句柄的传递等等细节比较多。
处理过程分为以下几个步骤:

  1. 监听信号(USR2)
  2. 收到信号时fork子进程(使用相同的启动命令),将服务监听的socket文件描述符传递给子进程
  3. 子进程监听父进程的socket,这个时候父进程和子进程都可以接收请求
  4. 子进程启动成功之后,父进程停止接收新的连接,等待旧连接处理完成(或超时)
  5. 父进程退出,升级完成

overseer是与grace和endless有些不同,主要是两点:

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

如下图表示的很形象

细节

  • 父进程将socket文件描述符传递给子进程可以通过命令行,或者环境变量等
  • 子进程启动时使用和父进程一样的命令行,对于golang来说用更新的可执行程序覆盖旧程序
  • server.Shutdown()优雅关闭方法是go1.8的新特性
  • server.Serve(l)方法在Shutdown时立即返回,Shutdown方法则阻塞至context完成,所以Shutdown的方法要写在主goroutine中

代码

package mainimport ("context""errors""flag""log""net""net/http""os""os/exec""os/signal""syscall""time"
)var (server   *http.Serverlistener net.Listenergraceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
)func handler(w http.ResponseWriter, r *http.Request) {time.Sleep(20 * time.Second)w.Write([]byte("hello world233333!!!!"))
}func main() {flag.Parse()http.HandleFunc("/hello", handler)server = &http.Server{Addr: ":9999"}var err errorif *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 goroutineerr = 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.Stdoutcmd.Stderr = os.Stderr// put socket FD at the first entrycmd.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 := <-chlog.Printf("signal: %v", sig)// timeout context for shutdownctx, _ := context.WithTimeout(context.Background(), 20*time.Second)switch sig {case syscall.SIGINT, syscall.SIGTERM:// stoplog.Printf("stop")signal.Stop(ch)server.Shutdown(ctx)log.Printf("graceful shutdown")returncase syscall.SIGUSR2:// reloadlog.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

systemd & supervisor

父进程退出之后,子进程会挂到1号进程上面。这种情况下使用systemd和supervisord等管理程序会显示进程处于failed的状态。解决这个问题有两个方法:

  • 使用pidfile,每次进程重启更新一下pidfile,让进程管理者通过这个文件感知到mainpid的变更。
  • 起一个master来管理服务进程,每次热重启master拉起一个新的进程,把旧的kill掉。这时master的pid没有变化,对于进程管理者来说进程处于正常的状态。一个简洁的实现

Golang服务平滑重启相关推荐

  1. supervisor 重启_Golang HTTP服务平滑重启及升级

    Golang HTTP服务在上线时,需要重新编译可执行文件,关闭正在运行的进程,然后再启动新的运行进程.对于访问频率比较高的面向终端用户的产品,关闭.重启的过程中会出现无法访问(nginx表现为502 ...

  2. golang中的web服务平滑重启

    新进来的请求怎么办? fork一个子进程,继承父进程的监听socket 子进程启动成功之后,接收新的连接 父进程停止接收新的连接,等已有的请求处理完毕,退出 优雅重启成功 平滑升级 子进程如何继承父进 ...

  3. 平滑重启_swoole服务平滑重启

    给swoole进程定义别名 $server = new SwooleWebSocketServer("0.0.0.0", 9501);$server->on('start', ...

  4. golang服务开发平滑升级之优雅重启

    女主宣言 本文章主要探讨golang服务器开发中在平滑升级过程中对优雅重启的使用与研究. PS:丰富的一线技术.多元化的表现形式,尽在"360云计算",点关注哦! 经典平滑升级方案 ...

  5. 【学习笔记】启动Nginx、查看nginx进程、查看nginx服务主进程的方式、Nginx服务可接受的信号、nginx帮助命令、Nginx平滑重启、Nginx服务器的升级

     1.启动nginx的方式: cd /usr/local/nginx ls ./nginx -c nginx.conf 2.查看nginx的进程方式: [root@localhost nginx] ...

  6. K8s上的Go服务怎么扩容、发版更新、回滚、平滑重启?教你用Deployment全搞定!

    经过前面不少文章的铺垫,终于可以写这个大家都感兴趣的话题了,在前面两篇文章,我们讲了Kubernetes里的 Pod和 副本集ReplicaSet (RS) 这两个API对象.知道了Pod是Kuber ...

  7. Nginx 的启动、停止、平滑重启、信号控制和平滑升级

    Nginx 的启动          假设 nginx 安装在 /usr/local/nginx 目录中,那么启动 nginx 的命令就是: [root@localhost ~]# /usr/loca ...

  8. liunx服务(Nginx服务器 web服务器源码包和rpm 服务平滑升级)

    Nginx服务器 和apache服务器是同样的功能都是发布网页web的但是不同的是功能上有些不同各有各的好处. Nginx服务器 开始安装 确认包安装 yum install pcre-devel o ...

  9. Nginx的平滑重启和升级

    Nginx的平滑重启和升级 Nginx平滑重启 如果修改了Nginx的配置文件(nginx.conf),想要重启Nginx,同样通过发送系统信号给Nginx的主进程的方式. 但是,重启之前,需要确认N ...

最新文章

  1. torch max 判断与筛选
  2. Java使用JAX-WS来写webservice时 Unable to create JAXBContext
  3. 安全的 ActiveMQ
  4. 划痕麻点检测程序_精密外观检测机设计
  5. java对象数组覆盖_java – 如何覆盖RAML 1.0中的对象数组属性类型
  6. java用redis缓存的步骤_详解在Java程序中运用Redis缓存对象的方法|chu
  7. Android Gradle和Gradle插件区别
  8. 信息学奥赛一本通 1126:矩阵转置 | OpenJudge NOI 1.8 10:矩阵转置
  9. 全国首个园区型绿色能源网一年“减碳”上万吨
  10. CSUST-2018区域赛选拔个人赛-1019 看直播(二分+DP)
  11. python中match用法_python re.match()用法相关示例
  12. JTAG与SWD接口定义
  13. 软件人员kpi制定模板_软件科技公司绩效考核办法模板.doc
  14. 一个炫酷的个人网站带后台
  15. Telnet + VTY(虚拟终端Virtual Teletype Terminal)远程管理路由器和交换机
  16. java的graphics2d_Java Graphics2D类的绘图方法
  17. Linux DMA驱动构架分析
  18. Oracle全局搜索
  19. vscode自动保存代码,自动按照eslint和standard规范格式化代码设置
  20. hibernate一对一主键唯一外键关联(二)

热门文章

  1. html在母版页中布局,MVC母版页_Layout.cshtml
  2. 关于结构体中重载小于号< (用于sort()排序) 或者大于号> (这是用于堆排序)
  3. 华为手机地震预警怎样设置
  4. 一加3t android 7.1,一加3T要来了!除了骁龙821还直接上安卓7.1?
  5. 量子计算机存储能力大吗,神奇的量子电脑,量子计算机超能力,强到惊人的存储运算能力...
  6. 关系型数据库规范化的通俗理解
  7. Verilog万年历时钟
  8. echarts+流程图
  9. 如何给Ubuntu16.04更新软件
  10. 在字符串指定位置插入一个字符