Golang HTTP 服务平滑重启及升级的思路

 更新时间:2020年04月03日 09:31:28   作者:温旭峰  
Golang HTTP服务在上线时,需要重新编译可执行文件,关闭正在运行的进程,然后再启动新的运行进程。这篇文章主要介绍了Golang HTTP 服务平滑重启及升级,需要的朋友可以参考下

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

实现的一般思路

  • 一般情况下,要实现平滑重启或升级,需要执行以下几个步骤:
  • 发布新的bin文件覆盖老的bin文件
  • 发送一个信号量(USR2),告诉正在运行的进程,进行重启
  • 正在运行的进程接受到信号后,以子进程的方式启动新的bin文件
  • 新进程接收并处理新的请求
  • 老进程不再接收新请求,等待所有正在处理的请求处理完成后自动退出
  • 新进程在老进程退出后,继续提供服务

选型与实践

重复造平滑重启及升级的轮子比较简单,但测试覆盖无法控制,比较耗时耗力。所以秉着不重复造轮子的思路,使用github中的三方库进行选择:

  • facebookgo/grace
  • fvbock/endless
  • jpillora/overseer

endless与grace的实现方式原理都比较类似,所以在选型初期我们以facebookgo/grace库为例集成到项目中进行测试:

func (h *Server) ListenAndServe(listenAddress string) error {
 // ....
 return gracehttp.Serve(&http.Server{
 Addr: listenAddress,
 Handler: h.httpServerMux,
 })
}

使用ab工具压测 api-publish服务进行测试,服务启动后,执行以下命令:

ab -c 10 -n 2000 http://127.0.0.1:38272/api/list

然后给进程发送USR2信号 kill -USR2 api-server-pid,可看到以下结果:

结果中 Failed requests表示在整个压测请求中没有错误的请求,这可以说明服务重启时没有中断请求的接收和处理。如果使用sleep的方式测试,可以明显的看到新进程替代老进程的过程。

supervisor的问题

实际项目中,线上服务是被supervisor启动的。如上所说的我们如果通过grace或者endless的子进程启动后退出父进程这种方式的话,存在的问题就是子进程会被1号进程接管,导致supervisor认为服务挂掉重启服务,为了避免这种问题我们需要使用master-worker的方式。

overseer这个备选库实现了master-worker的方式。简单集成方式:

return overseer.RunErr(overseer.Config{
 Address: address,
 Program: func(state overseer.State) {
 // ...
 http.Serve(state.Listener, nil)
 },
})

另外:在更新supervisor时,配置不需要更新,但重启服务的命令不能使用supervisor restart,需要使用supervisor signal sigusr2 api的命令。

还是使用上面的测试方式:

可以明显的看到,supervisor发送了USR2信号后,主进程的pid没有变化,重新启动了一个新的子进程来处理线上请求。

其他的问题

在使用overseer集成到项目中测试时,子进程的运行函数中仅仅加入了http服务的启动,这样导致一个问题。

main函数中任务会被执行两次,如果是cron的初始化,那么cron就会初始化两次,导致有两个cron在执行,这样的方式是不符合预期的。

导致这样的原因是:overseer在启动子进程时是使用和主进程一样的启动命令。所以main函数会执行两次。

func (mp *master) fork() error {
 mp.debugf("starting %s", mp.binPath)
 cmd := exec.Command(mp.binPath)
 //mark this new process as the "active" slave process.
 //this process is assumed to be holding the socket files.
 mp.slaveCmd = cmd
 mp.slaveID++
 //provide the slave process with some state
 e := os.Environ()
 e = append(e, envBinID+"="+hex.EncodeToString(mp.binHash))
 e = append(e, envBinPath+"="+mp.binPath)
 e = append(e, envSlaveID+"="+strconv.Itoa(mp.slaveID))
 e = append(e, envIsSlave+"=1")
 e = append(e, envNumFDs+"="+strconv.Itoa(len(mp.slaveExtraFiles)))
 cmd.Env = e
 //inherit master args/stdfiles
 cmd.Args = os.Args
 cmd.Stdin = os.Stdin
 cmd.Stdout = os.Stdout
 cmd.Stderr = os.Stderr
 //include socket files
 cmd.ExtraFiles = mp.slaveExtraFiles
 if err := cmd.Start(); err != nil {
 return fmt.Errorf("Failed to start slave process: %s", err)
 }
 // ...
}

我们通过调整main函数的内容来解决这个问题:

  • 将之前所有的初始化内容集成在initialization函数中
  • 将http初始化的内容集成在httpServer函数中,返回一个http.Server
