【源码剖析】goframe 的”平滑“重启并不平滑?
作者:Anson
来源:SegmentFault 思否社区
首先说一下平滑重启的定义:
优雅的重启服务,重启过程不会中断已经活跃的链接。我们熟知的nginx reload、php-fpm reload都是平滑重启,重启时,正在进行的请求依然能执行下去,直到超过指定的超时时间,而这里特别提一下php-fpm reload有个坑,它默认不是平滑重启的,因为process_control_timeout(设置子进程接受主进程复用信号的超时时间)的配置默认为0秒,代表超时时间为0,这就导致php-fpm reload都会中断请求,也不知道为什么官方要把默认值设置为0秒。
然后这篇文章要讲的就是goframe的平滑重启其实是"假平滑",重启过程中会有一部分请求中断。
官方的平滑重启文档在这里:
https://goframe.org/pages/viewpage.action?pageId=1114220
1、测试环境版本:
gf v1.16.5
go 1.15
centos 2核cpu 64位
2、先来实验一下文档中实例1的代码
package main
import (
    "time"
    "github.com/gogf/gf/frame/g"
    "github.com/gogf/gf/os/gproc"
    "github.com/gogf/gf/net/ghttp"
)
func main() {
    s := g.Server()
    s.BindHandler("/", func(r *ghttp.Request){
        r.Response.Writeln("哈喽!")
    })
    s.BindHandler("/pid", func(r *ghttp.Request){
        r.Response.Writeln(gproc.Pid())
    })
    s.BindHandler("/sleep", func(r *ghttp.Request){
        r.Response.Writeln(gproc.Pid())
        time.Sleep(10*time.Second)
        r.Response.Writeln(gproc.Pid())
    })
    s.EnableAdmin()
    s.SetPort(8999)
    s.Run()
}
config.toml配置
[server]
    Graceful = true3、执行 go build main.go && ./main

4、这时候我们正常请求sleep接口,在新窗口同时请求restart进行“平滑重启”,会看到请求被中断了
curl 127.0.0.1:8999/sleep
curl 127.0.0.1:8999/debug/admin/restart

5、而如果我们把代码中的10秒改成2秒,这就有概率不中断请求正常平滑重启,这取决与你实验的手速,这是为什么呢,接下来我们分析一下ghttp源码
    s.BindHandler("/sleep", func(r *ghttp.Request){
        r.Response.Writeln(gproc.Pid())
        time.Sleep(2*time.Second)
        r.Response.Writeln(gproc.Pid())
    })
6、定位到ghttp_server.go的195行(大概位置)
重启的时候会创建子进程,子进程启动http server之后会在2秒后向父进程发消息(adminGProcCommGroup),通知父进程退出,并且重启过程中,因为父进程没有马上停止接收新请求,就导致还会有部分请求进入到父进程中,所以把2改大也不现实,也就是说即使你的请求耗时100ms,也可能执行不完。

    // If this is a child process, it then notifies its parent exit.
    if gproc.IsChild() {
        gtimer.SetTimeout(2*time.Second, func() {
            if err := gproc.Send(gproc.PPid(), []byte("exit"), adminGProcCommGroup); err != nil {
                //glog.Error("server error in process communication:", err)
            }
        })
    }
