• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            Fork me on GitHub
            隨筆 - 215  文章 - 13  trackbacks - 0
            <2017年1月>
            25262728293031
            1234567
            891011121314
            15161718192021
            22232425262728
            2930311234


            專注即時通訊及網游服務端編程
            ------------------------------------
            Openresty 官方模塊
            Openresty 標準模塊(Opm)
            Openresty 三方模塊
            ------------------------------------
            本博收藏大部分文章為轉載,并在文章開頭給出了原文出處,如有再轉,敬請保留相關信息,這是大家對原創作者勞動成果的自覺尊重!!如為您帶來不便,請于本博下留言,謝謝配合。

            常用鏈接

            留言簿(1)

            隨筆分類

            隨筆檔案

            相冊

            Awesome

            Blog

            Book

            GitHub

            Link

            搜索

            •  

            積分與排名

            • 積分 - 216760
            • 排名 - 118

            最新評論

            閱讀排行榜

            來自:https://segmentfault.com/a/1190000004445975
            原文鏈接:http://tabalt.net/blog/gracef...
            Golang支持平滑升級(優雅重啟)的包已開源到Github:https://github.com/tabalt/gracehttp,歡迎使用和貢獻代碼。

            前段時間用Golang在做一個HTTP的接口,因編譯型語言的特性,修改了代碼需要重新編譯可執行文件,關閉正在運行的老程序,并啟動新程序。對于訪問量較大的面向用戶的產品,關閉、重啟的過程中勢必會出現無法訪問的情況,從而影響用戶體驗。

            使用Golang的系統包開發HTTP服務,是無法支持平滑升級(優雅重啟)的,本文將探討如何解決該問題。

            一、平滑升級(優雅重啟)的一般思路

            一般情況下,要實現平滑升級,需要以下幾個步驟:

            1. 用新的可執行文件替換老的可執行文件(如只需優雅重啟,可以跳過這一步)

            2. 通過pid給正在運行的老進程發送 特定的信號(kill -SIGUSR2 $pid)

            3. 正在運行的老進程,接收到指定的信號后,以子進程的方式啟動新的可執行文件并開始處理新請求

            4. 老進程不再接受新的請求,等待未完成的服務處理完畢,然后正常結束

            5. 新進程在父進程退出后,會被init進程領養,并繼續提供服務

            二、Golang Socket 網絡編程

            Socket是程序員層面上對傳輸層協議TCP/IP的封裝和應用。Golang中Socket相關的函數與結構體定義在net包中,我們從一個簡單的例子來學習一下Golang Socket 網絡編程,關鍵說明直接寫在注釋中。

            1、服務端程序 server.go

            package main
            
            import (
                "fmt"
                "log"
                "net"
                "time"
            )
            
            func main() {
                // 監聽8086端口
                listener, err := net.Listen("tcp", ":8086")
                if err != nil {
                    log.Fatal(err)
                }
                defer listener.Close()
            
                for {
                    // 循環接收客戶端的連接,沒有連接時會阻塞,出錯則跳出循環
                    conn, err := listener.Accept()
                    if err != nil {
                        fmt.Println(err)
                        break
                    }
            
                    fmt.Println("[server] accept new connection.")
            
                    // 啟動一個goroutine 處理連接
                    go handler(conn)
                }
            }
            
            func handler(conn net.Conn) {
                defer conn.Close()
            
                for {
                    // 循環從連接中 讀取請求內容,沒有請求時會阻塞,出錯則跳出循環
                    request := make([]byte, 128)
                    readLength, err := conn.Read(request)
            
                    if err != nil {
                        fmt.Println(err)
                        break
                    }
            
                    if readLength == 0 {
                        fmt.Println(err)
                        break
                    }
            
                    // 控制臺輸出讀取到的請求內容,并在請求內容前加上hello和時間后向客戶端輸出
                    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() {
            
                // 從命令行中讀取第二個參數作為名字,如果不存在第二個參數則報錯退出
                if len(os.Args) != 2 {
                    fmt.Fprintf(os.Stderr, "Usage: %s name ", os.Args[0])
                    os.Exit(1)
                }
                name := os.Args[1]
            
                // 連接到服務端的8086端口
                conn, err := net.Dial("tcp", "127.0.0.1:8086")
                checkError(err)
            
                for {
                    // 循環往連接中 寫入名字
                    _, err = conn.Write([]byte(name))
                    checkError(err)
            
                    // 循環從連接中 讀取響應內容,沒有響應時會阻塞
                    response := make([]byte, 256)
                    readLength, err := conn.Read(response)
                    checkError(err)
            
                    // 將讀取響應內容輸出到控制臺,并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、運行示例程序

            # 運行服務端程序
            go run server.go
            
            # 在另一個命令行窗口運行客戶端程序
            go run client.go "tabalt"
            

            三、Golang HTTP 編程

            HTTP是基于傳輸層協議TCP/IP的應用層協議。Golang中HTTP相關的實現在net/http包中,直接用到了net包中Socket相關的函數和結構體。

            我們再從一個簡單的例子來學習一下Golang HTTP 編程,關鍵說明直接寫在注釋中。

            1、http服務程序 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服務,會一直阻塞執行
                err := http.ListenAndServe("localhost:8086", nil)
                if err != nil {
                    log.Println(err)
                }
            
                // http服務因故停止后 才會輸出如下內容
                log.Println("Server on 8086 stopped")
                os.Exit(0)
            }

            2、運行示例程序

            # 運行HTTP服務程序
            go run http.go
            
            # 在另一個命令行窗口curl請求測試頁面
            curl http://localhost:8086/hello/
            
            # 輸出如下內容:
            http hello on golang
            

            四、Golang net/http包中 Socket操作的實現

            從上面的簡單示例中,我們看到在Golang中要啟動一個http服務,只需要簡單的三步:

            1. 定義http請求的處理方法

            2. 注冊http請求的處理方法

            3. 在某個端口啟動HTTP服務

            而最關鍵的啟動http服務,是調用http.ListenAndServe()函數實現的。下面我們找到該函數的實現:

            func ListenAndServe(addr string, handler Handler) error {
                server := &Server{Addr: addr, Handler: handler}
                return server.ListenAndServe()
            }

            這里創建了一個Server的對象,并調用它的ListenAndServe()方法,我們再找到結構體Server的ListenAndServe()方法的實現:

            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)})
            }

            從代碼上看到,這里監聽了tcp端口,并將監聽者包裝成了一個結構體 tcpKeepAliveListener,再調用srv.Serve()方法;我們繼續跟蹤Serve()方法的實現:

            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編程的示例代碼一樣,循環從監聽的端口上Accept連接,如果返回了一個net.Error并且這個錯誤是臨時性的,則會sleep一個時間再繼續。 如果返回了其他錯誤則會終止循環。成功Accept到一個連接后,調用了方法srv.newConn()對連接做了一層包裝,最后啟了一個goroutine處理http請求。

            五、Golang 平滑升級(優雅重啟)HTTP服務的實現

            我創建了一個新的包gracehttp來實現支持平滑升級(優雅重啟)的HTTP服務,為了少寫代碼和降低使用成本,新的包盡可能多地利用net/http包的實現,并和net/http包保持一致的對外方法。現在開始我們來看gracehttp包支持平滑升級 (優雅重啟)Golang HTTP服務涉及到的細節如何實現。

            1、Golang處理信號

            Golang的os/signal包封裝了對信號的處理。簡單用法請看示例:

            package main
            
            import (
                "fmt"
                "os"
                "os/signal"
                "syscall"
            )
            
            func main() {
            
                signalChan := make(chan os.Signal)
            
                // 監聽指定信號
                signal.Notify(
                    signalChan,
                    syscall.SIGHUP,
                    syscall.SIGUSR2,
                )
            
                // 輸出當前進程的pid
                fmt.Println("pid is: ", os.Getpid())
            
                // 處理信號
                for {
                    sig := <-signalChan
                    fmt.Println("get signal: ", sig)
                }
            }

            2、子進程啟動新程序,監聽相同的端口

            在第四部分的ListenAndServe()方法的實現代碼中可以看到,net/http包中使用net.Listen函數來監聽了某個端口,但如果某個運行中的程序已經監聽某個端口,其他程序是無法再去監聽這個端口的。解決的辦法是使用子進程的方式啟動,并將監聽端口的文件描述符傳遞給子進程,子進程里從這個文件描述符實現對端口的監聽。

            具體實現需要借助一個環境變量來區分進程是正常啟動,還是以子進程方式啟動的,相關代碼摘抄如下:

            // 啟動子進程執行新程序
            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]
            
                // 設置標識優雅重啟的環境變量
                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、父進程等待已有連接中未完成的請求處理完畢

            這一塊是最復雜的;首先我們需要一個計數器,在成功Accept一個連接時,計數器加1,在連接關閉時計數減1,計數器為0時則父進程可以正常退出了。Golang的sync的包里的WaitGroup可以很好地實現這個功能。

            然后要控制連接的建立和關閉,我們需要深入到net/http包中Server結構體的Serve()方法。重溫第四部分Serve()方法的實現,會發現如果要重新寫一個Serve()方法幾乎是不可能的,因為這個方法里調用了好多個不可導出的內部方法,重寫Serve()方法幾乎要重寫整個net/http包。

            幸運的是,我們還發現在 ListenAndServe()方法里傳遞了一個listener給Serve()方法,并最終調用了這個listener的Accept()方法,這個方法返回了一個Conn的示例,最終在連接斷開的時候會調用Conn的Close()方法,這些結構體和方法都是可導出的!

            我們可以定義自己的Listener結構體和Conn結構體,組合net/http包中對應的結構體,并重寫Accept()和Close()方法,實現對連接的計數,相關代碼摘抄如下:

            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包已經應用到每天幾億PV的項目中,也開源到了github上:github.com/tabalt/gracehttp,使用起來非常簡單。

            如以下示例代碼,引入包后只需修改一個關鍵字,將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)
                }
            }

            測試平滑升級(優雅重啟)的效果,可以參考下面這個頁面的說明:

            https://github.com/tabalt/gracehttp#demo

            使用過程中有任何問題和建議,歡迎提交issue反饋,也可以Fork到自己名下修改之后提交pull request。

            https://gocn.io/question/520
            可以用golang自己實現,但是只在類unix系統中有用,利用系統信號量和啟動子進程,將舊的socket描述符傳遞給新的socket描述符,github已經有不少這樣的庫,很多golang的http框架也實現了。這種實現叫“graceful restart”或者“zero downtime server”,實現不中斷服務更新。
            具體參考可以看看這些項目和幾篇文章:


            posted on 2016-12-26 14:47 思月行云 閱讀(512) 評論(0)  編輯 收藏 引用 所屬分類: Golang
            久久亚洲私人国产精品vA| 久久久久99这里有精品10| 久久亚洲欧美国产精品| 久久综合噜噜激激的五月天| 久久亚洲美女精品国产精品| 中文字幕亚洲综合久久| 久久综合给合综合久久| 亚洲色大成网站WWW久久九九| 99国产欧美久久久精品蜜芽| 久久e热在这里只有国产中文精品99| 青青草国产97免久久费观看| 久久精品国产第一区二区三区| 国产高潮久久免费观看| 亚洲中文字幕无码久久2020| 伊人久久大香线蕉影院95| 国产精品99久久久精品无码| 久久精品国产精品国产精品污| 中文字幕亚洲综合久久菠萝蜜 | 国内精品伊人久久久久| 久久久久久久免费视频| 久久国产精品二国产精品 | 久久99亚洲综合精品首页| 亚洲精品乱码久久久久久中文字幕 | 亚洲性久久久影院| 国产精品免费久久久久久久久| 精品国产乱码久久久久软件| 久久精品成人一区二区三区| 777米奇久久最新地址| 日本人妻丰满熟妇久久久久久| 亚洲国产精品无码久久九九| 99久久精品免费观看国产| 好属妞这里只有精品久久| 久久不见久久见免费视频7| 天堂久久天堂AV色综合| 亚洲女久久久噜噜噜熟女| 综合久久国产九一剧情麻豆| 久久丫忘忧草产品| 久久精品免费一区二区| 亚洲午夜久久久影院| 久久99精品国产自在现线小黄鸭| 久久亚洲精品人成综合网|