网站首页 > 基础教程 正文
什么是热重启
所谓热重启, 就是当关闭一个正在运行的进程时,该进程并不会立即停止,而是会等待所有当前逻辑继续执行完毕,才会中断。这就要求我们的服务需要支持一条重启命令,通过该命令可以重启服务,并同时保证重启过程中正在执行的逻辑不会中断,且重启后可以继续正常服务。
热重启的原理
热重启的原理会涉及到一些linux下系统调用以及进程之间socket句柄传递等细节,处理过程可以分为以下几个步骤:
1、监听重启信号 (SIGHUP);
2、收到重启信号时fork子进程,同时需要将服务监听的socket文件描述符传递给子进程;
3、子进程接收并监听父进程传递的socket (这时候父进程和子进程都可以接收请求);
4、等待子进程启动成功之后,停止父进程对新连接的接收 (父进程会等待旧连接逻辑处理完成);
5、父进程退出,重启完成
实现代码如下:
package main
import (
"flag"
"fmt"
"golang.org/x/net/context"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"strconv"
"syscall"
"time"
)
var (
server *http.Server
listener net.Listener
// 子进程的标志
child = flag.Bool("child", false, "")
)
// init
func init() {
updatePidFile()
}
// 更新pid文件
func updatePidFile() {
oldPid := fmt.Sprint(os.Getpid())
// 创建临时目录
tmpDir := os.TempDir()
// 判断进程是否启动
if err := procExist(tmpDir); err != nil {
fmt.Printf("pid file exists, update\n")
} else {
fmt.Printf("pid file NOT exists, create\n")
}
// 创建pid文件
pidFile, _ := os.Create(tmpDir + "/gracefulRestart.pid")
defer pidFile.Close()
// 写入文件内容
pidFile.WriteString(oldPid)
}
// 检查进程是否存在
func procExist(tmpDir string) (err error) {
// 打开文件
pidFile, err := os.Open(tmpDir + "/gracefulRestart.pid")
defer pidFile.Close()
if err != nil {
return
}
// 读取文件内容
filePid, err := ioutil.ReadAll(pidFile)
if err != nil {
return
}
pid, _ := strconv.Atoi(fmt.Sprintf("%s", filePid))
// 查找pid进程
if _, err = os.FindProcess(pid); err != nil {
fmt.Printf("Failed to find process: %v\n", err)
return
}
return
}
func main() {
flag.Parse()
// 启动监听
http.HandleFunc("/hello", HelloHandler)
server = &http.Server{Addr: ":8081"}
var err error
if *child { // 子进程
fmt.Println("In Child, Listening...")
f := os.NewFile(3, "")
listener, err = net.FileListener(f)
} else {
fmt.Println("In Father, Listening...")
listener, err = net.Listen("tcp", server.Addr)
}
if err != nil {
fmt.Printf("Listening failed: %v\n", err)
return
}
// go协程启动server
go func() {
err = server.Serve(listener)
if err != nil {
fmt.Printf("server.Serve failed: %v\n", err)
}
}()
// 监听系统信号
signalHandler()
fmt.Printf("singalHandler end\n")
}
func HelloHandler(w http.ResponseWriter, r *http.Request) {
//time.Sleep(30 * time.Second)
for i := 0; i < 20; i++ {
log.Printf("working %v\n", i)
time.Sleep(1 * time.Second)
}
w.Write([]byte("hello world..."))
}
// 信号处理
func signalHandler() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
for {
sig := <-ch
fmt.Printf("signal: %v\n", sig)
ctx, _ := context.WithTimeout(context.Background(), 20*time.Second)
switch sig {
case syscall.SIGINT, syscall.SIGTERM: // 终止信号
log.Printf("stop")
signal.Stop(ch)
server.Shutdown(ctx)
fmt.Printf("graceful shutdown\n")
return
case syscall.SIGHUP: // 重启信号
// reload
log.Printf("restart")
err := restart()
if err != nil {
fmt.Printf("graceful restart failed: %v\n", err)
}
// 更新当前pid文件
updatePidFile()
// golang >1.8,可以通过调用Golang中的Server.Shutdown()方法直接实现graceful stop
// 老版本golang自己实现一个shutdown功能步骤:
// 1、关闭listenr,停止接收新请求;
// 2、通过sync.WaitGroup.wait()阻塞服务退出,从而实现等待其他逻辑的全部退出
server.Shutdown(ctx)
fmt.Printf("graceful reload\n")
return
}
}
}
// 重启
func restart() error {
// 提取文件描述符
tl, ok := listener.(*net.TCPListener)
if !ok {
return fmt.Errorf("listener is not tcp listener")
}
f, err := tl.File()
if err != nil {
return err
}
// 创建子进程,同时传递了child参数到子进程中,从而可以执行在进程监听时走子进程创建socket的流程。
args := []string{"-child"}
cmd := exec.Command(os.Args[0], args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// cmd的ExtraFiles参数会将额外的文件描述符传递给继承的新进程(不包括标准输入、标准输出和标准错误)
// 父进程传递listener的fd给子进程,而子进程里0、1、2是预留给标准输入、输出和错误的,所以父进程给的第一个fd在子进程里顺序排就是从3开始,
// 即上文中的os.NewFile(3,"")中3的由来
cmd.ExtraFiles = []*os.File{f}
return cmd.Start()
}
测试:
$ go run main.go
pid file NOT exists, create
In Father, Listening... // 父进程启动
2022/01/21 17:42:21 working 0
2022/01/21 17:42:22 working 1
2022/01/21 17:42:23 working 2
2022/01/21 17:42:24 working 3
2022/01/21 17:42:25 working 4
2022/01/21 17:42:26 working 5
2022/01/21 17:42:27 working 6
2022/01/21 17:42:28 working 7
2022/01/21 17:42:29 working 8
2022/01/21 17:42:30 working 9
2022/01/21 17:42:31 working 10
2022/01/21 17:42:32 working 11
2022/01/21 17:42:33 working 12
2022/01/21 17:42:34 working 13
2022/01/21 17:42:35 working 14
2022/01/21 17:42:36 working 15 // 执行到这里时进行重启
# 通过端口号8081查看对应的pid
$ lsof -i :8081
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
main 79963 admin 3u IPv6 0xeacf6c71139ace71 0t0 TCP *:sunproxyadmin (LISTEN)
# 重启(发送 SIGHUP 信号)
$ kill -HUP 79963
signal: hangup
2022/01/21 17:42:36 restart
pid file NOT exists, create
server.Serve failed: http: Server closed
pid file NOT exists, create
In Child, Listening... // 子进程启动
2022/01/21 17:42:37 working 16 // 重启成功后,继续执行,不中断
2022/01/21 17:42:38 working 17
2022/01/21 17:42:39 working 18
2022/01/21 17:42:40 working 19
graceful reload
singalHandler end
猜你喜欢
- 2024-11-26 golang defer、panic、recover实践
- 2024-11-26 go-fastdfs文件上传
- 2024-11-26 OpenHarmony3.0在树莓派3B上的烧录与通讯
- 2024-11-26 go|bytes.buffer
- 2024-11-26 AOVX资产跟踪产品测试工具mPower1203如何使用python对接 (二)
- 2024-11-26 Python os.dup2() 方法是什么?os.du
- 2024-11-26 go中关于文件和json操作的知识点小结
- 2024-11-26 python3从零学习-5.8.4、mmap—内存映射文件支持
- 2024-11-26 29.Python 读取文件的六种方式
- 2024-11-26 macOS安装并设置五笔输入法
- 最近发表
- 标签列表
-
- gitpush (61)
- pythonif (68)
- location.href (57)
- tail-f (57)
- pythonifelse (59)
- deletesql (62)
- c++模板 (62)
- css3动画 (57)
- c#event (59)
- linuxgzip (68)
- 字符串连接 (73)
- nginx配置文件详解 (61)
- html标签 (69)
- c++初始化列表 (64)
- exec命令 (59)
- canvasfilltext (58)
- mysqlinnodbmyisam区别 (63)
- arraylistadd (66)
- node教程 (59)
- console.table (62)
- c++time_t (58)
- phpcookie (58)
- mysqldatesub函数 (63)
- window10java环境变量设置 (66)
- c++虚函数和纯虚函数的区别 (66)