Golang實現平滑重啟(優雅重啟)
最近在看traefik的原始碼, 看到其中有一個功能是平滑重啟, 不過他是通過一個叫做ofollow,noindex" target="_blank">graceful 的庫來做到的, 是在HTTP Server的層級. 於是我探索了一下方案, 在TCP層級做了一個demo出來.
先看traefik的實現方案
上面說的graceful這個庫,如它在Github的簡介所說: "Graceful is a Go package enabling graceful shutdown of an http.Handler server." 只提供了優雅關閉, 不提供優雅重啟. 那麼什麼叫做優雅關閉呢? 意思就是伺服器要關閉了, 會拒絕新的連線,但是老的連線不會被強制關閉,而是 會等待一定時間, 等待客戶端主動關閉, 除非客戶端一直沒有關閉, 到了預設的超時時間才進行伺服器端關閉.
我們來看看traefik是怎麼做的:
func main() { // goroutine 0 goAway := false go func() {// goroutine 1 sig := <-sigs fmt.Println("I have to go...", sig) goAway = true srv.Stop(10 * time.Second) }() for{ if (goAway){ break } fmt.Println("Started") srv = &graceful.Server{ Timeout: 10 * time.Second, NoSignalHandling: true, ConnState: func(conn net.Conn, state http.ConnState) { fmt.Println( "Connection ", state) }, Server: &http.Server{ Addr: ":8001", Handler: userRouter, }, } go srv.ListenAndServe()// goroutine 2 <- srv.StopChan() // goroutine 0 fmt.Println("Stopped") } }
可以看到, 我們用goroutine + 數字
來表示所註釋的程式碼會在哪個goroutine裡執行, main函式我們假設是在goroutine 0裡執行.
sigs
而追蹤進去就會發現,srv.StopChan()
是一個確保 graceful Server正確初始化srv.stopChan
的函式. 搜尋stopChan, 我們可以看到
func (srv *Server) shutdown(shutdown chan chan struct{}, kill chan struct{}) { // Request done notification done := make(chan struct{}) shutdown <- done srv.stopLock.Lock() defer srv.stopLock.Unlock() if srv.Timeout > 0 { select { case <-done: case <-time.After(srv.Timeout): close(kill) } } else { <-done } // Close the stopChan to wake up any blocked goroutines. srv.chanLock.Lock() if srv.stopChan != nil { close(srv.stopChan) } srv.chanLock.Unlock() }
在呼叫shutdown之後, 就會關閉這個channel, 然後上面所說的for迴圈就會重新初始化. 於是似乎就實現了一次 "平滑重啟". 為什麼打
引號呢? 因為在關閉伺服器端的監聽和下一次for迴圈重新執行到srv.ListenAndServe()
之間的這一段時間間隙, 很有可能會有新的
連線到來卻因為伺服器端沒有監聽而連線失敗. 所以這個實現和我們直接執行sudo systemctl restart nginx
是類似的.
更詳細的traefik原始碼分析我會另外再寫一篇部落格來分析, 這裡就此打住. 接下來來看一下簡單的在TCP層面實現平滑重啟的伺服器.
TCP平滑重啟
首先我們來看看怎麼起一個TCP伺服器:
package main import ( "fmt" "net" ) func handleConnection(conn net.Conn) { conn.Write([]byte("hello")) conn.Close() } func main() { ln, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println(err) } else { fmt.Println(ln.Addr()) } for { if conn, err := ln.Accept(); err == nil { fmt.Println("new conn...") go handleConnection(conn) } } }
為了測試,我們寫一個Python指令碼(Python果然還是更加簡潔):
import socket def foo(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("127.0.0.1", 8080)) s.close() if __name__ == "__main__": while True: foo()
執行之後就可以看到輸出.
我們知道多個程序是不可以監聽在同一個(IP地址,埠號)對上的, 即,不能對同一對(IP地址,埠號) 執行多次listen函式,我們可以做個實驗,把ListenAndServe抽出來,起另外一個goroutine去執行,為了方便 區分,我們加入一個引數,就是goroutine的名字:
package main import ( "fmt" "net" ) func handleConnection(conn net.Conn) { conn.Write([]byte("hello")) conn.Close() } func ListenAndServe(name string) { ln, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println(err) } else { fmt.Println(ln.Addr()) } fmt.Println(name) for { if conn, err := ln.Accept(); err == nil { go handleConnection(conn) } } } func main() { go ListenAndServe("server1") ListenAndServe("server2") }
執行一下:
$ go run t.go [::]:8080 server2 listen tcp :8080: bind: address already in use server1 panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x20 pc=0x4dc9a4]
但是我們可以在同一個socket對上, 共享同一個監聽套接字地址, 然後在多個goroutine中執行accept函式:
package main import ( "fmt" "net" ) func handleConnection(conn net.Conn) { conn.Write([]byte("hello")) conn.Close() } func ListenAndServe(ln net.Listener, name string) { for { if conn, err := ln.Accept(); err == nil { fmt.Println(name) go handleConnection(conn) } } } func main() { ln, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println(err) } else { fmt.Println(ln.Addr()) } go ListenAndServe(ln, "server1") ListenAndServe(ln, "server2") }
但是這還遠遠不是平滑重啟, 只是證明了平滑重啟是可行的, 畢竟平滑重啟的前提就是在父子程序中能夠
共享同一個套接字, 而且在不同的地方可以進行accept
操作. 接下來我們來看一下怎麼fork, 然後帶上
socket套接字的檔案描述符, 然後再在子程序中重新把套接字描述符還原成tcp.Listener
:
- 先來看怎麼把套接字轉換成檔案描述符, 傳遞給另外一個goroutine, 然後這個goroutine還原成listener:
package main import ( "fmt" "net" ) func handleConnection(conn net.Conn) { conn.Write([]byte("hello")) conn.Close() } func listenAndServe(ln net.Listener, name string) { for { if conn, err := ln.Accept(); err == nil { fmt.Println(name) go handleConnection(conn) } } } func main() { ln, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println(err) } else { fmt.Println(ln.Addr()) } l := ln.(*net.TCPListener) newFile, _ := l.File() fmt.Println(newFile.Fd()) anotherListener, _ := net.FileListener(newFile) go listenAndServe(anotherListener, "listener 1") listenAndServe(ln, "listener 2") }
接下來我們要在go中進行fork並且傳遞檔案描述符, 查看了文件, 可以通過exec.Cmd
裡的ExtraFiles []*os.File
來傳遞:
package main import ( "flag" "fmt" "net" "os" "os/exec" ) var ( graceful = flag.Bool("graceful", false, "-graceful") ) func handleConnection(conn net.Conn) { conn.Write([]byte("hello")) conn.Close() } func listenAndServe(ln net.Listener, name string) { for { if conn, err := ln.Accept(); err == nil { fmt.Println(name) go handleConnection(conn) } } } func gracefulRestart() { ln, err := net.FileListener(os.NewFile(3, "graceful server")) if err != nil { fmt.Println(err) } else { fmt.Println(ln) } listenAndServe(ln, "graceful server") } func main() { flag.Parse() fmt.Printf("given args: %t\n", *graceful) if *graceful { gracefulRestart() } else { ln, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println(err) } else { fmt.Println(ln.Addr()) } l := ln.(*net.TCPListener) newFD, _ := l.File() fmt.Println(newFD.Fd()) cmd := exec.Command(os.Args[0], "-graceful") cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.ExtraFiles = []*os.File{newFD} cmd.Run() } }
當然我們還可以做的更好, 例如讓graceful server支援再次graceful restart, 於是程式碼變成了這樣:
ckage main import ( "flag" "fmt" "net" "os" "os/exec" "os/signal" "syscall" ) var ( graceful = flag.Bool("graceful", false, "-graceful") ) // Accepted accepted connection type Accepted struct { conn net.Conn errerror } func handleConnection(conn net.Conn) { conn.Write([]byte("hello")) conn.Close() } func listenAndServe(ln net.Listener, sig chan os.Signal) { accepted := make(chan Accepted, 1) go func() { for { conn, err := ln.Accept() accepted <- Accepted{conn, err} } }() for { select { case a := <-accepted: if a.err == nil { fmt.Println("handle connection") go handleConnection(a.conn) } case _ = <-sig: fmt.Println("gonna fork and run") forkAndRun(ln) break } } } func gracefulListener() net.Listener { ln, err := net.FileListener(os.NewFile(3, "graceful server")) if err != nil { fmt.Println(err) } return ln } func firstBootListener() net.Listener { ln, err := net.Listen("tcp", ":8080") if err != nil { fmt.Println(err) } return ln } func forkAndRun(ln net.Listener) { l := ln.(*net.TCPListener) newFile, _ := l.File() fmt.Println(newFile.Fd()) cmd := exec.Command(os.Args[0], "-graceful") cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr cmd.ExtraFiles = []*os.File{newFile} cmd.Run() } func main() { flag.Parse() fmt.Printf("given args: %t, pid: %d\n", *graceful, os.Getpid()) c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGUSR1) var ln net.Listener if *graceful { ln = gracefulListener() } else { ln = firstBootListener() } listenAndServe(ln, c) }