服务端代码经常需要升级,对于线上系统的升级常用的做法是,通过前端的负载均衡(如nginx)来保证升级时至少有一个服务可用,依次(灰度)升级。

而另一种更方便的方法是在应用上做热重启,直接更新源码、配置或升级应用而不停服务。

这个功能在重要业务上尤为重要,会影响服务可用性、用户体验。

原理

热重启的原理比较简单,但是涉及到一些系统调用以及父子进程之间文件句柄的传递等等细节比较多。
处理过程分为以下几个步骤:

  1. 监听信号(USR2..)
  2. 收到信号时fork子进程(使用相同的启动命令),将服务监听的socket文件描述符传递给子进程
  3. 子进程监听父进程的socket,这个时候父进程和子进程都可以接收请求
  4. 子进程启动成功之后,父进程停止接收新的连接,等待旧连接处理完成(或超时)
  5. 父进程退出,重启完成

细节

  • 父进程将socket文件描述符传递给子进程可以通过命令行,或者环境变量等
  • 子进程启动时使用和父进程一样的命令行,对于golang来说用更新的可执行程序覆盖旧程序
  • server.Shutdown()优雅关闭方法是go>=1.8的新特性
  • server.Serve(l)方法在Shutdown时立即返回,Shutdown方法则阻塞至context完成,所以Shutdown的方法要写在主goroutine中

代码

package main

