优雅的停止

不中断正常的请求,等所有请求处理完才停止服务
如果你使用的是 Go 1.8,你可能不需要使用这些库!考虑使用 http.Server 内置的 Shutdown() 方法正常关闭。查看 Gin 中完整的 graceful-shutdown 示例。

  1. package main
  2. import (
  3. "context"
  4. "github.com/gin-gonic/gin"
  5. "log"
  6. "net/http"
  7. "os"
  8. "os/signal"
  9. "time"
  10. )
  11. func main() {
  12. router := gin.Default()
  13. router.GET("/", func(c *gin.Context) {
  14. time.Sleep(5 * time.Second)
  15. c.String(http.StatusOK, "Welcome Gin Server")
  16. //c.String(http.StatusOK, "Welcome Gin Server")
  17. })
  18. router.NoMethod()
  19. srv := &http.Server{
  20. Addr: ":8080",
  21. Handler: router,
  22. }
  23. go func() {
  24. // 连接服务器
  25. if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
  26. log.Fatalf("listen: %s\n", err)
  27. }
  28. }()
  29. // 等待信号处理完正常关闭服务
  30. quit := make(chan os.Signal)
  31. signal.Notify(quit, os.Interrupt)
  32. <-quit
  33. log.Println("Shutdown Server ...")
  34. // 在这里也可以设定超时,就算有存活的链接也强制关闭服务
  35. if err := srv.Shutdown(context.Background()); err != nil {
  36. log.Fatal("Server Shutdown:", err)
  37. }
  38. log.Println("Server exiting")
  39. }

Shutdown源码如下:
它会定期的(500毫秒),检查当前连接状态,如果连接全部处于空闲状态,那么将停止服务

  1. func (srv *Server) Shutdown(ctx context.Context) error {
  2. atomic.StoreInt32(&srv.inShutdown, 1)
  3. srv.mu.Lock()
  4. lnerr := srv.closeListenersLocked()
  5. srv.closeDoneChanLocked()
  6. for _, f := range srv.onShutdown {
  7. go f()
  8. }
  9. srv.mu.Unlock()
  10. ticker := time.NewTicker(shutdownPollInterval) // 这里是500毫秒
  11. defer ticker.Stop()
  12. for {
  13. if srv.closeIdleConns() {
  14. return lnerr
  15. }
  16. select {
  17. case <-ctx.Done():
  18. return ctx.Err()
  19. case <-ticker.C:
  20. }
  21. }
  22. }

热更新

你想正常的重启或停止你的 web 服务器吗?
有一些方法可以做到。
我们能使用 fvbock/endless 去替换默认的 ListenAndServe. 参考 issue #296 了解更多细节。

  1. router := gin.Default()
  2. router.GET("/", handler)
  3. // [...]
  4. endless.ListenAndServe(":4242", router)

另外一些替代方案:

  • manners : 一个有礼貌的 Go HTTP 服务器,它可以正常的关闭。
  • graceful : Graceful 是一个 Go 包,它可以正常的关闭一个 http.Handler 服务器。
  • grace : 正常的重启 & Go 服务器零停机部署。

endless使用与原理

文档:https://godoc.org/github.com/fvbock/endless

  1. var (
  2. DefaultReadTimeOut time.Duration
  3. DefaultWriteTimeOut time.Duration
  4. DefaultMaxHeaderBytes int
  5. DefaultHammerTime time.Duration
  6. )

func ListenAndServe(addr string, handler http.Handler) error
func ListenAndServeTLS(addr string, certFile string, keyFile string, handler http.Handler) error
func NewServer(addr string, handler http.Handler) (srv *endlessServer)

  1. package main
  2. import (
  3. "github.com/fvbock/endless"
  4. "github.com/gin-gonic/gin"
  5. "log"
  6. "net/http"
  7. "time"
  8. )
  9. func main() {
  10. router := gin.Default()
  11. router.GET("/", func(c *gin.Context) {
  12. time.Sleep(10 * time.Second)
  13. c.String(http.StatusOK, "111111111")
  14. // c.String(http.StatusOK, "222222222")
  15. })
  16. endless.DefaultReadTimeOut = readTimeout
  17. endless.DefaultWriteTimeOut = writeTimeout
  18. endless.DefaultMaxHeaderBytes = maxHeaderBytes
  19. s := endless.NewServer(":8080", router)
  20. err := s.ListenAndServe()
  21. if err != nil {
  22. log.Printf("server err: %v", err)
  23. }
  24. }
  1. 开启服务,删除可执行的文件
  2. 访问 等10s 得到111111111
  3. 将返回改为222222222,编译——上传——-赋予执行权限
  4. 访问 ps到服务的进程 xxx kill -l xxx 手速快点,10s内完成,依然得到111111111
  5. 访问,等10s222222222