7、adminGProcCommGroup消息的监听在ghttp_server_admin_process.go的256行
可以看到最后会调用shutdownWebServersGracefully(),我们再找到这个方法
func handleProcessMessage() {
    for {
        if msg := gproc.Receive(adminGProcCommGroup); msg != nil {
            if bytes.EqualFold(msg.Data, []byte("exit")) {
                intlog.Printf("%d: process message: exit", gproc.Pid())
                shutdownWebServersGracefully()
                allDoneChan <- struct{}{}
                intlog.Printf("%d: process message: exit done", gproc.Pid())
                return
            }
        }
    }
}
8、shutdownWebServersGracefully
可以看到最后会调用shutdown方法停止服务
// shutdownWebServersGracefully gracefully shuts down all servers.
func shutdownWebServersGracefully(signal ...string) {
    if len(signal) > 0 {
        glog.Printf("%d: server gracefully shutting down by signal: %s", gproc.Pid(), signal[0])
    } else {
        glog.Printf("%d: server gracefully shutting down by api", gproc.Pid())
    }
    serverMapping.RLockFunc(func(m map[string]interface{}) {
        for _, v := range m {
            for _, s := range v.(*Server).servers {
                s.shutdown()
            }
        }
    })
}
9、shutdown方法最后也是调用net/http的Shutdown方法,但是因为没有传超时时间,就导致执行中的请求会中断
// shutdown shuts down the server gracefully.
func (s *gracefulServer) shutdown() {
    if s.status == ServerStatusStopped {
        return
    }
    if err := s.httpServer.Shutdown(context.Background()); err != nil {
        s.server.Logger().Errorf(
            "%d: %s server [%s] shutdown error: %v",
            gproc.Pid(), s.getProto(), s.address, err,
        )
    }
}
10、我们来看看别的网友是怎么调用Shutdown停止服务的
https://juejin.cn/post/6844903891461472270
可以看到,他调用了WithTimeout,并传给Shutdown
for {
        ctx, _ := context.WithTimeout(context.Background(), 20*time.Second)
        switch sig {
        case syscall.SIGINT, syscall.SIGTERM: // 终止进程执行
            server.Shutdown(ctx)
            log.Println("graceful shutdown")
            return
    }
11、参考10的示例,我也给goframe改造了一下,添加了WithTimeout,测试过了的确能保证在超时时间内正常执行完请求
https://github.com/gogf/gf/pull/1242/files
if err := s.httpServer.Shutdown(context.Background()); err != nil {
    ctx, cancel := context.WithTimeout(context.Background(), time.Duration(s.server.config.ShutdownTimeout)*time.Second)
    defer func() {
        cancel()
    }()
    if err := s.httpServer.Shutdown(ctx); err != nil {
        s.server.Logger().Errorf(
            "%d: %s server [%s] shutdown error: %v",
            gproc.Pid(), s.getProto(), s.address, err,
        )
    }
12、我们再来看看beego是怎么shutdown的,可以看到也是使用WithTimeout
func (srv *Server) shutdown() {
    if srv.state != StateRunning {
        return
    }
    srv.state = StateShuttingDown
    log.Println(syscall.Getpid(), "Waiting for connections to finish...")
    ctx := context.Background()
    if DefaultTimeout >= 0 {    
        var cancel context.CancelFunc
        ctx, cancel = context.WithTimeout(context.Background(), DefaultTimeout)
        defer cancel()
    }
    srv.terminalChan <- srv.Server.Shutdown(ctx)
}
13、而这个问题其实作者早也知道,只是现在还没修复,我觉得这个问题还是挺严重的。

14、我学习go也没有多久,文中可能有错误的地方,欢迎大家来指正,共同学习。


关注公众号:拾黑(shiheibook)了解更多
赞助链接:
                        关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
                        四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
                        让资讯触达的更精准有趣:https://www.0xu.cn/
                    
 关注网络尖刀微信公众号
            关注网络尖刀微信公众号随时掌握互联网精彩
- 豆包网页版入口官网 豆包在线登录地址
- KittenTTS :不用 GPU、不联网!8 种真人音色随选随播,轻到离谱(仅25MB)
- ai-logo一个开源 AI 生成 Logo 项目
- 华夏ERP国内人气领先的国产ERP系统之一
- 卢布汇率人民币2024年6月3日
- 统心同晋 信创未来丨统信软件2024年山西合作伙伴峰会成功召开
- 运营商全力以赴保通信:因灾退服基站已全部恢复
- 【杂谈快报】富士康否认苹果计划将供应链转出中国大陆;中国进口车销量创10年来历史新低
- 本周大新闻|Niantic公布AR社交应用Truffel,DecaGear办公室疑似关闭
- 手术台上的“元宇宙”!混合现实+机器学习=医疗新“视界”
- 携程老了?
- 新美国安全中心发布《民主设计:对2021年非法使用技术的正面回应》报告



 微信扫码关注公众号
                            微信扫码关注公众号