來自:https://segmentfault.com/a/1190000004445975
原文鏈接:http://tabalt.net/blog/gracef...
Golang支持平滑升級(優(yōu)雅重啟)的包已開源到Github:https://github.com/tabalt/gracehttp,歡迎使用和貢獻(xiàn)代碼。
前段時(shí)間用Golang在做一個(gè)HTTP的接口,因編譯型語言的特性,修改了代碼需要重新編譯可執(zhí)行文件,關(guān)閉正在運(yùn)行的老程序,并啟動新程序。對于訪問量較大的面向用戶的產(chǎn)品,關(guān)閉、重啟的過程中勢必會出現(xiàn)無法訪問的情況,從而影響用戶體驗(yàn)。
使用Golang的系統(tǒng)包開發(fā)HTTP服務(wù),是無法支持平滑升級(優(yōu)雅重啟)的,本文將探討如何解決該問題。
一、平滑升級(優(yōu)雅重啟)的一般思路
一般情況下,要實(shí)現(xiàn)平滑升級,需要以下幾個(gè)步驟:
-
用新的可執(zhí)行文件替換老的可執(zhí)行文件(如只需優(yōu)雅重啟,可以跳過這一步)
-
通過pid給正在運(yùn)行的老進(jìn)程發(fā)送 特定的信號(kill -SIGUSR2 $pid)
-
正在運(yùn)行的老進(jìn)程,接收到指定的信號后,以子進(jìn)程的方式啟動新的可執(zhí)行文件并開始處理新請求
-
老進(jìn)程不再接受新的請求,等待未完成的服務(wù)處理完畢,然后正常結(jié)束
-
新進(jìn)程在父進(jìn)程退出后,會被init進(jìn)程領(lǐng)養(yǎng),并繼續(xù)提供服務(wù)
二、Golang Socket 網(wǎng)絡(luò)編程
Socket是程序員層面上對傳輸層協(xié)議TCP/IP的封裝和應(yīng)用。Golang中Socket相關(guān)的函數(shù)與結(jié)構(gòu)體定義在net包中,我們從一個(gè)簡單的例子來學(xué)習(xí)一下Golang Socket 網(wǎng)絡(luò)編程,關(guān)鍵說明直接寫在注釋中。
1、服務(wù)端程序 server.go
package main
import (
"fmt"
"log"
"net"
"time"
)
func main() {
// 監(jiān)聽8086端口
listener, err := net.Listen("tcp", ":8086")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
for {
// 循環(huán)接收客戶端的連接,沒有連接時(shí)會阻塞,出錯(cuò)則跳出循環(huán)
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
break
}
fmt.Println("[server] accept new connection.")
// 啟動一個(gè)goroutine 處理連接
go handler(conn)
}
}
func handler(conn net.Conn) {
defer conn.Close()
for {
// 循環(huán)從連接中 讀取請求內(nèi)容,沒有請求時(shí)會阻塞,出錯(cuò)則跳出循環(huán)
request := make([]byte, 128)
readLength, err := conn.Read(request)
if err != nil {
fmt.Println(err)
break
}
if readLength == 0 {
fmt.Println(err)
break
}
// 控制臺輸出讀取到的請求內(nèi)容,并在請求內(nèi)容前加上hello和時(shí)間后向客戶端輸出
fmt.Println("[server] request from ", string(request))
conn.Write([]byte("hello " + string(request) + ", time: " + time.Now().Format("2006-01-02 15:04:05")))
}
}
2、客戶端程序 client.go
package main
import (
"fmt"
"log"
"net"
"os"
"time"
)
func main() {
// 從命令行中讀取第二個(gè)參數(shù)作為名字,如果不存在第二個(gè)參數(shù)則報(bào)錯(cuò)退出
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s name ", os.Args[0])
os.Exit(1)
}
name := os.Args[1]
// 連接到服務(wù)端的8086端口
conn, err := net.Dial("tcp", "127.0.0.1:8086")
checkError(err)
for {
// 循環(huán)往連接中 寫入名字
_, err = conn.Write([]byte(name))
checkError(err)
// 循環(huán)從連接中 讀取響應(yīng)內(nèi)容,沒有響應(yīng)時(shí)會阻塞
response := make([]byte, 256)
readLength, err := conn.Read(response)
checkError(err)
// 將讀取響應(yīng)內(nèi)容輸出到控制臺,并sleep一秒
if readLength > 0 {
fmt.Println("[client] server response:", string(response))
time.Sleep(1 * time.Second)
}
}
}
func checkError(err error) {
if err != nil {
log.Fatal("fatal error: " + err.Error())
}
}
3、運(yùn)行示例程序
# 運(yùn)行服務(wù)端程序
go run server.go
# 在另一個(gè)命令行窗口運(yùn)行客戶端程序
go run client.go "tabalt"
三、Golang HTTP 編程
HTTP是基于傳輸層協(xié)議TCP/IP的應(yīng)用層協(xié)議。Golang中HTTP相關(guān)的實(shí)現(xiàn)在net/http包中,直接用到了net包中Socket相關(guān)的函數(shù)和結(jié)構(gòu)體。
我們再從一個(gè)簡單的例子來學(xué)習(xí)一下Golang HTTP 編程,關(guān)鍵說明直接寫在注釋中。
1、http服務(wù)程序 http.go
package main
import (
"log"
"net/http"
"os"
)
// 定義http請求的處理方法
func handlerHello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("http hello on golang\n"))
}
func main() {
// 注冊http請求的處理方法
http.HandleFunc("/hello", handlerHello)
// 在8086端口啟動http服務(wù),會一直阻塞執(zhí)行
err := http.ListenAndServe("localhost:8086", nil)
if err != nil {
log.Println(err)
}
// http服務(wù)因故停止后 才會輸出如下內(nèi)容
log.Println("Server on 8086 stopped")
os.Exit(0)
}
2、運(yùn)行示例程序
# 運(yùn)行HTTP服務(wù)程序
go run http.go
# 在另一個(gè)命令行窗口curl請求測試頁面
curl http://localhost:8086/hello/
# 輸出如下內(nèi)容:
http hello on golang
四、Golang net/http包中 Socket操作的實(shí)現(xiàn)
從上面的簡單示例中,我們看到在Golang中要啟動一個(gè)http服務(wù),只需要簡單的三步:
-
定義http請求的處理方法
-
注冊http請求的處理方法
-
在某個(gè)端口啟動HTTP服務(wù)
而最關(guān)鍵的啟動http服務(wù),是調(diào)用http.ListenAndServe()函數(shù)實(shí)現(xiàn)的。下面我們找到該函數(shù)的實(shí)現(xiàn):
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
這里創(chuàng)建了一個(gè)Server的對象,并調(diào)用它的ListenAndServe()方法,我們再找到結(jié)構(gòu)體Server的ListenAndServe()方法的實(shí)現(xiàn):
func (srv *Server) ListenAndServe() error {
addr := srv.Addr
if addr == "" {
addr = ":http"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
從代碼上看到,這里監(jiān)聽了tcp端口,并將監(jiān)聽者包裝成了一個(gè)結(jié)構(gòu)體 tcpKeepAliveListener,再調(diào)用srv.Serve()方法;我們繼續(xù)跟蹤Serve()方法的實(shí)現(xiàn):
func (srv *Server) Serve(l net.Listener) error {
defer l.Close()
var tempDelay time.Duration // how long to sleep on accept failure
for {
rw, e := l.Accept()
if e != nil {
if ne, ok := e.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
time.Sleep(tempDelay)
continue
}
return e
}
tempDelay = 0
c, err := srv.newConn(rw)
if err != nil {
continue
}
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve()
}
}
可以看到,和我們前面Socket編程的示例代碼一樣,循環(huán)從監(jiān)聽的端口上Accept連接,如果返回了一個(gè)net.Error并且這個(gè)錯(cuò)誤是臨時(shí)性的,則會sleep一個(gè)時(shí)間再繼續(xù)。 如果返回了其他錯(cuò)誤則會終止循環(huán)。成功Accept到一個(gè)連接后,調(diào)用了方法srv.newConn()對連接做了一層包裝,最后啟了一個(gè)goroutine處理http請求。
五、Golang 平滑升級(優(yōu)雅重啟)HTTP服務(wù)的實(shí)現(xiàn)
我創(chuàng)建了一個(gè)新的包gracehttp來實(shí)現(xiàn)支持平滑升級(優(yōu)雅重啟)的HTTP服務(wù),為了少寫代碼和降低使用成本,新的包盡可能多地利用net/http
包的實(shí)現(xiàn),并和net/http
包保持一致的對外方法?,F(xiàn)在開始我們來看gracehttp
包支持平滑升級 (優(yōu)雅重啟)Golang HTTP服務(wù)涉及到的細(xì)節(jié)如何實(shí)現(xiàn)。
1、Golang處理信號
Golang的os/signal
包封裝了對信號的處理。簡單用法請看示例:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
signalChan := make(chan os.Signal)
// 監(jiān)聽指定信號
signal.Notify(
signalChan,
syscall.SIGHUP,
syscall.SIGUSR2,
)
// 輸出當(dāng)前進(jìn)程的pid
fmt.Println("pid is: ", os.Getpid())
// 處理信號
for {
sig := <-signalChan
fmt.Println("get signal: ", sig)
}
}
2、子進(jìn)程啟動新程序,監(jiān)聽相同的端口
在第四部分的ListenAndServe()方法的實(shí)現(xiàn)代碼中可以看到,net/http包中使用net.Listen
函數(shù)來監(jiān)聽了某個(gè)端口,但如果某個(gè)運(yùn)行中的程序已經(jīng)監(jiān)聽某個(gè)端口,其他程序是無法再去監(jiān)聽這個(gè)端口的。解決的辦法是使用子進(jìn)程的方式啟動,并將監(jiān)聽端口的文件描述符傳遞給子進(jìn)程,子進(jìn)程里從這個(gè)文件描述符實(shí)現(xiàn)對端口的監(jiān)聽。
具體實(shí)現(xiàn)需要借助一個(gè)環(huán)境變量來區(qū)分進(jìn)程是正常啟動,還是以子進(jìn)程方式啟動的,相關(guān)代碼摘抄如下:
// 啟動子進(jìn)程執(zhí)行新程序
func (this *Server) startNewProcess() error {
listenerFd, err := this.listener.(*Listener).GetFd()
if err != nil {
return fmt.Errorf("failed to get socket file descriptor: %v", err)
}
path := os.Args[0]
// 設(shè)置標(biāo)識優(yōu)雅重啟的環(huán)境變量
environList := []string{}
for _, value := range os.Environ() {
if value != GRACEFUL_ENVIRON_STRING {
environList = append(environList, value)
}
}
environList = append(environList, GRACEFUL_ENVIRON_STRING)
execSpec := &syscall.ProcAttr{
Env: environList,
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), listenerFd},
}
fork, err := syscall.ForkExec(path, os.Args, execSpec)
if err != nil {
return fmt.Errorf("failed to forkexec: %v", err)
}
this.logf("start new process success, pid %d.", fork)
return nil
}
func (this *Server) getNetTCPListener(addr string) (*net.TCPListener, error) {
var ln net.Listener
var err error
if this.isGraceful {
file := os.NewFile(3, "")
ln, err = net.FileListener(file)
if err != nil {
err = fmt.Errorf("net.FileListener error: %v", err)
return nil, err
}
} else {
ln, err = net.Listen("tcp", addr)
if err != nil {
err = fmt.Errorf("net.Listen error: %v", err)
return nil, err
}
}
return ln.(*net.TCPListener), nil
}
3、父進(jìn)程等待已有連接中未完成的請求處理完畢
這一塊是最復(fù)雜的;首先我們需要一個(gè)計(jì)數(shù)器,在成功Accept一個(gè)連接時(shí),計(jì)數(shù)器加1,在連接關(guān)閉時(shí)計(jì)數(shù)減1,計(jì)數(shù)器為0時(shí)則父進(jìn)程可以正常退出了。Golang的sync的包里的WaitGroup可以很好地實(shí)現(xiàn)這個(gè)功能。
然后要控制連接的建立和關(guān)閉,我們需要深入到net/http包中Server結(jié)構(gòu)體的Serve()方法。重溫第四部分Serve()方法的實(shí)現(xiàn),會發(fā)現(xiàn)如果要重新寫一個(gè)Serve()方法幾乎是不可能的,因?yàn)檫@個(gè)方法里調(diào)用了好多個(gè)不可導(dǎo)出的內(nèi)部方法,重寫Serve()方法幾乎要重寫整個(gè)net/http
包。
幸運(yùn)的是,我們還發(fā)現(xiàn)在 ListenAndServe()方法里傳遞了一個(gè)listener給Serve()方法,并最終調(diào)用了這個(gè)listener的Accept()方法,這個(gè)方法返回了一個(gè)Conn的示例,最終在連接斷開的時(shí)候會調(diào)用Conn的Close()方法,這些結(jié)構(gòu)體和方法都是可導(dǎo)出的!
我們可以定義自己的Listener結(jié)構(gòu)體和Conn結(jié)構(gòu)體,組合net/http
包中對應(yīng)的結(jié)構(gòu)體,并重寫Accept()和Close()方法,實(shí)現(xiàn)對連接的計(jì)數(shù),相關(guān)代碼摘抄如下:
type Listener struct {
*net.TCPListener
waitGroup *sync.WaitGroup
}
func (this *Listener) Accept() (net.Conn, error) {
tc, err := this.AcceptTCP()
if err != nil {
return nil, err
}
tc.SetKeepAlive(true)
tc.SetKeepAlivePeriod(3 * time.Minute)
this.waitGroup.Add(1)
conn := &Connection{
Conn: tc,
listener: this,
}
return conn, nil
}
func (this *Listener) Wait() {
this.waitGroup.Wait()
}
type Connection struct {
net.Conn
listener *Listener
closed bool
}
func (this *Connection) Close() error {
if !this.closed {
this.closed = true
this.listener.waitGroup.Done()
}
return this.Conn.Close()
}
4、gracehttp包的用法
gracehttp包已經(jīng)應(yīng)用到每天幾億PV的項(xiàng)目中,也開源到了github上:github.com/tabalt/gracehttp,使用起來非常簡單。
如以下示例代碼,引入包后只需修改一個(gè)關(guān)鍵字,將http.ListenAndServe 改為 gracehttp.ListenAndServe即可。
package main
import (
"fmt"
"net/http"
"github.com/tabalt/gracehttp"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello world")
})
err := gracehttp.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println(err)
}
}
測試平滑升級(優(yōu)雅重啟)的效果,可以參考下面這個(gè)頁面的說明:
https://github.com/tabalt/gracehttp#demo
使用過程中有任何問題和建議,歡迎提交issue反饋,也可以Fork到自己名下修改之后提交pull request。
https://gocn.io/question/520
可以用golang自己實(shí)現(xiàn),但是只在類unix系統(tǒng)中有用,利用系統(tǒng)信號量和啟動子進(jìn)程,將舊的socket描述符傳遞給新的socket描述符,github已經(jīng)有不少這樣的庫,很多golang的http框架也實(shí)現(xiàn)了。這種實(shí)現(xiàn)叫“graceful restart”或者“zero downtime server”,實(shí)現(xiàn)不中斷服務(wù)更新。
具體參考可以看看這些項(xiàng)目和幾篇文章:
- 項(xiàng)目
facebook實(shí)現(xiàn)的https://github.com/facebookgo/grace
另外一個(gè)項(xiàng)目:https://github.com/tylerb/graceful
github還不少這樣的項(xiàng)目,可以自己找找。 - 文章
https://segmentfault.com/a/1190000004445975
http://tabalt.net/blog/graceful-http-server-for-golang
https://grisha.org/blog/2014/06/03/graceful-restart-in-golang/