import (
"context"
"errors"
"flag"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
) var (
server *http.Server
listener net.Listener
graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
) func handler(w http.ResponseWriter, r *http.Request) {
time.Sleep( * time.Second)
w.Write([]byte("hello world233333!!!!"))
} func main() {
flag.Parse() http.HandleFunc("/hello", handler)
server = &http.Server{Addr: ":9999"} var err error
if *graceful {
log.Print("main: Listening to existing file descriptor 3.")
// cmd.ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
// when we put socket FD at the first entry, it will always be 3(0+3)
     //为什么是3呢,而不是1 0 或者其他数字?这是因为父进程里给了个fd给子进程了 而子进程里0,1,2是预留给 标准输入、输出和错误的,所以父进程给的第一个fd在子进程里顺序排就是从3开始了;如果fork的时候cmd.ExtraFiles给了两个文件句柄,那么子进程里还可以用4开始,就看你开了几个子进程自增就行。因为我这里就开一个子进程所以把3写死了。l, err = net.FileListener(f)这一步只是把 fd描述符包装进TCPListener这个结构体。
f := os.NewFile(3, "")
     //先复制fd到新的fd, 然后设置子进程exec时自动关闭父进程的fd,即“F_DUPFD_CLOEXEC”
listener, err =
net.FileListener(f)
} else {
log.Print("main: Listening on a new file descriptor.")
listener, err = net.Listen("tcp", server.Addr)
} if err != nil {
log.Fatalf("listener error: %v", err)
} go func() {
// server.Shutdown() stops Serve() immediately, thus server.Serve() should not be in main goroutine
err = server.Serve(listener)
log.Printf("server.Serve err: %v\n", err)
}()
signalHandler()
log.Printf("signal end")
} func reload() error {
tl, ok := listener.(*net.TCPListener)
if !ok {
return errors.New("listener is not tcp listener")
} f, err := tl.File()
if err != nil {
return err
} args := []string{"-graceful"}
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 signalHandler() {
ch := make(chan os.Signal, )
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
for {
sig := <-ch
log.Printf("signal: %v", sig) // timeout context for shutdown
ctx, _ := context.WithTimeout(context.Background(), *time.Second)
switch sig {
case syscall.SIGINT, syscall.SIGTERM:
// stop
log.Printf("stop")
signal.Stop(ch)
server.Shutdown(ctx)
log.Printf("graceful shutdown")
return
case syscall.SIGUSR2:
// reload
log.Printf("reload")
err := reload()
if err != nil {
log.Fatalf("graceful restart error: %v", err)
}
server.Shutdown(ctx)
log.Printf("graceful reload")
return
}
}
}

我的实现


package main

import (
"net"
"net/http"
"time"
"log"
"syscall"
"os"
"os/signal"
"context"
"fmt"
"os/exec"
"flag"
)
var (
listener net.Listener
err error
server http.Server
graceful = flag.Bool("g", false, "listen on fd open 3 (internal use only)")
) type MyHandler struct { } func (*MyHandler)ServeHTTP(w http.ResponseWriter, r *http.Request){
fmt.Println("request start at ", time.Now(), r.URL.Path+"?"+r.URL.RawQuery, "request done at ", time.Now(), " pid:", os.Getpid())
time.Sleep(10 * time.Second)
w.Write([]byte("this is test response"))
fmt.Println("request done at ", time.Now(), " pid:", os.Getpid() ) } func main() {
flag.Parse()
fmt.Println("start-up at " , time.Now(), *graceful)
if *graceful {
f := os.NewFile(3, "")
listener, err = net.FileListener(f)
fmt.Printf( "graceful-reborn %v %v %#v \n", f.Fd(), f.Name(), listener)
}else{
listener, err = net.Listen("tcp", ":1111")
tcp,_ := listener.(*net.TCPListener)
fd,_ := tcp.File()
fmt.Printf( "first-boot %v %v %#v \n ", fd.Fd(),fd.Name(), listener)
} server := http.Server{
Handler: &MyHandler{},
ReadTimeout: 6 * time.Second,
}
log.Printf("Actual pid is %d\n", syscall.Getpid())
if err != nil {
println(err)
return
}
log.Printf(" listener: %v\n", listener)
go func(){//不要阻塞主进程
err := server.Serve(listener)
if err != nil {
log.Println(err)
}
}() //signals
func(){
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGHUP, syscall.SIGTERM)
for{//阻塞主进程, 不停的监听系统信号
sig := <- ch
log.Printf("signal: %v", sig)
ctx, _ := context.WithTimeout(context.Background(), 20*time.Second)
switch sig {
case syscall.SIGTERM, syscall.SIGHUP:
println("signal cause reloading")
signal.Stop(ch)
{//fork new child process
tl, ok := listener.(*net.TCPListener)
if !ok {
fmt.Println("listener is not tcp listener")
return
}
currentFD, err := tl.File()
if err != nil {
fmt.Println("acquiring listener file failed")
return
}
cmd := exec.Command(os.Args[0], "-g")
cmd.ExtraFiles, cmd.Stdout,cmd.Stderr = []*os.File{currentFD} ,os.Stdout, os.Stderr
err = cmd.Start() if err != nil {
fmt.Println("cmd.Start fail: ", err)
return
}
fmt.Println("forked new pid : ",cmd.Process.Pid)
} server.Shutdown(ctx)
fmt.Println("graceful shutdown at ", time.Now())
} }
}()
}
 
