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

    https://blog.csdn.net/w05980598/article/details/79053209

  2. javascript文件加载模式与加载方法

    加载方式 形象图像化方法,见 http://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html 1. script标签, ...

  3. GCC编译器原理(二)------编译原理一:ELF文件(2)

    四. ELF 文件格式分析 ELF文件(目标文件)格式主要四种: 可重定向文件: 文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件.(目标文件或者静态库文 ...

  4. JavaScript 小工具

    1. 字符串格式化输出 支持形如: Orders of {1} or more {0}' {0},{1}代表第几个参数,包含了完善的异常处理.当给定参数少于格式化串中占位符个数时,未找到的直接留白. ...

  5. [C++]猜数字游戏的提示(Master-Mind Hints,UVa340)

    [本博文非博主原创,思路与题目均摘自 刘汝佳<算法竞赛与入门经典(第2版)>] Question 例题3-4 猜数字游戏的提示(Master-Mind Hints,UVa340) 实现一个 ...

  6. lua 语言笔记

    Lua语言基础汇总(1) -- 类型与值 基础介绍 Lua是一种动态类型的语言.在语言中没有类型定义的语法,每个值都带有其自身的类型信息.在Lua中有8种基本类型,分别是: nil(空)类型 bool ...

  7. 数据库截取字符串SUBSTR函数的使用

    背景 今天中午做需求的时候,有类似于根据银行卡卡号的前几位判断出是哪个银行的情况,每个银行需要截取的位数都不一样,这时我就想到了SUBSTR 数据库截取字符串SUBSTR函数的使用 假设有一个表的结构 ...

  8. JavaScript实现框选效果

    <html> <head> <title>region</title> <style> body { margin: 0; padding: ...

  9. Java基础_0205: 程序逻辑结构

    使用if语句进行判断 public class TestDemo { public static void main(String args[]) { double score = 90.0; // ...

  10. 数组去重的4种方法(Which one is the fastest???嘻嘻嘻....)

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...