优雅关闭
实现原理
等待进程处理完任务,关闭进程
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
优雅重启
优雅关闭可以防止程序强制终止,导致的脏数据问题。但是在关闭到重启的期间,有一个真空期,用户的请求是不会被接收到的。优雅重启可以解决这类问题
实现原理
- fork子进程
- 父进程优雅关闭
通过共享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
在分布式的环境下,有必要优雅重启吗? 虽然分布式环境可以避免用户请求的真空期,但是还是会可能产生脏数据。
有时候出现服务端执行完成,但是客户端读取response失败? 也许可以尝试:在服务端初始化http.Server的时候,可以把ReadTimeout,WriteTimeout时间调长一点
beego的grace正确使用姿势? 在配置文件中,Graceful设置为true,重启时使用kill -1 xxx来结束程序。另外第一次执行程序时不要加参数 -graceful=true,否则getListener会因为查找listener失败的,并且因为这个时候程序的ppid为1,是没有权限杀死的,beego这一块没有做错误处理。
os.FindProcess(os.Getppid()) 查出来的ppid等于1? 有两种可能,1.直接调用了-graceful参数;2.find的时候父进程已经结束,该进程会转移为1下面的子进程
getListener方法中,为什么根据 os.NewFile(uintptr(3), “”) 就能获取之前的socket文件? 有待研究