平滑重启是指能让我们的程序在重启的过程不中断服务,新老进程无缝衔接,实现零停机时间(Zero-Downtime)部署;

平滑重启是建立在优雅退出的基础之上的,之前一篇文章介绍了相关实现:Golang中使用Shutdown特性对http服务进行优雅退出使用总结

目前实现平滑重启的主要策略有两种:

方案一:我们的服务如果是多机器部署,可以通过网关程序,将即将重启服务的机器从网关下线,重启完成后再重新上线,该方案适合多机器部署的企业级应用;

方案二:让我们的程序实现自启动,重启子进程来实现平滑重启,核心策略是通过拷贝文件描述符实现子进程和父进程切换,适合单机器部署应用;

今天我们就主要介绍方案二,让我们的程序拥有平滑重启的功能,相关实现参考一个开源库:https://github.com/fvbock/endless

实现原理介绍

http 连接介绍:

我们知道,http 服务也是基于 tcp 连接,我们通过 golang http 包源码也能看到底层是通过监听 tcp 连接实现的;

func (srv *Server) ListenAndServe() error {
if srv.shuttingDown() {
return ErrServerClosed
}
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}

复用 socket:

当程序开启 tcp 连接监听时会创建一个 socket 并返回一个文件描述符 handler 给我们的程序;

通过拷贝文件描述符文件可以使 socket 不关闭继续使用原有的端口,自然 http 连接也不会断开,启动一个相同的进程也不会出现端口被占用的问题;

通过如下代码进行测试:

package main

import (
"fmt"
"net/http"
"context"
"time"
"os"
"os/signal"
"syscall"
"net"
"flag"
"os/exec"
) var (
graceful = flag.Bool("grace", false, "graceful restart flag")
procType = ""
) func main() {
flag.Parse()
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, fmt.Sprintf("Hello world! ===> %s", procType))
})
server := &http.Server{
Addr: ":8080",
Handler: mux, } var err error
var listener net.Listener
if *graceful {
f := os.NewFile(3, "")
listener, err = net.FileListener(f)
procType = "fork process"
} else {
listener, _ = net.Listen("tcp", server.Addr)
procType = "main process" //主程序开启5s 后 fork 子进程
go func() {
time.Sleep(5*time.Second)
forkSocket(listener.(*net.TCPListener))
}() } err=server.Serve(listener.(*net.TCPListener)) fmt.Println(fmt.Sprintf("proc exit %v", err))
} func forkSocket(tcpListener *net.TCPListener) error {
f, err := tcpListener.File()
if err != nil {
return err
} args := []string{"-grace"}
fmt.Println(os.Args[0], args)
cmd := exec.Command(os.Args[0], args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// put socket FD at the first entry
cmd.ExtraFiles = []*os.File{f}
return cmd.Start()
}

该程序启动后,等待 5s 会自动 fork 子进程,通过 ps 命令查看如图可以看到有两个进程同时共存:

然后我们可以通过浏览器访问 http://127.0.0.1/ 可以看到会随机显示主进程或子进程的输出;

写一个测试代码进行循环请求:

package main

import (
"net/http"
"io/ioutil"
"fmt"
"sync"
) func main(){ wg:=sync.WaitGroup{}
wg.Add(100)
for i:=0; i<100; i++ {
go func(index int) {
result:=getUrl(fmt.Sprintf("http://127.0.0.1:8080?%d", i))
fmt.Println(fmt.Sprintf("loop:%d %s", index, result))
wg.Done()
}(i)
}
wg.Wait()
} func getUrl(url string) string{
resp, _ := http.Get(url)
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
return string(body)
}

能看到返回的数据也是有些是主进程有些是子进程。

切换过程:

在开启新的进程和老进程退出的瞬间,会有一个短暂的瞬间是同时有两个进程使用同一个文件描述符,此时这种状态,通过http请求访问,会随机请求到新进程或老进程上,这样也没有问题,因为请求不是在新进程上就是在老进程上;当老进程结束后请求就会全部到新进程上进行处理,通过这种方式即可实现平滑重启;

综上,我们可以将核心的实现总结如下:

1.监听退出信号;

2.监听到信号后 fork 子进程,使用相同的命令启动程序,将文件描述符传递给子进程;

3.子进程启动后,父进程停止服务并处理正在执行的任务(或超时)退出;

4.此时只有一个新的进程在运行,实现平滑重启。

一个完整的 demo 代码,通过发送 USR1 信号,程序会自动创建子进程并关闭主进程,实现平滑重启:

package main

import (
"fmt"
"net/http"
"context"
"os"
"os/signal"
"syscall"
"net"
"flag"
"os/exec"
) var (
graceful = flag.Bool("grace", false, "graceful restart flag")
) func main() {
flag.Parse()
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello world!")
})
server := &http.Server{
Addr: ":8080",
Handler: mux, } var err error
var listener net.Listener
if *graceful {
f := os.NewFile(3, "")
listener, err = net.FileListener(f)
} else {
listener, err = net.Listen("tcp", server.Addr)
}
if err != nil{
fmt.Println(fmt.Sprintf("listener error %v", err))
return
} go listenSignal(context.Background(), server, listener) err=server.Serve(listener.(*net.TCPListener))
fmt.Println(fmt.Sprintf("proc exit %v", err))
} func forkSocket(tcpListener *net.TCPListener) error {
f, err := tcpListener.File()
if err != nil {
return err
} args := []string{"-grace"}
fmt.Println(os.Args[0], args)
cmd := exec.Command(os.Args[0], args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// put socket FD at the first entry
cmd.ExtraFiles = []*os.File{f}
return cmd.Start()
} func listenSignal(ctx context.Context, httpSrv *http.Server, listener net.Listener) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.USR1) select {
case <-sigs:
forkSocket(listener.(*net.TCPListener))
httpSrv.Shutdown(ctx)
fmt.Println("http shutdown")
}
}

