服务端代码经常需要升级,对于线上系统的升级常用的做法是,通过前端的负载均衡(如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. HttpService

    // // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler ...

  2. docker搭建lnmp环境

    1.搭建lnmp网站平台 1.创建mysql数据库容器 docker run -it -d --name lnmp_mysql -p 3308:3306 -e MYSQL_ROOT_PASSWORD= ...

  3. JavaSE回顾及巩固的自学之路(一)——————前言

    长长的呼出一口气,终于到了末端了,JavaSE,Java入门的基础,历经一个多月,终于细细的学了一遍,大部分的时间都是熬夜到半夜两点钟.本来是一个月的课程,足足让我给学了将近两个月,为什么呢?第一,还 ...

  4. 20155324 《Java程序设计》实验五 网络编程与安全

    20155324 <Java程序设计>实验五 网络编程与安全 实验内容 任务一 编写MyBC.java实现中缀表达式转后缀表达式的功能 编写MyDC.java实现从上面功能中获取的表达式中 ...

  5. luogu P3297 [SDOI2013]逃考

    传送门 gugugu 首先每个人管理的区域是一个多边形,并且整个矩形是被这样的多边形填满的.现在的问题是求一条经过多边形最少的路径到达边界,这个可以最短路. 现在的问题是建图,显然我们应该给相邻的多边 ...

  6. java中出现内存溢出的几种情况

    情况一:java.lang.OutOfMemoryError: Java heap space 原因:java堆内存不足,可能是真的不足,也可能是程序中有死循环 方案:1.调整JVM参数-Xms204 ...

  7. SpringBoot - 添加定时任务

    SpringBoot 添加定时任务 EXample1: import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spri ...

  8. canvas粒子背景

  9. stega -- Pcat老入群题

    stega -- Pcat老入群题 Pcat师傅的题果然给力,就是看着wp也是琢磨了半天. WP地址:http://mp.weixin.qq.com/s/T9jJLACiZNB6FR226IjmEA ...

  10. AutoML总结

    原文:JxKing的博客 | JxKing Blog 前言 AutoML是指尽量不通过人来设定超参数,而是使用某种学习机制,来调节这些超参数.这些学习机制包括传统的贝叶斯优化,多臂老虎机(multi- ...