Golang服务器热重启、热升级、热更新(safe and graceful hot-restart/reload http server)详解
时间:2022-07-23
本文章向大家介绍Golang服务器热重启、热升级、热更新(safe and graceful hot-restart/reload http server)详解,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
服务端代码经常需要升级,对于线上系统的升级常用的做法是,通过前端的负载均衡(如nginx)来保证升级时至少有一个服务可用,依次(灰度)升级。
而另一种更方便的方法是在应用上做热重启,直接更新源码、配置或升级应用而不停服务。
这个功能在重要业务上尤为重要,会影响服务可用性、用户体验。
原理
热重启的原理比较简单,但是涉及到一些系统调用以及父子进程之间文件句柄的传递等等细节比较多。 处理过程分为以下几个步骤:
- 监听信号(USR2..)
- 收到信号时fork子进程(使用相同的启动命令),将服务监听的socket文件描述符传递给子进程
- 子进程监听父进程的socket,这个时候父进程和子进程都可以接收请求
- 子进程启动成功之后,父进程停止接收新的连接,等待旧连接处理完成(或超时)
- 父进程退出,重启完成
细节
- 父进程将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(20 * 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: %vn", 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, 1)
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(), 20*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
}
}
}
我的实现
qiangjian@sun-pro:/data1/works/IdeaProjects/go_core$ go run src/wright/hotrestart/booter.go
start-up at 2018-10-12 15:29:34.586269 +0800 CST m=+0.004439497 false
first-boot 5 tcp:[::]:1111-> &net.TCPListener{fd:(*net.netFD)(0xc00010e000)}
2018/10/12 15:29:34 Actual pid is 10771
2018/10/12 15:29:34 listener: &{0xc00010e000}
request start at 2018-10-12 15:29:40.287928 +0800 CST m=+5.705965906 /aa/bb?c=d request done at 2018-10-12 15:29:40.287929 +0800 CST m=+5.705966554 pid: 10771
2018/10/12 15:29:49 signal: terminated
signal cause reloading
forked new pid : 10775
start-up at 2018-10-12 15:29:49.689064 +0800 CST m=+0.001613279 true
graceful-reborn 3 &net.TCPListener{fd:(*net.netFD)(0xc0000ec000)}
2018/10/12 15:29:49 Actual pid is 10775
2018/10/12 15:29:49 listener: &{0xc0000ec000}
request done at 2018-10-12 15:29:50.288525 +0800 CST m=+15.706330718 pid: 10771
2018/10/12 15:29:50 http: Server closed
request start at 2018-10-12 15:29:50.290622 +0800 CST m=+15.708426906 /aa/bb?c=d request done at 2018-10-12 15:29:50.290623 +0800 CST m=+15.708428113 pid: 10771
request start at 2018-10-12 15:29:50.290713 +0800 CST m=+0.603248262 /aa/bb?c=d request done at 2018-10-12 15:29:50.290714 +0800 CST m=+0.603249293 pid: 10775
request done at 2018-10-12 15:30:00.293988 +0800 CST m=+10.606290169 pid: 10775
request done at 2018-10-12 15:30:00.294043 +0800 CST m=+25.711615717 pid: 10771
request start at 2018-10-12 15:30:00.295554 +0800 CST m=+10.607856283 /aa/bb?c=d request done at 2018-10-12 15:30:00.295555 +0800 CST m=+10.607857307 pid: 10775
request start at 2018-10-12 15:30:00.29558 +0800 CST m=+10.607881997 /aa/bb?c=d request done at 2018-10-12 15:30:00.295581 +0800 CST m=+10.607883004 pid: 10775
graceful shutdown at 2018-10-12 15:30:00.79544 +0800 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: 1826891 $>
Copyright 1996 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: 1111
Document Path: /aa/bb?c=d
Document Length: 21 bytes
Concurrency Level: 2
Time taken for tests: 48.292 seconds
Complete requests: 7
Failed requests: 0
Total transferred: 966 bytes
HTML transferred: 147 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[0], 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
- 基于Windows下处理Java错误:编码GBK的不可映射字符的解决方案
- 浅析ASCII、Unicode和UTF-8三种常见字符编码
- 【Java学习笔记之三十一】详解Java8 lambda表达式
- 2017 Multi-University Training Contest - Team 9 1003&&HDU 6163 CSGO【计算几何】
- 【Code】关关的刷题日记21——Leetcode 485. Max Consecutive Ones
- 2017 Multi-University Training Contest - Team 9 1002&&HDU 6162 Ch’s gift【树链部分+线段树】
- 【Java学习笔记之三十二】浅谈Java中throw与throws的用法及异常抛出处理机制剖析
- Linux上访问SQL Server数据库
- 2017 Multi-University Training Contest - Team 9 1001&&HDU 6161 Big binary tree【树形dp+hash】
- 【Java学习笔记之三十三】详解Java中try,catch,finally的用法及分析
- 【Java学习笔记之二十九】Java中的"equals"和"=="的用法及区别
- NET跨平台:在Ubuntu下搭建ASP.NET 5开发环境
- 【Code】关关的刷题日记22——Leetcode 53. Maximum Subarray
- 【Java学习笔记之三十四】超详解Java多线程基础
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 对JiaThis Flash XSS的挖掘与分析
- Spark 3.0.1 Structured Streaming 提交程序异常解决
- 一起来探索下小程序包的魔数
- 新浪微博IPAD客户端XSS(file域) + 构造Worm
- Firefox 31~34远程命令执行漏洞的分析
- emlog绕过验证码刷评论
- cmseasy最新注入+360webscan的绕过分析
- 新型任意文件读取漏洞的研究
- Chrome XSS Auditor Bypass Using SVG
- ngx_lua_waf针对性改写
- Wordpress < 4.1.2 存储型XSS分析与稳定POC
- 重构Sec-News之路
- ThinkPHP留后门技巧
- 『三个白帽』某题的writeup
- 创造tips的秘籍——PHP回调后门