• <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


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

            常用鏈接

            留言簿(1)

            隨筆分類

            隨筆檔案

            相冊(cè)

            Awesome

            Blog

            Book

            GitHub

            Link

            搜索

            •  

            積分與排名

            • 積分 - 215552
            • 排名 - 118

            最新評(píng)論

            閱讀排行榜

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

            前段時(shí)間用Golang在做一個(gè)HTTP的接口,因編譯型語言的特性,修改了代碼需要重新編譯可執(zhí)行文件,關(guān)閉正在運(yùn)行的老程序,并啟動(dòng)新程序。對(duì)于訪問量較大的面向用戶的產(chǎn)品,關(guān)閉、重啟的過程中勢(shì)必會(huì)出現(xiàn)無法訪問的情況,從而影響用戶體驗(yàn)。

            使用Golang的系統(tǒng)包開發(fā)HTTP服務(wù),是無法支持平滑升級(jí)(優(yōu)雅重啟)的,本文將探討如何解決該問題。

            一、平滑升級(jí)(優(yōu)雅重啟)的一般思路

            一般情況下,要實(shí)現(xiàn)平滑升級(jí),需要以下幾個(gè)步驟:

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

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

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

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

            5. 新進(jìn)程在父進(jìn)程退出后,會(huì)被init進(jìn)程領(lǐng)養(yǎng),并繼續(xù)提供服務(wù)

            二、Golang Socket 網(wǎng)絡(luò)編程

            Socket是程序員層面上對(duì)傳輸層協(xié)議TCP/IP的封裝和應(yīng)用。Golang中Socket相關(guān)的函數(shù)與結(jié)構(gòu)體定義在net包中,我們從一個(gè)簡(jiǎn)單的例子來學(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í)會(huì)阻塞,出錯(cuò)則跳出循環(huán)
                    conn, err := listener.Accept()
                    if err != nil {
                        fmt.Println(err)
                        break
                    }
            
                    fmt.Println("[server] accept new connection.")
            
                    // 啟動(dòng)一個(gè)goroutine 處理連接
                    go handler(conn)
                }
            }
            
            func handler(conn net.Conn) {
                defer conn.Close()
            
                for {
                    // 循環(huán)從連接中 讀取請(qǐng)求內(nèi)容,沒有請(qǐng)求時(shí)會(huì)阻塞,出錯(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
                    }
            
                    // 控制臺(tái)輸出讀取到的請(qǐng)求內(nèi)容,并在請(qǐng)求內(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í)會(huì)阻塞
                    response := make([]byte, 256)
                    readLength, err := conn.Read(response)
                    checkError(err)
            
                    // 將讀取響應(yīng)內(nèi)容輸出到控制臺(tá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)體。

            我們?cè)購(gòu)囊粋€(gè)簡(jiǎn)單的例子來學(xué)習(xí)一下Golang HTTP 編程,關(guān)鍵說明直接寫在注釋中。

            1、http服務(wù)程序 http.go

            package main
            
            import (
                "log"
                "net/http"
                "os"
            )
            
            // 定義http請(qǐng)求的處理方法
            func handlerHello(w http.ResponseWriter, r *http.Request) {
                w.Write([]byte("http hello on golang\n"))
            }
            
            func main() {
            
                // 注冊(cè)http請(qǐng)求的處理方法
                http.HandleFunc("/hello", handlerHello)
            
                // 在8086端口啟動(dòng)http服務(wù),會(huì)一直阻塞執(zhí)行
                err := http.ListenAndServe("localhost:8086", nil)
                if err != nil {
                    log.Println(err)
                }
            
                // http服務(wù)因故停止后 才會(huì)輸出如下內(nèi)容
                log.Println("Server on 8086 stopped")
                os.Exit(0)
            }

            2、運(yùn)行示例程序

            # 運(yùn)行HTTP服務(wù)程序
            go run http.go
            
            # 在另一個(gè)命令行窗口curl請(qǐng)求測(cè)試頁面
            curl http://localhost:8086/hello/
            
            # 輸出如下內(nèi)容:
            http hello on golang
            

            四、Golang net/http包中 Socket操作的實(shí)現(xiàn)

            從上面的簡(jiǎn)單示例中,我們看到在Golang中要啟動(dòng)一個(gè)http服務(wù),只需要簡(jiǎn)單的三步:

            1. 定義http請(qǐng)求的處理方法

            2. 注冊(cè)http請(qǐng)求的處理方法

            3. 在某個(gè)端口啟動(dòng)HTTP服務(wù)

            而最關(guān)鍵的啟動(dòng)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的對(duì)象,并調(diào)用它的ListenAndServe()方法,我們?cè)僬业浇Y(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í)性的,則會(huì)sleep一個(gè)時(shí)間再繼續(xù)。 如果返回了其他錯(cuò)誤則會(huì)終止循環(huán)。成功Accept到一個(gè)連接后,調(diào)用了方法srv.newConn()對(duì)連接做了一層包裝,最后啟了一個(gè)goroutine處理http請(qǐng)求。

            五、Golang 平滑升級(jí)(優(yōu)雅重啟)HTTP服務(wù)的實(shí)現(xiàn)

            我創(chuàng)建了一個(gè)新的包gracehttp來實(shí)現(xiàn)支持平滑升級(jí)(優(yōu)雅重啟)的HTTP服務(wù),為了少寫代碼和降低使用成本,新的包盡可能多地利用net/http包的實(shí)現(xiàn),并和net/http包保持一致的對(duì)外方法。現(xiàn)在開始我們來看gracehttp包支持平滑升級(jí) (優(yōu)雅重啟)Golang HTTP服務(wù)涉及到的細(xì)節(jié)如何實(shí)現(xiàn)。

            1、Golang處理信號(hào)

            Golang的os/signal包封裝了對(duì)信號(hào)的處理。簡(jiǎn)單用法請(qǐng)看示例:

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

            2、子進(jìn)程啟動(dòng)新程序,監(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)程的方式啟動(dòng),并將監(jiān)聽端口的文件描述符傳遞給子進(jìn)程,子進(jìn)程里從這個(gè)文件描述符實(shí)現(xiàn)對(duì)端口的監(jiān)聽。

            具體實(shí)現(xiàn)需要借助一個(gè)環(huán)境變量來區(qū)分進(jìn)程是正常啟動(dòng),還是以子進(jìn)程方式啟動(dòng)的,相關(guān)代碼摘抄如下:

            // 啟動(dòng)子進(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)識(shí)優(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)程等待已有連接中未完成的請(qǐng)求處理完畢

            這一塊是最復(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),會(huì)發(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í)候會(huì)調(diào)用Conn的Close()方法,這些結(jié)構(gòu)體和方法都是可導(dǎo)出的!

            我們可以定義自己的Listener結(jié)構(gòu)體和Conn結(jié)構(gòu)體,組合net/http包中對(duì)應(yīng)的結(jié)構(gòu)體,并重寫Accept()和Close()方法,實(shí)現(xiàn)對(duì)連接的計(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,使用起來非常簡(jiǎn)單。

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

            測(cè)試平滑升級(jí)(優(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)信號(hào)量和啟動(dòng)子進(jìn)程,將舊的socket描述符傳遞給新的socket描述符,github已經(jīng)有不少這樣的庫(kù),很多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) 評(píng)論(0)  編輯 收藏 引用 所屬分類: Golang
            久久久久久久尹人综合网亚洲| 日本免费久久久久久久网站 | 精品一区二区久久久久久久网站| 久久久久亚洲AV成人网| 久久精品国产99国产精偷| 欧美一区二区三区久久综合 | 久久www免费人成看片| 理论片午午伦夜理片久久| 国产精品欧美久久久久天天影视| 久久精品国产99国产电影网| 狠狠色丁香久久婷婷综合五月| 亚洲国产另类久久久精品黑人| 国产香蕉久久精品综合网| 亚洲精品午夜国产va久久| 久久影视综合亚洲| 久久亚洲精品无码VA大香大香| 国产巨作麻豆欧美亚洲综合久久| 精品久久人人爽天天玩人人妻| 91精品国产综合久久四虎久久无码一级| 99精品国产在热久久无毒不卡| 国产精品久久久久久久| 久久香蕉一级毛片| 精品99久久aaa一级毛片| 久久综合五月丁香久久激情| 久久亚洲AV成人无码| 久久人爽人人爽人人片AV| 久久精品中文闷骚内射| 久久99久久99小草精品免视看| 99久久www免费人成精品| 国产精品亚洲综合专区片高清久久久| 狠狠精品干练久久久无码中文字幕| 亚洲?V乱码久久精品蜜桃| 无码人妻精品一区二区三区久久久| 国产精品久久亚洲不卡动漫| 国产精品久久久久一区二区三区| 怡红院日本一道日本久久 | 久久无码AV中文出轨人妻| 伊人 久久 精品| 99久久成人国产精品免费| 欧美久久久久久精选9999| 伊人久久大香线蕉av一区|