时间: 2020-09-03 00:08:26 人气: 2282 评论: 0
这几天在写组里的一个http框架,于是研究了下,在golang中如何实现服务的热重启,从而实现整个服务的重启可以实现对请求客户端的透明。
所谓热重启, 就是当关闭一个正在运行的进程时,该进程并不会立即停止,而是会等待所有当前逻辑继续执行完毕,才会中断。这就要求我们的服务需要支持一条重启命令,通过该命令我们可以重启服务,并同时保证重启过程中正在执行的逻辑不会中断,且重启后可以继续正常服务。
之前在写C++服务的时候实现过热重启,其实原理还是非常简单的,只是会需要涉及到一些linux下系统调用以及进程之间socket句柄传递等细节,为了怕写错,又翻了几篇文章,总的来看,处理过程可以分为以下几个步骤:
关于上述几点,需要说明下:对于1,仅仅是我们后文将以SIGHUP信号来表示重启,同时需要了解到的是,在第3步,这个时候父进程和子进程都可以接收请求,而在第4步,此时父进程会等待旧连接逻辑处理完成。
// 启动监听 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 }
上述的代码段中,实现了一个简单的服务监听。其中child是子进程的标志,我们可以看到在子进程分支中,通过os.NewFile(3,"")打开了文件描述符为3的文件并转为网络监听句柄(至于为什么是3呢,而不是0、1或者其他数字?我们在下面介绍)。
func singalHandler() { 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) } //更新当前pidfile updatePidFile() //带超时的优雅停止 server.Shutdown(ctx) fmt.Printf("graceful reload\n") return } } }
上述代码段中,我们监听了系统的SIGINT、SIGTERM和SIGHUP信号,其中,对于SIGINT和SIGTERM信号,我们认定为终止信号,需要graceful stop。对于SIGHUP信号,我们认定为重启信号,此时需要执行graceful restart(热重启操作)。
需要注意的是,为了实现graceful stop,在以往我们需要自己实现一个这样的shutdown功能:
1.关闭listenr,停止接收新请求;
2.通过sync.WaitGroup.wait()阻塞服务退出,从而实现等待其他逻辑的全部退出;
然而,得益于golang的更新(>1.8),如上述代码段所示,现在通过调用Golang中的Server.Shutdown()方法就可以直接实现。
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 } args := []string{"-child"} 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() }
上述的代码段中,通过系统调用exec.Command()创建了一个子进程,同时传递了child参数到了子进程中,从而可以执行在进程监听时走子进程创建socket的流程。这儿就回到了上文中我们抛出的os.NewFile(3,"")中的3是如何来的问题了,cmd的ExtraFiles参数会将额外的文件描述符传递给继承的新进程(不包括标准输入、标准输出和标准错误),在这儿父进程给了个listener的fd给子进程了,而子进程里0、1、2是预留给标准输入、输出和错误的,所以父进程给的第一个fd在子进程里顺序排就是从3开始了(需要注意的是,ExtraFiles是不支持Windows操作系统的)。
附录:
基本上上述就是一个完整的热重启逻辑了,下面附上完成的代码段:
package main import ( "flag" "net/http" "net" "log" "os" "os/signal" "syscall" "golang.org/x/net/context" "time" "os/exec" "fmt" "io/ioutil" "strconv" ) var ( server *http.Server listener net.Listener child = flag.Bool("child", false, "") ) func init() { updatePidFile() } func updatePidFile() { sPid := fmt.Sprint(os.Getpid()) tmpDir := os.TempDir() if err := procExsit(tmpDir); err != nil { fmt.Printf("pid file exists, update\n") } else { fmt.Printf("pid file NOT exists, create\n") } pidFile, _ := os.Create(tmpDir + "/gracefulRestart.pid") defer pidFile.Close() pidFile.WriteString(sPid) } // 判断进程是否启动 func procExsit(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 } pidStr := fmt.Sprintf("%s", filePid) pid, _ := strconv.Atoi(pidStr) 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) } }() //监听系统信号 singalHandler() fmt.Printf("singalHandler end\n") } func HelloHandler(w http.ResponseWriter, r *http.Request) { //time.Sleep(20 * time.Second) for i := 0; i < 20; i++ { log.Printf("working %v\n", i) time.Sleep(1 * time.Second) } w.Write([]byte("world233333!!!!")) } func singalHandler() { 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) } //更新当前pidfile updatePidFile() server.Shutdown(ctx) fmt.Printf("graceful reload\n") 技术沙龙 教程文章 热点综合