当线上代码有更新时,咱们要首先关闭服务,而后再启动服务,若是访问量比较大,当关闭服务的时候,当前服务器颇有可能有不少 链接,那么若是此时直接关闭服务,这些链接将所有断掉,影响用户体验,绝对称不上优雅laravel
因此咱们要想出一种能够平滑关闭或者重启程序的方式git
是谓优雅。github
思路
- 服务端启动时多开启一个协程用来监听关闭信号
- 当协程接收到关闭信号时,将拒绝接收新的链接,并处理好当前全部链接后断开
- 启动一个新的服务端进程来接管新的链接
- 关闭当前进程
实现
以 siluser/bingo框架为例golang
关于这个框架的系列文章:docker
我使用了tim1020/godaemon这个包来实现平滑重启的功能(对于大部分项目来讲,直接使用能够知足大部分需求,无需改造)shell
指望效果:windows
bingo run daemon [start|restart|stop]启动|重启|中止
bingo run dev
bingo
bingo rungo run start.goshell
bingo run devgo 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 daemonbingo run daemon startDaemonInit()
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() } 复制代码
pidFilepidpid
以后要获取对应的操做 (start|restart|stop),一个一个说
start
isRunning()pidFile
truefalse
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 } 复制代码
syscallwindowsdocker
syscall.ForkExec()fork
go run start.go devargs[0]start.go
forkpidFile
bingo run dev
restart
pidFile
函数体也比较简单,只有两行
syscall.Kill(pid, syscall.SIGHUP) //kill -HUP, daemon only时,会直接退出 forkDaemon() 复制代码
第一行杀死这个进程 第二行开启一个新进程
stop
这里就一行代码,就是杀死这个进程
额外的想法
bingo run daemon restart
bingo run watch
我使用了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 } 复制代码
fsnotifywatcher
而后开启两个协程:
sliceforsliceslice
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,欢迎提意见