4

【源码剖析】goframe的”平滑“重启并不平滑?

 2 years ago
source link: https://segmentfault.com/a/1190000040552942
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【源码剖析】goframe的”平滑“重启并不平滑?

发布于 8 月 21 日

首先说一下平滑重启的定义:
优雅的重启服务,重启过程不会中断已经活跃的链接。我们熟知的nginx reload、php-fpm reload都是平滑重启,重启时,正在进行的请求依然能执行下去,直到超过指定的超时时间,而这里特别提一下php-fpm reload有个坑,它默认不是平滑重启的,因为process_control_timeout(设置子进程接受主进程复用信号的超时时间)的配置默认为0秒,代表超时时间为0,这就导致php-fpm reload都会中断请求,也不知道为什么官方要把默认值设置为0秒。

然后这篇文章要讲的就是goframe的平滑重启其实是"假平滑",重启过程中会有一部分请求中断。

官方的平滑重启文档在这里:https://goframe.org/pages/vie...

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 = true

3、执行 go build main.go && ./main
image.png

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

curl 127.0.0.1:8999/sleep
curl 127.0.0.1:8999/debug/admin/restart

image.png

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,也可能执行不完。
image.png

    // 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/684490...
可以看到,他调用了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/pu...

    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、而这个问题其实作者早也知道,只是现在还没修复,我觉得这个问题还是挺严重的。
image.png

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK