首先说一下 平滑重启 的定义:
优雅的重启服务,重启过程不会中断曾经沉闷的链接。咱们熟知的 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
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/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、而这个问题其实作者早也晓得,只是当初还没修复,我感觉这个问题还是挺重大的。
13、我学习 go 也没有多久,文中可能有谬误的中央,欢送大家来斧正,独特学习。