使用 apache 的 ab 压测工具进行验证一下,执行 ab -c 50 -t 20 http://127.0.0.1:8080/ 持续 50 的并发 20s,在压测的期间向程序运行的pid发送 USR1 信号,可以看到压测结果,没有失败的请求,由此可知,该方案实现平滑重启是木有问题的。

最后给大家安利一个 Web 开发框架,该框架已经将平滑重启进行的封装,开箱即用,快速构建一个带平滑重启的 Web 服务。

框架源码:https://gitee.com/zhucheer/orange

文档:https://www.kancloud.cn/chase688/orange_framework/1448035

Gong服务实现平滑重启分析的更多相关文章

  1. yarn关于app max attempt深度解析,针对长服务appmaster平滑重启

    在YARN上开发长服务,需要注意fault-tolerance,本篇文章对appmaster的平滑重启的一个参数做了解析,如何设置可以有助于达到appmaster平滑重启. 在yarn-site.xm ...

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

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

  3. 启动Nginx、查看nginx进程、nginx帮助命令、Nginx平滑重启、Nginx服务器的升级

    1.启动nginx的方式: cd /usr/local/nginx ls

  4. golang 服务平滑重启小结

    背景 golang 程序平滑重启框架 supervisor 出现 defunct 原因 使用 master/worker 模式 背景 在业务快速增长中,前期只是验证模式是否可行,初期忽略程序发布重启带 ...

  5. Centos--swoole平滑重启服务

    平滑重启: 已经打开的服务: 首先在server服务中为进程添加名字: /** * @param $server */ public function onStart($server) { swool ...

  6. Golang学习--平滑重启

    在上一篇博客介绍TOML配置的时候,讲到了通过信号通知重载配置.我们在这一篇中介绍下如何的平滑重启server. 与重载配置相同的是我们也需要通过信号来通知server重启,但关键在于平滑重启,如果只 ...

  7. node.js cluster多进程、负载均衡和平滑重启

    1 cluster多进程 cluster经过好几代的发展,现在已经比较好使了.利用cluster,可以自动完成子进程worker分配request的事情,就不再需要自己写代码在master进程中rob ...

  8. Nginx的平滑重启和平滑升级

    一,Nginx的平滑重启如果改变了Nginx的配置文件(nginx.conf),想重启Nginx,可以发送系统信号给Nginx主进程的方式来进行.在重启之前,要确认Nginx配置文件的语法是正确的. ...

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

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

随机推荐

  1. RegExp正则表达式基础

    什么是正则表达式 正则表达式用于对字符串模式匹配及检索替换 如何创建正则 有两种创建正则的方法: js风格 new RegExp(pattern [, flags]) perl风格 /pattern/ ...

  2. kuangbin专题专题十一 网络流 POJ 1087 A Plug for UNIX

    题目链接:https://vjudge.net/problem/POJ-1087 题目:有n个插座,插座上只有一个插孔,有m个用电器,每个用电器都有插头,它们的插头可以一样, 有k个插孔转化器, a ...

  3. NOI4.6 最小新整数——切山游戏

    描述 给定一个十进制正整数n(0 < n < 1000000000),每个数位上数字均不为0.n的位数为m. 现在从m位中删除k位(0<k < m),求生成的新整数最小为多少? ...

  4. Spring Boot定义系统启动任务的两种方式

    Spring Boot定义系统启动任务的两种方式 概述 如果涉及到系统任务,例如在项目启动阶段要做一些数据初始化操作,这些操作有一个共同的特点,只在项目启动时进行,以后都不再执行,这里,容易想到web ...

  5. C语言秋季作业4

    本周作业头 这个作业属于那个课程 C语言程序设计II 这个作业要求在哪里 作业连接 我在这个课程的目标是 熟练掌握for循环语句的运用 这个作业在那个具体方面帮助我实现目标 通过作业练习 参考文献 文 ...

  6. Windows10怎么用Administrator登录?

    1.首先按下快捷键win+X键, 2.然后在命令提示符中输入命令“net user administrator /active:yes”后回车 3.此时administrator管理员账户已开启,点击 ...

  7. 微服务之docker(二)

    一.SpringCloud/SpringBoot整合docker 使用docker的maven组建构建springboot应用(官方文档:https://spring.io/guides/gs/spr ...

  8. ROS之服务

    服务(service)是另一种在节点之间传递数据的方法,服务其实就是同步的跨进程函数调用,它能够让一个节点调用运行在另一个节点中的函数. 我们就像之前消息类型一样定义这个函数的输入/输出.服务端(提供 ...

  9. Request和Session的生命周期

    request的生命周期是request请求域,一个请求结束,则request结束 session的生命周期是session会话域,打开一个浏览器请求一个网站的页面后,Session开始,当sessi ...

  10. 视觉slam十四讲ch6曲线拟合 代码注释(笔记版)

    // ceres 版本 1 #include <opencv2/core/core.hpp> #include <ceres/ceres.h> #include <chr ...