golang 熱更新技巧

序言

Golang標(biāo)準(zhǔn)庫(kù)的http部分提供了強(qiáng)大的web應(yīng)用支持,再加上negroni等中間件框架的支持,可以開(kāi)發(fā)高性能的web應(yīng)用(如提供Restful的api服務(wù)等)。
通常這些web應(yīng)用部署在多臺(tái)Linux操作系統(tǒng)的應(yīng)用服務(wù)器上,并用Nginx等做為反向代理,實(shí)現(xiàn)高可用的集群服務(wù)。當(dāng)應(yīng)用版本升級(jí)時(shí),如何實(shí)現(xiàn)比較優(yōu)雅的多態(tài)服務(wù)器的版本更新呢?

問(wèn)題分析

Web應(yīng)用的更新,我覺(jué)得可能需要考慮幾個(gè)方面的問(wèn)題:

  1. 編譯好的應(yīng)用二進(jìn)制文件、配置文件上傳到服務(wù)器上;
  2. 應(yīng)用服務(wù)器能感知到有新的版本上傳;
  3. 在沒(méi)有停止服務(wù)的情況下,熱更新版本;
  4. 最好所有的更新過(guò)程,可以腳本化,減少手動(dòng)操作的錯(cuò)誤。

方案

其實(shí),go社區(qū)有一些開(kāi)源項(xiàng)目,可以自動(dòng)檢測(cè)web應(yīng)用的改變,并實(shí)現(xiàn)自動(dòng)的更新,但這些應(yīng)用都是檢測(cè)源碼、資源文件的更新,啟動(dòng)build過(guò)程,實(shí)現(xiàn)自動(dòng)的編譯和重啟,例如 ginfresh,這些應(yīng)用適合應(yīng)用于開(kāi)發(fā)和測(cè)試階段,可能并不適合應(yīng)用的部署和更新,但提供了良好的思路。

部署環(huán)境的目錄及版本的上傳
我將發(fā)布的應(yīng)用二進(jìn)制文件和配置文件,存放在某個(gè)目錄下,如 ~/app/release,每個(gè)版本都保留在這個(gè)目錄中,例如 app.1.0、app.1.1、app.2.0,一旦發(fā)現(xiàn)有問(wèn)題,可以及時(shí)的回滾。
同時(shí),在~/app目錄下,利用軟鏈接文件,指向到最新版本,如

ln -s ~/app/release/app.2.0 ~/app/app.bin

此外,利用一個(gè)保存在 ~/app/release 下的文本文件,來(lái)指明當(dāng)前應(yīng)用的版本,如current.conf:

{
    "bin.file": "~/app/release/app.2.0",
    "cfg.file": "~/app/release/cfg.2.0"
}

當(dāng)需要更新服務(wù)器的版本時(shí),可以通過(guò)腳本調(diào)用scp,將新版本上傳到release目錄下,然后更新current.conf文件。
監(jiān)控current.conf文件,獲知版本更新
current.conf文件中是當(dāng)前的版本,一旦這個(gè)文件發(fā)生變化,即表示有版本需要更新(或者回滾),我們只需要監(jiān)控這個(gè)文件的變化,一旦發(fā)生變化,則做相應(yīng)的處理。文件的監(jiān)控,可以通過(guò) fsnotify來(lái)實(shí)現(xiàn)。

func watch() {
    watcher, err := fsnotify.NewWatcher()
    if err != nil {
        logger.Fatal(err)
    }
    defer watcher.Close()

    go func() {
        for {
                select {
                case event := <-watcher.Events:
                    logger.Println("event:", event)
                    if event.Op&fsnotify.Write == fsnotify.Write {
                        logger.Println("notify runner to do the ln -s and restart server.")
                        restartChan <- true
                    }
                case err := <-watcher.Errors:
                    logger.Println("error:", err)
            }
        }
    }()

    err = watcher.Add("/path/to/current.conf")
    if err != nil {
        logger.Fatal(err)
    }

    <- make(chan bool)
}

重啟服務(wù)
監(jiān)控到current.conf文件的變化后,接下來(lái)就是重啟服務(wù)。
為了讓服務(wù)不中斷,優(yōu)雅的進(jìn)行重啟,可以利用 endless 來(lái)替換標(biāo)準(zhǔn)庫(kù)net/http的ListenAndServe:

 n := negroni.New()
    n.Use(middleware.NewRecovery())
    n.Use(middleware.NewMaintainMiddleware())
    n.Use(middleware.NewLogMiddleware())
    n.Use(middleware.NewStatic(http.Dir("static")))
    n.UseHandler(router.NewRouter())

    log.Fatal(endless.ListenAndServe(":3000", n))

在current.conf變更后,首先將~/app下的軟鏈接文件指向最新版本,然后利用

kill -HUP

通知應(yīng)用重啟。

func run() {
    for {
        <- restartChan

        c, err := ioutil.ReadFile("/path/to/current.conf")
        if err != nil {
            logger.Println("current.conf read error:", err)
            return
        }

        var j interface{}
        err = json.Unmarshal(c, &j)
        if err != nil {
            logger.Println("current.conf parse error:", err)
            return
        }

        parsed, ok := j.(map[string]interface{})
        if !ok {
            logger.Println("current.conf parse error: mapping errors")
            return
        }

        exec.Command("rm", "app.bin").Run()
        exec.Command("ln", "-s", parsed["bin.file"].(string), "app.bin").Run()

        exec.Command("rm", "app.conf").Run()
        exec.Command("ln", "-s", parsed["cfg.file"].(string), "app.cfg").Run()

        if !started {
            cmd := exec.Command("./app.bin", "-c", "app.cfg")
            started = true
        } else {
            processes, _ := ps.Processes()
            for _, v := range processes {
                if strings.Contains(v.Executable(), parsed["bin.file"]) {
                    process, _ := os.FindProcess(v.Pid())
                    process.Signal(syscall.SIGHUP)
                }
            }
        }
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容