qiangjian@sun-pro:/data1/works/IdeaProjects/go_core$ go  run src/wright/hotrestart/booter.go  
start-up at -- ::34.586269 + CST m=+0.004439497 false
first-boot tcp:[::]:-> &net.TCPListener{fd:(*net.netFD)(0xc00010e000)}
// :: Actual pid is
// :: listener: &{0xc00010e000}
request start at -- ::40.287928 + CST m=+5.705965906 /aa/bb?c=d request done at -- ::40.287929 + CST m=+5.705966554 pid:
// :: signal: terminated
signal cause reloading
forked new pid :
start-up at -- ::49.689064 + CST m=+0.001613279 true
graceful-reborn &net.TCPListener{fd:(*net.netFD)(0xc0000ec000)}
// :: Actual pid is
// :: listener: &{0xc0000ec000}
request done at -- ::50.288525 + CST m=+15.706330718 pid:
// :: http: Server closed
request start at -- ::50.290622 + CST m=+15.708426906 /aa/bb?c=d request done at -- ::50.290623 + CST m=+15.708428113 pid:
request start at -- ::50.290713 + CST m=+0.603248262 /aa/bb?c=d request done at -- ::50.290714 + CST m=+0.603249293 pid:
request done at -- ::00.293988 + CST m=+10.606290169 pid:
request done at -- ::00.294043 + CST m=+25.711615717 pid:
request start at -- ::00.295554 + CST m=+10.607856283 /aa/bb?c=d request done at -- ::00.295555 + CST m=+10.607857307 pid:
request start at -- ::00.29558 + CST m=+10.607881997 /aa/bb?c=d request done at -- ::00.295581 + CST m=+10.607883004 pid:
graceful shutdown at -- ::00.79544 + CST m=+26.213000502
ab -v -k -c2 -n100 '127.0.0.1:1111/aa/bb?c=d'
This is ApacheBench, Version 2.3 <$Revision: $>
Copyright Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking 127.0.0.1 (be patient)...^C Server Software:
Server Hostname: 127.0.0.1
Server Port: Document Path: /aa/bb?c=d
Document Length: bytes Concurrency Level:
Time taken for tests: 48.292 seconds
Complete requests:
Failed requests:
Total transferred: bytes
HTML transferred: bytes
Requests per second: 0.14 [#/sec] (mean)
Time per request: 13797.702 [ms] (mean)
Time per request: 6898.851 [ms] (mean, across all concurrent requests)
Transfer rate: 0.02 [Kbytes/sec] received
kill 进程ID  #发送TERM信号
//还有一种方式去fork,和上面本质一样:
execSpec := &syscall.ProcAttr{
Env: os.Environ(),
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), lFd},
}
pid, err := syscall.ForkExec(os.Args[], os.Args, execSpec)

可以看出: ab测试器Failed为0,且console中显示老请求处理完后才shutdown,即在kill触发reload后,请求无论是老进程的旧请求,还是fork子进程后的新请求,全都处理成功,没有失败的。 这就是我们说的热重启!

systemd & supervisor

父进程退出之后,子进程会挂到1号进程上面。这种情况下使用systemd和supervisord等管理程序会显示进程处于failed的状态。解决这个问题有两个方法:

  • 使用pidfile,每次进程重启更新一下pidfile,让进程管理者通过这个文件感知到main pid的变更。
  • 更通用的做法:起一个master来管理服务进程,每次热重启master拉起一个新的进程,把旧的kill掉。这时master的pid没有变化,对于进程管理者来说进程处于正常的状态。一个简洁的实现

FD复制时细节

请看:

https://blog.csdn.net/ChrisNiu1984/article/details/7050663

http://man7.org/linux/man-pages/man2/fcntl.2.html#F_DUPFD_CLOEXEC

References

