關於GoLang服務的平滑重啓

引出問題

我們在用go開發的過程中,如果修改了代碼,都是control+c殺死運行的進程,然後再go run 或者是 go build之後運行,而當我們的項目上線後,直接殺死進程會導致線上服務中斷,在生產環境中是絕對不允許的

 

解決思路

更改代碼之後,重新編譯,重啓進程時,當前主進程fork出一個子進程,讓它運行改動後的程序。

 

實現細節

如何通知進程進行平滑重啓呢,答案是通過註冊SIGHUP信號量,在handle方法中去進行處理。那麼在fork出子進程時,都做了什麼呢,是如何處理正在執行的服務呢?我們知道所有的連接都是通過socket文件描述符來進行通信的,所以我們只要獲取父進程的socket文件描述符,賦值給新fork出來的子進程,此時再來新的請求,文件描述符指向的是新的子進程,所以都由子進程來處理,當父進程處理完當前請求後,則會執行SIGTERM信號將其殺死,此時子進程由於沒有了父進程,所以變爲了殭屍進程,移交給1號系統進程接管。大致代碼如下:

用endless框架去註冊信號量處理方法(go get github.com/fvbock/endless)

server := gin.New();
group := server.Group("")
group.GET("/ping", func(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"errno": 0,
		"errmsg": "success",
		"data":	"",
		"user_msg": "",
	})
})

tmpServer := endless.NewServer(fmt.Sprintf(":%s", strconv.Itoa(Port)), server)
tmpServer.BeforeBegin = func(add string) {
	log.Printf("Actual pid is %d", syscall.Getpid())
}
err := tmpServer.ListenAndServe()
if err != nil {
	log.Printf("Server err: %v", err)
}

下面是endless的源碼

ListenAndServe:

func (srv *endlessServer) ListenAndServe() (err error) {
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}

	go srv.handleSignals()  //註冊信號量處理方法

	l, err := srv.getListener(addr)
	if err != nil {
		log.Println(err)
		return
	}

	srv.EndlessListener = newEndlessListener(l, srv)

	if srv.isChild {
		syscall.Kill(syscall.Getppid(), syscall.SIGTERM) //子進程中通過SIGTERM信號kill掉父進程
	}

	srv.BeforeBegin(srv.Addr)

	return srv.Serve()
}

handleSignals:

func (srv *endlessServer) handleSignals() {
	var sig os.Signal

	signal.Notify(
		srv.sigChan,
		hookableSignals...,
	)

	pid := syscall.Getpid()
	for {
		sig = <-srv.sigChan
		srv.signalHooks(PRE_SIGNAL, sig)
		switch sig {
		case syscall.SIGHUP:
			log.Println(pid, "Received SIGHUP. forking.")
			err := srv.fork() //fork子進程 
			if err != nil {
				log.Println("Fork err:", err)
			}
		case syscall.SIGUSR1:
			log.Println(pid, "Received SIGUSR1.")
		case syscall.SIGUSR2:
			log.Println(pid, "Received SIGUSR2.")
			srv.hammerTime(0 * time.Second)
		case syscall.SIGINT:
			log.Println(pid, "Received SIGINT.")
			srv.shutdown()
		case syscall.SIGTERM:
			log.Println(pid, "Received SIGTERM.")
			srv.shutdown()
		case syscall.SIGTSTP:
			log.Println(pid, "Received SIGTSTP.")
		default:
			log.Printf("Received %v: nothing i care about...\n", sig)
		}
		srv.signalHooks(POST_SIGNAL, sig)
	}
}

fork去賦值socket文件描述符:

func (srv *endlessServer) fork() (err error) {
	runningServerReg.Lock()
	defer runningServerReg.Unlock()

	// only one server instance should fork!
	if runningServersForked {
		return errors.New("Another process already forked. Ignoring this one.")
	}

	runningServersForked = true

	var files = make([]*os.File, len(runningServers))
	var orderArgs = make([]string, len(runningServers))
	// get the accessor socket fds for _all_ server instances
	for _, srvPtr := range runningServers {
		// introspect.PrintTypeDump(srvPtr.EndlessListener)
		switch srvPtr.EndlessListener.(type) {
		case *endlessListener:
			// normal listener
			files[socketPtrOffsetMap[srvPtr.Server.Addr]] = srvPtr.EndlessListener.(*endlessListener).File()
		default:
			// tls listener
			files[socketPtrOffsetMap[srvPtr.Server.Addr]] = srvPtr.tlsInnerListener.File()
		}
		orderArgs[socketPtrOffsetMap[srvPtr.Server.Addr]] = srvPtr.Server.Addr
	}

	env := append(
		os.Environ(),
		"ENDLESS_CONTINUE=1",
	)
	if len(runningServers) > 1 {
		env = append(env, fmt.Sprintf(`ENDLESS_SOCKET_ORDER=%s`, strings.Join(orderArgs, ",")))
	}

	// log.Println(files)
	path := os.Args[0]
	var args []string
	if len(os.Args) > 1 {
		args = os.Args[1:]
	}

	cmd := exec.Command(path, args...)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.ExtraFiles = files
	cmd.Env = env

	// cmd.SysProcAttr = &syscall.SysProcAttr{
	// 	Setsid:  true,
	// 	Setctty: true,
	// 	Ctty:    ,
	// }

	err = cmd.Start()
	if err != nil {
		log.Fatalf("Restart: Failed to launch, error: %v", err)
	}

	return
}

運行結果

1、編譯並啓動程序

localhost:go why$ go build main.go 
localhost:go why$ ./main.go 
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (1 handlers)
2020/02/08 17:52:26 Actual pid is 16333

2、調用接口

curl -XGET localhost:777/ping

{
    "data": "",
    "errmsg": "success",
    "errno": 0,
    "user_msg": ""
}

3、改動代碼

c.JSON(http.StatusOK, gin.H{
	"errno": 0,
	"errmsg": "success",
	"data":	"new data",
	"user_msg": "",
})

4、重新編譯

go build main.go

5、平滑重啓,可以看到新進程的PID爲1

whydeMacBook-Pro:go why$ kill -1 16333
whydeMacBook-Pro:go why$ lsof -i:777
COMMAND   PID USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
main    16350  why    3u  IPv6 0x8dbd126b94b08875      0t0  TCP *:multiling-http (LISTEN)
main    16350  why    6u  IPv6 0x8dbd126b94b08875      0t0  TCP *:multiling-http (LISTEN)
whydeMacBook-Pro:go why$ ps -ef | grep 16350
  501 16350     1   0  5:53下午 ttys004    0:00.03 /var/folders/_s/jfrm6_712w58sytpc753pmr40000gn/T/go-build001868412/b001/exe/main
  501 16395 34106   0  5:56下午 ttys007    0:00.00 grep 16350
whydeMacBook-Pro:go why$

6、查看結果

{
    "data": "new data",
    "errmsg": "success",
    "errno": 0,
    "user_msg": ""
}

 

發佈了216 篇原創文章 · 獲贊 29 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章