优雅关闭的几种实现

优雅关闭

实现原理

等待进程处理完任务,关闭进程

go1.8之后标准包实现优雅关闭

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(5 * time.Second)
        fmt.Fprintf(w, "Hello World, %v\n", time.Now())
        fmt.Println("hello:", time.Now())
    })

    s := &http.Server{
        Addr:           ":8080",
        Handler:        http.DefaultServeMux,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }

    go func() {
        log.Println(s.ListenAndServe())
        log.Println("server shutdown")
    }()

    // Handle SIGINT and SIGTERM.
    ch := make(chan os.Signal)
    signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
    log.Println("ch:", <-ch)

    // Stop the service gracefully.
    ctx := context.Background()
    log.Println("shut:", s.Shutdown(ctx))

    log.Println("done.")
}

通过waitgroup实现

参考beego.graceful.shutdown

优雅重启

优雅关闭可以防止程序强制终止,导致的脏数据问题。但是在关闭到重启的期间,有一个真空期,用户的请求是不会被接收到的。优雅重启可以解决这类问题

实现原理

  1. fork子进程
  2. 父进程优雅关闭

通过共享listener,即socket文件,fork子进程

func forkAndRun(ln net.Listener) {
    l := ln.(*net.TCPListener)
    newFile, _ := l.File()

    cmd := exec.Command(os.Args[0], "-graceful")
    cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
    cmd.ExtraFiles = []*os.File{newFile}
    fmt.Printf("cmd:%#v", cmd)
    cmd.Run()
}

fork子进程的时候,获取ppid,关闭父进程

if graceful {
    process, err := os.FindProcess(os.Getppid())
    fmt.Println("ppid:", os.Getppid())
    if err != nil {
        log.Println(err)
        return err
    }
    err = process.Signal(syscall.SIGTERM)
    if err != nil {
        return err
    }
}

160行代码实现一个graceful server

package mygrace

import (
    "context"
    "flag"
    "fmt"
    "log"
    "net"
    "net/http"
    "os"
    "os/exec"
    "os/signal"
    "syscall"
    "time"
)

var (
    graceful bool
)

func init() {
    // 第一次启动时不要添加graceful,否则会报错
    flag.BoolVar(&graceful, "graceful", false, "is graceful")
}

type Server struct {
    ln net.Listener
    *http.Server
    Done chan bool
}

func NewServer(addr string, handler http.Handler) (srv *Server) {
    if !flag.Parsed() {
        flag.Parse()
    }
    srv = &Server{Done: make(chan bool)}
    hsrv := &http.Server{
        Addr:           addr,
        Handler:        handler,
        ReadTimeout:    10 * time.Second, // 值如果过小,会导致client端读取resp失败
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    srv.Server = hsrv
    return
}

// 这里addr可以不用传,因为下面
func ListenAndServe(addr string, handler http.Handler) error {
    s := NewServer(addr, handler)
    err := s.ListenAndServe()
    return err
}

func (srv *Server) ListenAndServe() error {
    // 这里获取listener
    // 如果是第一次启动,是从net.Listen获取
    // 其余情况,是根据文件描述符获取
    ln, err := srv.getListener()
    if err != nil {
        log.Println("srv.getListener():", err)
        return err
    }
    srv.ln = ln

    if graceful {
        process, err := os.FindProcess(os.Getppid())
        log.Println("ppid:", os.Getppid())
        if err != nil {
            log.Println(err)
            return err
        }
        err = process.Signal(syscall.SIGTERM)
        if err != nil {
            return err
        }
    }
    log.Println(os.Getpid(), srv.Addr)
    go srv.Serve(srv.ln)
    go srv.handleSignals()
    <-srv.Done
    log.Println("srv done!!!")
    return nil
}

func (srv *Server) shutdown() error {
    ctx := context.Background()
    err := srv.Shutdown(ctx)
    srv.Done <- true
    return err
}

func (srv *Server) fork() error {

    tl := srv.ln.(*net.TCPListener)
    file, err := tl.File()
    if err != nil {
        log.Println("ln.File() err:", err)
        return err
    }

    cmd := exec.Command(os.Args[0], "-graceful")
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.Stdin = os.Stdin
    cmd.ExtraFiles = []*os.File{file}
    err = cmd.Start()
    if err != nil {
        log.Fatalf("Restart: Failed to launch, error: %v", err)
    }
    return nil
}

func (srv *Server) handleSignals() {
    for {
        sig := make(chan os.Signal, 1)
        signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)

        sigMsg := <-sig
        switch sigMsg {
        case syscall.SIGHUP:
            log.Println("receive syscall.SIGHUP")
            err := srv.fork()
            if err != nil {
                log.Println("srv.fork() err:", err)
            }
        case syscall.SIGINT:
            log.Println("receive syscall.SIGINT")
            err := srv.shutdown()
            if err != nil {
                log.Println("srv.fork() err:", err)
            }
            return
        case syscall.SIGTERM:
            log.Println("receive syscall.SIGTERM")
            err := srv.shutdown()
            if err != nil {
                log.Println("srv.fork() err:", err)
            }
            return
        }
    }
    return
}

func (srv *Server) getListener() (l net.Listener, err error) {
    if graceful {
        f := os.NewFile(uintptr(3), "")
        l, err = net.FileListener(f)
        if err != nil {
            err = fmt.Errorf("net.FileListener error: %v", err)
            return
        }
    } else {
        l, err = net.Listen("tcp", srv.Addr)
        if err != nil {
            err = fmt.Errorf("net.Listen error: %v", err)
            return
        }
    }
    return
}

Q&A

  1. 在分布式的环境下,有必要优雅重启吗? 虽然分布式环境可以避免用户请求的真空期,但是还是会可能产生脏数据。

  2. 有时候出现服务端执行完成,但是客户端读取response失败? 也许可以尝试:在服务端初始化http.Server的时候,可以把ReadTimeout,WriteTimeout时间调长一点

  3. beego的grace正确使用姿势? 在配置文件中,Graceful设置为true,重启时使用kill -1 xxx来结束程序。另外第一次执行程序时不要加参数 -graceful=true,否则getListener会因为查找listener失败的,并且因为这个时候程序的ppid为1,是没有权限杀死的,beego这一块没有做错误处理。

  4. os.FindProcess(os.Getppid()) 查出来的ppid等于1? 有两种可能,1.直接调用了-graceful参数;2.find的时候父进程已经结束,该进程会转移为1下面的子进程

  5. getListener方法中,为什么根据 os.NewFile(uintptr(3), “”) 就能获取之前的socket文件? 有待研究

相关链接

Golang实现平滑重启(优雅重启)

beego的graceful实现