Golang服务器热重启、热升级、热更新(safe and graceful hot-restart/reload http server)详解的更多相关文章

  1. [转]Linux服务器上11种网络连接状态 和 TCP三次握手/四次挥手详解

    一.Linux服务器上11种网络连接状态: 图:TCP的状态机 通常情况下:一个正常的TCP连接,都会有三个阶段:1.TCP三次握手;2.数据传送;3.TCP四次挥手. 注:以下说明最好能结合”图:T ...

  2. 如何用 Go 实现热重启

    热重启 热重启(Zero Downtime),指新老进程无缝切换,在替换过程中可保持对 client 的服务. 原理 父进程监听重启信号 在收到重启信号后,父进程调用 fork ,同时传递 socke ...

  3. 前端搭建Linux云服务器,Nginx配置详解及部署自己项目到服务器上

    目录 搭建Linux云服务器 购买与基本配置 链接linux服务器 目录结构 基本命令 软件安装 Linux 系统启动 启动过程 运行级别 Nginx详解 1.安装 方式一:yum安装 方式二:自定义 ...

  4. Golang入门教程(十三)延迟函数defer详解

    前言 大家都知道go语言的defer功能很强大,对于资源管理非常方便,但是如果没用好,也会有陷阱哦.Go 语言中延迟函数 defer 充当着 try...catch 的重任,使用起来也非常简便,然而在 ...

  5. Node.js中的express框架,修改内容后自动更新(免重启),express热更新

    个人网站 https://iiter.cn 程序员导航站 开业啦,欢迎各位观众姥爷赏脸参观,如有意见或建议希望能够不吝赐教! 以前node中的express框架,每次修改代码之后,都需要重新npm s ...

  6. Nginx热部署 平滑升级 日志切割

    1.重载 修改nginx配置文件之后,在不影响服务的前提下想加载最新的配置,就可以重载配置即可. 操作如下: 1)修改nginx配置文件 2)nginx -t     检查nginx文件语法是否有误 ...

  7. prometheus热重启

    prometheus启动命令添加参数 --web.enable-lifecycle 然后热重启:curl -XPOST http://localhost:9090/-/reload

  8. [4G]4G模块的热重启

    最近在调试4G模块,发现在开机启动时执行的AT指令会概率性的出现返回杂乱字符串的问题.想尽了各种办法还是行不通,在系统中使用minicom敲AT指令就不会有问题,开始怀疑是串口初始化的问题,修改了很多 ...

  9. Go 如何实现热重启

    https://mp.weixin.qq.com/s/UVZKFmv8p4ghm8ICdz85wQ Go 如何实现热重启 原创 zhijiezhang 腾讯技术工程 2020-09-09  

随机推荐

  1. 无法定位程序输入点 InitializeCriticalSectionEx、GetTickCount64

    (1)方法一:在vc项目中把对应的方法名改为 InitializeCriticalSection.GetTickCount. (2)方法二:添加如下定义#define WINVER           ...

  2. tcp_connect函数

    #include <netdb.h> #include <stddef.h> #include <unistd.h> #include <strings.h& ...

  3. 关于PHP面向对象中—类的定义与对象的实例化操作以及构造、析构函数的特殊用法

    类的定义与对象的实例化操作 <?php //类里面的成员是属于对象的class Hero{    public $name;//成员变量    //成员属性(存在于强类型语言中)    prot ...

  4. luogu P4173 残缺的字符串

    传送门 两种做法,一种是依次考虑每种字符,然后如果某个位置是该字符或者是\(*\)对应的值就是1,否则是0,然后把第一个串倒过来,fft卷积起来,最后看对应位置的值是否为m 然而上面那个做法在字符集大 ...

  5. linux 截图工具 shutter

    ubuntu 安装shutter sudo apt install shutter libgoo-canvas-perl libgoo-canvas-perl是提供对截图编辑功能,例如,添加画框,文字 ...

  6. Got error 28 from storage engine 解决方法

    早上一来,jira点击任何页面都会报错,首先查看服务器的日志 然后服务器首先看了磁盘,果不其然,是根目录的磁盘满了, 然后就一层一层找占用最大的文件 命令: cd /usr du -sh * 找到了文 ...

  7. windows server 2012 配置多用户ftp服务器配置注意点

    1.ftp根目录配置“FTP授权规则”为: 2.配置“FTP用户隔离”为: 3.配置“FTP目录浏览”为: 4.ftp虚拟目录“FTP授权规则”配置为:

  8. 【CentOS】JDK的安装

    FTP文件上传方式(推荐) # 解压 tar zxvf jdk-9_linux-x64_bin.tar.gz # 修改profile文件 sudo vi /etc/profile # 在文件结尾添加如 ...

  9. J - Joyful HDU - 5245 (概率)

    题目链接: J - Joyful  HDU - 5245 题目大意:给你一个n*m的矩阵,然后你有k次涂色机会,然后每一次可以选定当前矩阵的一个子矩阵染色,问你这k次用完之后颜色个数的期望. 具体思路 ...

  10. Git更新远程仓库代码到本地(转)

    参考链接:https://blog.csdn.net/chailyuan/article/details/53292031 在下载一个较大的github项目以后,当该项目代码更新以后,我们想将更新的内 ...