func main() {
 // 配置初始化
 if err := config.Init(appConf); err != nil {
 fmt.Println(err)
 return
 }
 cfg := config.GetConfig()

 // 初始化graceful http服务
 gracefulHTTPServer := microsvr.GracefulHTTPServer{
 Address: cfg.HTTPListenAddress,
 Conf:  cfg,
 Initialization: initialization,
 HttpServer: httpServer,
 }

 // 启动
 if err := gracefulHTTPServer.Run(); err != nil {
 fmt.Println(err)
 return
 }
}

// 初始化日志、数据库链接、定时任务等
func initialization(cfg *config.Conf) {
 if err := microsvr.Init(cfg); err != nil {
 fmt.Println(err)
 return
 }

 if err := server.AddConnect(cfg.Databases.String()); err != nil {
 fmt.Println(err)
 return
 }
 logger.Info("数据库链接成功:" + cfg.Databases.Address)
 // cron
 cron.Cron.Init()
}

// 初始化http服务,但不启动
func httpServer() *http.Server {
 server := microsvr.NewHTTPServer()
 server.SetAllowOrginBack()
 Routers(server)
 return server
}

实践对比结果:

  • grace与endless:旧的api都不会断掉,会执行原来的逻辑,但pid会变化;不支持supervisor管理
  • overseer:旧api不会断掉,会执行原来的逻辑,主进程pid也不会变化,支持supervisor、systemd等管理

grace与endless的原理比较相像,都是类似上述的一般思路的实现原理。overseer的不同,主要有两点:

  • 添加了fetcher:用来支持自动升级bin文件,fetcher运行在一个goroutine中,通过预先设置好的间隔时间来检查bin文件;支持File、Github、S3的方式
  • 添加了主进程管理平滑重启:子进程处理链接,能够保持主进程pid不变

我们使用了overseer作为最终的选型结果。

总结

到此这篇关于Golang HTTP 服务平滑重启及升级的思路的文章就介绍到这了,更多相关golang http 平滑重启内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • golang image图片处理示例

    golang image图片处理示例

    这篇文章主要介绍了golang image图片处理的方法,结合实例形式分析了Go语言针对图片的打开、读取、转换等相关操作技巧,需要的朋友可以参考下
    2016-07-07
  • Golang跳转语句continue与goto使用语法详解

    Golang跳转语句continue与goto使用语法详解

    这篇文章主要介绍了Golang跳转语句continue与goto使用语法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-01-01
  • Golang通脉之流程控制详情

    Golang通脉之流程控制详情

    这篇文章主要介绍了Golang通脉之流程控制,流程控制是每种编程语言控制逻辑走向和执行次序的重要部分,Go语言中最常用的流程控制有if和for,而switch和goto主要是为了简化代码,下面文章将详细介绍改该内容,需要的朋友可以参考一下
    2021-10-10
  • go语言的四数相加等于指定数算法

    go语言的四数相加等于指定数算法

    这篇文章主要介绍了go语言的四数相加等于指定数算法的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04
  • 关于Golang中range指针数据的坑详解

    关于Golang中range指针数据的坑详解

    这篇文章主要给大家介绍了关于Golang中range指针数据的坑的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-02-02
  • 使用Golang简单实现七牛图片处理API

    使用Golang简单实现七牛图片处理API

    本文给大家实现的是使用Golang简单实现七牛图片处理API的方法和步骤,基于PIPE库实现的,非常的实用,有需要的小伙伴可以参考下
    2016-08-08
  • Golang TCP粘包拆包问题的解决方法

    Golang TCP粘包拆包问题的解决方法

    这篇文章主要给大家介绍了Golang TCP粘包拆包问题的解决方法,文中通过示例代码介绍的非常详细,对大家学习或者使用Golang具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-07-07
  • Go时间格式化的实现

    Go时间格式化的实现

    本文主要介绍了Go时间格式化的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • golang log4go的日志输出优化详解

    golang log4go的日志输出优化详解

    log4go源于google的一项log工程,但官方已经停止维护更新,下面这篇文章主要给大家介绍了关于golang log4go的日志输出优化的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-12-12
  • GoLang context包的使用方法介绍

    GoLang context包的使用方法介绍

    日常Go开发中,Context包是用的最多的一个了,几乎所有函数的第一个参数都是ctx,那么我们为什么要传递Context呢,Context又有哪些用法,底层实现是如何呢?相信你也一定会有探索的欲望,那么就跟着本篇文章,一起来学习吧
    2023-03-03

最新评论