image.png

  • Received SIGHUP. forking. : 接收到kill -l xxx的信号
  • Waiting for connections to finish… :父进程等待连接处理完成
  • [::]:8080 Listener closed.:当前的服务已关闭,这里的关闭是准备准备的意思,如果当前的服务还没处理完,例如处理A时又来了个B,最后返回时A与B都会返回111111111
  • Serve() returning… :此时表示旧服务中的请求都已处理完,新的服务已经运行起来
  • accept tcp [::]:8080: use of closed network connection : 提示新的请求将进入新程序

其使用非常简单,实现代码也很少,但是很强大,下面我们看看实现:
kill -1
endless的使用方法是先编译新程序,并执行”kill -1 旧进程id”,我们看看旧程序接收到-1信号之后作了什么:

  1. func (srv *endlessServer) handleSignals() {
  2. ...
  3. for {
  4. sig = <-srv.sigChan
  5. srv.signalHooks(PRE_SIGNAL, sig)
  6. switch sig {
  7. case syscall.SIGHUP: //接收到-1信号之后,fork一个进程,并运行新编译的程序
  8. log.Println(pid, "Received SIGHUP. forking.")
  9. err := srv.fork()
  10. if err != nil {
  11. log.Println("Fork err:", err)
  12. }
  13. ...
  14. default:
  15. log.Printf("Received %v: nothing i care about...\n", sig)
  16. }
  17. srv.signalHooks(POST_SIGNAL, sig)
  18. }
  19. }
  20. func (srv *endlessServer) fork() (err error) {
  21. ...
  22. path := os.Args[0] //获取当前程序的路径,在子进程执行。所以要保证新编译的程序路径和旧程序的一致。
  23. var args []string
  24. if len(os.Args) > 1 {
  25. args = os.Args[1:]
  26. }
  27. cmd := exec.Command(path, args...)
  28. cmd.Stdout = os.Stdout
  29. cmd.Stderr = os.Stderr
  30. //socket在此处传给子进程,windows系统不支持获取socket文件,所以endless无法在windows上用。
  31. //windows获取socket文件时报错:file tcp [::]:9999: not supported by windows。
  32. cmd.ExtraFiles = files
  33. cmd.Env = env //env有一个ENDLESS_SOCKET_ORDER变量存储了socket传递的顺序(如果有多个socket)
  34. ...
  35. err = cmd.Start() //运行新程序
  36. if err != nil {
  37. log.Fatalf("Restart: Failed to launch, error: %v", err)
  38. }
  39. return
  40. }

接下来我们看看程序启动之后做了什么

ListenAndServe**
新进程启动之后会执行ListenAndServe这个方法,这个方法主要做了系统信号监听,并且判断自己所在进程是否是子进程,如果是,则发送中断信号给父进程,让其退出。最后调用Serve方法给socket提供新的服务。

  1. func (srv *endlessServer) ListenAndServe() (err error) {
  2. ...
  3. go srv.handleSignals()
  4. l, err := srv.getListener(addr)
  5. if err != nil {
  6. log.Println(err)
  7. return
  8. }
  9. srv.EndlessListener = newEndlessListener(l, srv)
  10. if srv.isChild {
  11. syscall.Kill(syscall.Getppid(), syscall.SIGTERM) //给父进程发出中断信号
  12. }
  13. ...
  14. return srv.Serve() //为socket提供新的服务
  15. }


复用socket**
前面提到复用socket是endless的核心,必须在Serve前准备好,否则会导致端口已使用的异常。复用socket的实现在上面的getListener方法中:

  1. func (srv *endlessServer) getListener(laddr string) (l net.Listener, err error) {
  2. if srv.isChild {//如果此方法运行在子进程中,则复用socket
  3. var ptrOffset uint = 0
  4. runningServerReg.RLock()
  5. defer runningServerReg.RUnlock()
  6. if len(socketPtrOffsetMap) > 0 {
  7. ptrOffset = socketPtrOffsetMap[laddr]//获取和addr相对应的socket的位置
  8. }
  9. f := os.NewFile(uintptr(3+ptrOffset), "")//创建socket文件描述符
  10. l, err = net.FileListener(f)//创建socket文件监听器
  11. if err != nil {
  12. err = fmt.Errorf("net.FileListener error: %v", err)
  13. return
  14. }
  15. } else {//如果此方法不是运行在子进程中,则新建一个socket
  16. l, err = net.Listen("tcp", laddr)
  17. if err != nil {
  18. err = fmt.Errorf("net.Listen error: %v", err)
  19. return
  20. }
  21. }
  22. return
  23. }

但是父进程关闭socket和子进程绑定socket并不可能同时进行,如果这段时间有请求进来,这个请求会到哪里去呢?

  • 第一种情况:如果某个终端跟服务器建立了长连接(应该是设置了keepalive属性),那么该终端的所有请求都会发到建立长连接的进程去,即父进程
  • 第二种情况:如果有新的请求进来,当父进程没结束(与新服务启动是对立的),会被分配的父进程
  • 第三种情况,父进程或者子进程任意一个退出之后,所有请求都会转发到另一个进程进行处理

从以上三种情况看,endless的做法不会落下任何请求,因为请求不是被父进程处理了就是被子进程处理了,所以endless是个可放心使用的热更新方案