當線上代碼有更新時,咱們要首先關閉服務,而後再啓動服務,若是訪問量比較大,當關閉服務的時候,當前服務器頗有可能有不少 鏈接,那麼若是此時直接關閉服務,這些鏈接將所有斷掉,影響用戶體驗,絕對稱不上優雅laravel
因此咱們要想出一種能夠平滑關閉或者重啓程序的方式git
是謂優雅。github
以 siluser/bingo框架爲例golang
關於這個框架的系列文章:docker
我使用了tim1020/godaemon這個包來實現平滑重啓的功能(對於大部分項目來講,直接使用能夠知足大部分需求,無需改造)shell
指望效果:windows
在控制檯輸入 bingo run daemon [start|restart|stop]
能夠令服務器 啓動|重啓|中止
數組
bingo run dev
)關於 bingo
命令的實現能夠看我之前的博客: 仿照laravel-artisan實現簡易go開發腳手架服務器
由於是開發環境嘛,大致的思路就是吧 bingo run
命令轉換成令 go run start.go
這種 shell
命令app
因此 bingo run dev
就等於 go run start.go dev
//處理http.Server,使支持graceful stop/restart
func Graceful(s http.Server) error {
// 設置一個環境變量
os.Setenv("__GRACEFUL", "true")
// 建立一個自定義的server
srv = &server{
cm: newConnectionManager(),
Server: s,
}
// 設置server的狀態
srv.ConnState = func(conn net.Conn, state http.ConnState) {
switch state {
case http.StateNew:
srv.cm.add(1)
case http.StateActive:
srv.cm.rmIdleConns(conn.LocalAddr().String())
case http.StateIdle:
srv.cm.addIdleConns(conn.LocalAddr().String(), conn)
case http.StateHijacked, http.StateClosed:
srv.cm.done()
}
}
l, err := srv.getListener()
if err == nil {
err = srv.Server.Serve(l)
} else {
fmt.Println(err)
}
return err
}
複製代碼
這樣就能夠啓動一個服務器,而且在鏈接狀態變化的時候能夠監聽到
當使用 bingo run daemon
或者 bingo run daemon start
的時候,會觸發 DaemonInit()
函數,內容以下:
func DaemonInit() {
// 獲得存放pid文件的路徑
dir, _ := os.Getwd()
pidFile = dir + "/" + Env.Get("PID_FILE")
if os.Getenv("__Daemon") != "true" { //master
cmd := "start" //缺省爲start
if l := len(os.Args); l > 2 {
cmd = os.Args[l-1]
}
switch cmd {
case "start":
if isRunning() {
fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+strconv.Itoa(pidVal)+"] Bingo is running", 0x1B)
} else { //fork daemon進程
if err := forkDaemon(); err != nil {
fmt.Println(err)
}
}
case "restart": //重啓:
if !isRunning() {
fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "[Warning]bingo not running", 0x1B)
restart(pidVal)
} else {
fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+strconv.Itoa(pidVal)+"] Bingo restart now", 0x1B)
restart(pidVal)
}
case "stop": //中止
if !isRunning() {
fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "[Warning]bingo not running", 0x1B)
} else {
syscall.Kill(pidVal, syscall.SIGTERM) //kill
}
case "-h":
fmt.Println("Usage: " + appName + " start|restart|stop")
default: //其它不識別的參數
return //返回至調用方
}
//主進程退出
os.Exit(0)
}
go handleSignals()
}
複製代碼
首先要獲取pidFile
這個文件主要是存儲令程序運行時候的進程pid
,爲何要持久化pid
呢?是爲了讓屢次程序運行過程當中,斷定是否有相同程序啓動等操做
以後要獲取對應的操做 (start|restart|stop),一個一個說
case
start
:
首先使用 isRunning()
方法判斷當前程序是否在運行,如何判斷?就是從上面提到的 pidFile
中取出進程號
而後判斷當前系統是否運行令這個進程,若是有,證實正在運行,返回 true
,反之返回 false
若是沒有運行的話,調用 forkDaemon()
函數啓動程序,這個函數是整個功能的核心
func forkDaemon() error {
args := os.Args
os.Setenv("__Daemon", "true")
procAttr := &syscall.ProcAttr{
Env: os.Environ(),
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
}
pid, err := syscall.ForkExec(args[0], []string{args[0], "dev"}, procAttr)
if err != nil {
panic(err)
}
savePid(pid)
fmt.Printf("\n %c[0;48;32m%s%c[0m", 0x1B, "["+strconv.Itoa(pid)+"] Bingo running...", 0x1B)
fmt.Println()
return nil
}
複製代碼
syscall
包不支持win系統,也就意味着若是想在 windows
上作開發的話,只能使用虛擬機或者 docker
啦
這裏的主要功能就是,使用 syscall.ForkExec()
,fork
一個進程出來
運行這個進程所執行的命令就是這裏的參數(由於咱們的原始命令是 go run start.go dev
,因此這裏的args[0]
其實是 start.go
編譯以後的二進制文件)
而後再把 fork
出來的進程號保存在 pidFile
裏
因此最終運行的效果就是咱們第一步時候說到的 bingo run dev
達到的效果
case
restart
:
這個比較簡單,經過 pidFile
斷定程序是否正在運行,若是正在運行,纔會繼續向下執行
函數體也比較簡單,只有兩行
syscall.Kill(pid, syscall.SIGHUP) //kill -HUP, daemon only時,會直接退出
forkDaemon()
複製代碼
第一行殺死這個進程 第二行開啓一個新進程
case
stop
:
這裏就一行代碼,就是殺死這個進程
在開發過程當中,每當有一丁點變更(好比更改來一丁點控制器),就須要再次執行一次 bingo run daemon restart
命令,讓新的改動生效,十分麻煩
因此我又開發了 bingo run watch
命令,監聽改動,自動重啓server服務器
我使用了github.com/fsnotify/fs…包來實現監聽
func startWatchServer(port string, handler http.Handler) {
// 監聽目錄變化,若是有變化,重啓服務
// 守護進程開啓服務,主進程阻塞不斷掃描當前目錄,有任何更新,向守護進程傳遞信號,守護進程重啓服務
// 開啓一個協程運行服務
// 監聽目錄變化,有變化運行 bingo run daemon restart
f, err := fsnotify.NewWatcher()
if err != nil {
panic(err)
}
defer f.Close()
dir, _ := os.Getwd()
wdDir = dir
fileWatcher = f
f.Add(dir)
done := make(chan bool)
go func() {
procAttr := &syscall.ProcAttr{
Env: os.Environ(),
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
}
_, err := syscall.ForkExec(os.Args[0], []string{os.Args[0], "daemon", "start"}, procAttr)
if err != nil {
fmt.Println(err)
}
}()
go func() {
for {
select {
case ev := <-f.Events:
if ev.Op&fsnotify.Create == fsnotify.Create {
fmt.Printf("\n %c[0;48;33m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]created file:"+ev.Name, 0x1B)
}
if ev.Op&fsnotify.Remove == fsnotify.Remove {
fmt.Printf("\n %c[0;48;31m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]deleted file:"+ev.Name, 0x1B)
}
if ev.Op&fsnotify.Rename == fsnotify.Rename {
fmt.Printf("\n %c[0;48;34m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]renamed file:"+ev.Name, 0x1B)
} else {
fmt.Printf("\n %c[0;48;32m%s%c[0m", 0x1B, "["+time.Now().Format("2006-01-02 15:04:05")+"]modified file:"+ev.Name, 0x1B)
}
// 有變化,放入重啓數組中
restartSlice = append(restartSlice, 1)
case err := <-f.Errors:
fmt.Println("error:", err)
}
}
}()
// 準備重啓守護進程
go restartDaemonServer()
<-done
}
複製代碼
首先按照 fsnotify
的文檔,建立一個 watcher
,而後添加監聽目錄(這裏只是監聽目錄下的文件,不能監聽子目錄)
而後開啓兩個協程:
監聽文件變化,若是有文件變化,把變化的個數寫入一個 slice
裏,這是一個阻塞的 for
循環
每隔1s中查看一次記錄文件變化的 slice
, 若是有的話,就重啓服務器,並從新設置監聽目錄,而後清空 slice
,不然跳過
遞歸遍歷子目錄,達到監聽整個工程目錄的效果:
func listeningWatcherDir(dir string) {
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
dir, _ := os.Getwd()
pidFile = dir + "/" + Env.Get("PID_FILE")
fileWatcher.Add(path)
// 這裏不能監聽 pidFile,不然每次重啓都會致使pidFile有更新,會不斷的觸發重啓功能
fileWatcher.Remove(pidFile)
return nil
})
}
複製代碼
這裏這個 slice
的做用也就是爲了不當一次保存更新了多個文件的時候,也重啓了屢次服務器
下面看看重啓服務器的代碼:
go func() {
// 執行重啓命令
cmd := exec.Command("bingo", "run", "daemon", "restart")
stdout, err := cmd.StdoutPipe()
if err != nil {
fmt.Println(err)
}
defer stdout.Close()
if err := cmd.Start(); err != nil {
panic(err)
}
reader := bufio.NewReader(stdout)
//實時循環讀取輸出流中的一行內容
for {
line, err2 := reader.ReadString('\n')
if err2 != nil || io.EOF == err2 {
break
}
fmt.Print(line)
}
if err := cmd.Wait(); err != nil {
fmt.Println(err)
}
opBytes, _ := ioutil.ReadAll(stdout)
fmt.Print(string(opBytes))
}()
複製代碼
使用 exec.Command()
方法獲得一個 cmd
調用 cmd.Stdoutput()
獲得一個輸出管道,命令打印出來的數據都會從這個管道流出來
而後使用 reader := bufio.NewReader(stdout)
從管道中讀出數據
用一個阻塞的for
循環,不斷的從管道中讀出數據,以 \n
爲一行,一行一行的讀
並打印在控制檯裏,達到輸出的效果,若是這幾行不寫的話,在新的進程裏的 fmt.Println()
方法打印出來的數據將沒法顯示在控制檯上.
就醬,最後貼下項目連接 silsuer/bingo ,歡迎star,歡迎PR,歡迎提意見