序言
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)題:
- 編譯好的應(yīng)用二進(jìn)制文件、配置文件上傳到服務(wù)器上;
- 應(yīng)用服務(wù)器能感知到有新的版本上傳;
- 在沒(méi)有停止服務(wù)的情況下,熱更新版本;
- 最好所有的更新過(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)的編譯和重啟,例如 gin和 fresh,這些應(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)
}
}
}
}
}