• <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年5月>
            30123456
            78910111213
            14151617181920
            21222324252627
            28293031123
            45678910


            專注即時(shí)通訊及網(wǎng)游服務(wù)端編程
            ------------------------------------
            Openresty 官方模塊
            Openresty 標(biāo)準(zhǔn)模塊(Opm)
            Openresty 三方模塊
            ------------------------------------
            本博收藏大部分文章為轉(zhuǎn)載,并在文章開頭給出了原文出處,如有再轉(zhuǎn),敬請保留相關(guān)信息,這是大家對原創(chuàng)作者勞動成果的自覺尊重?。∪鐬槟鷰聿槐悖堄诒静┫铝粞?,謝謝配合。

            常用鏈接

            留言簿(1)

            隨筆分類

            隨筆檔案

            相冊

            Awesome

            Blog

            Book

            GitHub

            Link

            搜索

            •  

            積分與排名

            • 積分 - 215511
            • 排名 - 118

            最新評論

            閱讀排行榜

            來自: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è)步驟:

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

            2. 通過pid給正在運(yùn)行的老進(jìn)程發(fā)送 特定的信號(kill -SIGUSR2 $pid)

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

            4. 老進(jìn)程不再接受新的請求,等待未完成的服務(wù)處理完畢,然后正常結(jié)束

            5. 新進(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ù),只需要簡單的三步:

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

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

            3. 在某個(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)目和幾篇文章:


            posted on 2016-12-26 14:47 思月行云 閱讀(511) 評論(0)  編輯 收藏 引用 所屬分類: Golang
            久久精品夜色噜噜亚洲A∨| 亚洲国产精品成人久久蜜臀| 91久久香蕉国产熟女线看| 国产成人久久精品一区二区三区| 久久精品国产精品亚洲人人| 久久综合久久综合九色| 欧美激情精品久久久久| 婷婷综合久久中文字幕| 99热都是精品久久久久久| 久久久久国产精品| 欧美亚洲国产精品久久蜜芽| 久久91精品国产91久久小草| 91视频国产91久久久| 日本精品久久久久中文字幕8| 99国产精品久久| 品成人欧美大片久久国产欧美| 久久精品国产99国产电影网 | 老男人久久青草av高清| 亚洲国产成人精品91久久久| 亚洲中文字幕伊人久久无码 | 国内精品久久久久影院薰衣草| 香蕉久久久久久狠狠色| 人妻久久久一区二区三区| 99久久99这里只有免费费精品| 2021精品国产综合久久| 国产视频久久| 久久精品国产乱子伦| 久久国产精品-国产精品| 久久中文精品无码中文字幕 | 亚洲精品tv久久久久久久久 | 久久精品无码一区二区WWW| 日韩AV无码久久一区二区 | 精品999久久久久久中文字幕 | 欧美与黑人午夜性猛交久久久| 亚洲精品乱码久久久久久蜜桃| 香蕉久久av一区二区三区| 久久综合九色综合久99| 一本一本久久aa综合精品| 国产 亚洲 欧美 另类 久久| 伊人久久综合成人网| 久久狠狠一本精品综合网|