热重启(Zero Downtime),指新老进程无缝切换,在替换过程中可保持对 client 的服务。
fork
,同时传递 socket
描述符给子进程socket
描述符package main
import (
"context"
"errors"
"flag"
"log"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"syscall"
"time"
)
/************************** 测试热重启 ***************************/
var (
server *http.Server
listener net.Listener = nil
graceful = flag.Bool("graceful",false,"listen on fd open 3 (internal use only)")
message = flag.String("message","hello world","message to send")
)
func handler(w http.ResponseWriter,r *http.Request){
time.Sleep(5 * time.Second)
w.Write([]byte(*message))
}
func main(){
var err error
//解析参数
flag.Parse()
//开启服务器
http.HandleFunc("/test",handler)
server = &http.Server{Addr: ":3000"}
//设置监听的对象(新建或已存在的socket描述符)
if *graceful {
//子进程监听父进程传递的 socket描述符
log.Println("listening on the existing file descriptor 3")
//子进程的 0 1 2 是预留给 标准输入 标准输出 错误输出
//因此传递的socket 描述符应该放在子进程的 3
f := os.NewFile(3,"")
listener,err = net.FileListener(f)
}else{
//父进程监听新建的 socket 描述符
log.Println("listening on a new file descriptor")
listener,err = net.Listen("tcp",server.Addr)
}
if err != nil{
log.Fatalf("listener error: %v\n",err)
}
go func(){
err = server.Serve(listener)
log.Printf("server.Serve err: %v\n",err)
}()
//监听信号
handleSignal()
log.Println("signal end")
}
//处理信号
func handleSignal(){
//把信号 赋值给 通道
ch := make(chan os.Signal,1)
//监听信号
signal.Notify(ch, syscall.SIGINT,syscall.SIGTERM,syscall.SIGUSR2)
for{
//通道 赋值给 sig
sig := <-ch
log.Printf("signal receive: %v \n",sig)
ctx,_ := context.WithTimeout(context.Background(),20*time.Second)
switch sig{
case syscall.SIGINT,syscall.SIGTERM: //终止进程执行
log.Println("shutdown")
signal.Stop(ch)
server.Shutdown(ctx)
log.Println("graceful shutdown")
return
case syscall.SIGUSR2: //进程热重启
log.Println("reload")
err := reload() //执行热重启
if err != nil{
log.Fatalf("graceful reload error: %v\n",err)
}
server.Shutdown(ctx)
log.Println("graceful reload")
return
}
}
}
//热重启
func reload() error{
tl,ok := listener.(*net.TCPListener)
if !ok {
return errors.New("listener is not a tcp listener")
}
//获取socket描述符
f,err := tl.File()
if err != nil{
return err
}
//设置传递给子进程的参数(包含 socket描述符)
args := []string{"-graceful"}
cmd := exec.Command(os.Args[0],args...)
cmd.Stdout = os.Stdout //标准输出
cmd.Stderr = os.Stderr //错误输出
cmd.ExtraFiles = []*os.File{f} //文件描述符
//新建并执行子进程
return cmd.Start()
}
/*
我们在父进程执行 cmd.ExtraFiles = []*os.File{f} 来传递 socket 描述符给子进程,子进程通过执行 f := os.NewFile(3, "") 来获取该描述符。值得注意的是,子进程的 0 、1 和 2 分别预留给标准输入、标准输出和错误输出,所以父进程传递的 socket 描述符在子进程的顺序是从 3 开始。
*/
[root@localhost hotupdate]# go mod init hotupdate
go: creating new go.mod: module hotupdate
[root@localhost hotupdate]# go build
[root@localhost hotupdate]# ll
total 7440
-rw-------. 1 root root 26 Jul 18 10:32 go.mod
-rwxr-xr-x. 1 root root 7606372 Jul 18 10:34 hotupdate
-rw-r--r--. 1 root root 3108 Jul 18 10:34 main.go
[root@localhost hotupdate]# ./hotupdate -message echo
2019/07/18 10:35:49 listening on a new file descriptor
[root@localhost ~]# curl http://127.0.0.1:3000/test
echo
[root@localhost hotupdate]# netstat -tunlp | grep 3000
tcp6 0 0 :::3000 :::* LISTEN 2198/./hotupdate
[root@localhost hotupdate]# kill -USR2 2198
2019/07/18 10:54:01 signal receive: user defined signal 2
2019/07/18 10:54:01 reload
2019/07/18 10:54:01 graceful reload
2019/07/18 10:54:01 signal end
[root@localhost hotupdate]# netstat -tunlp | grep 3000
tcp6 0 0 :::3000 :::* LISTEN 2639/./hotupdate
[root@localhost hotupdate]# kill -INT 2639
2019/07/18 11:11:16 signal receive: interrupt
2019/07/18 11:11:16 shutdown
2019/07/18 11:11:16 graceful shutdown
2019/07/18 11:11:16 signal end
参考:https://studygolang.com/articles/21952
参考:https://www.cnblogs.com/sunsky303/p/9778466.html
graceful:https://github.com/kuangchanglang/graceful