引出問題
我們在用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": ""
}