首页 > golang > golang中如何真正平滑重启
2021
01-06

golang中如何真正平滑重启

背景

在业务快速增长中,前期只是验证模式是否可行,初期忽略程序发布重启带来的暂短停机影响。当模式实验成熟之后会逐渐放量,此时我们的发布停机带来的影响就会大很多。我们整个服务都是基于云,请求流量从 四层->七层->机器。

要想实现平滑重启大致有三种方案,一种是在流量调度的入口处理,一般的做法是 ApiGateway + CD ,发布的时候自动摘除机器,等待程序处理完现有请求再做发布处理,这样的好处就是程序不需要关心如何做平滑重启。

第二种就是程序自己完成平滑重启,保证在重启的时候 listen socket  FD(文件描述符) 依然可以接受请求进来,只不过切换新老进程,但是这个方案需要程序自己去完成,有些技术栈可能实现起来不是很简单,有些语言无法控制到操作系统级别,实现起来会很麻烦。

第三种方案就是完全 docker,所有的东西交给 k8s 统一管理,我们正在小规模接入中。


问题

    程序升级过程中,如何不影响正在处理的请求?

    正在处理的请求怎么办?

    新进来的请求怎么办?

正在处理的请求

    等待处理完成后退出

    go1.8就支持了,使用go1.8版本的shutdown方法进行优雅关闭

新来的请求怎么办?

    Fork一个子进程,继承父进程的监听socket

    子进程启动成功后,接收新的连接

    父进程停止接收新的连接,等已有的请求处理完毕,退出

    优雅重启成功

子进程如何继承父进程的文件句柄?(linux下)

    通过os.Cmd对象中的ExtraFiles参数进行传递

    文件句柄继承实例分析

package main

import (
   "flag"
   "fmt"
   "os"
   "os/exec"
   "time"
)

var (
   child *bool
)

func init() {
   child = flag.Bool("child", false, "继承于父进程(internal use only)")
   flag.Parse()
}

func readFromParent() {
   //fd = 0,标准输出
   //fd = 1,标准输入
   //fd = 2,标准错误输出
   //fd = 3, ==> ExtraFiles[0]
   //fd = 4, ==> ExtraFiles[1]

   //第一个参数文件句柄的下标,就是ExtraFiles[0], 第二个参数名字可以随便取
   f := os.NewFile(3, "")
   count := 0
   for {
      //格式化字符串
      str := fmt.Sprintf("hello, i'child process, write: %d line n", count)
      count++
      //写入到这个文件
      _, err := f.WriteString(str)
      if err != nil {
         fmt.Printf("write string failed, err: %vn", err)
         time.Sleep(time.Second)
         continue
      }
      //每一秒写下文件
      time.Sleep(time.Second)
   }
}

//启动子进程
func startChild(file *os.File) {
   args := []string{"-child"}
   //os.Args[0]是文件路径,带上-child选项
   cmd := exec.Command(os.Args[0], args...)
   cmd.Stdout = os.Stdout
   cmd.Stderr = os.Stderr
   //放socket fd在第一个entry,只要把父进程传递过来的放在这里
   cmd.ExtraFiles = []*os.File{file}
   //到main函数
   err := cmd.Start()

   if err != nil {
      fmt.Printf("start child failed, err: %vn", err.Error())
      return
   }
}

func main() {
   //表示已经是一个子进程了
   if child != nil && *child == true {
      fmt.Printf("继承于父进程的文件句柄n")
      //子进程
      readFromParent()
      return
   }

   //父进程的逻辑,打开文件句柄
   file, err := os.OpenFile("./test.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0755)
   if err != nil {
      fmt.Printf("open file failed, err:%vn", err)
      return
   }

   //启动一个子进程,把文件句柄给子进程
   startChild(file)
   fmt.Println("父进程退出")
}

golang 程序平滑重启框架

java、net 等基于虚拟机的语言不同,golang  天然支持系统级别的调用,平滑重启处理起来很容易。从原理上讲,基于 linux fork 子进程的方式,启动新的代码,再切换 listen socket FD,原理固然不难,但是完全自己实现还是会有很多细节问题的。好在有比较成熟的开源库帮我们实现了。


具体参考:https://phpmianshi.com/?id=2050


程序自身完成平滑重启


进程在不关闭其所监听端口的情况下进行重启,并且重启的整个过程保证所有请求都能被正确处理。

主要步骤:

  1. 原进程(父进程)先fork一个子进程,同时让fork出来的子进程继承父进程所监听的socket

  2. 子进程完成初始化后,开始接收socket的请求。

  3. 父进程停止接收新的请求,并将当下的请求处理完,等待连接空闲后,平滑退出。

信号(Signal)

服务的平滑重启,主要依赖进程接收的信号(实现进程间通信),这里简单的介绍Golang中信号的处理:

发送信号

    kill命令允许用户发送一个特定的信号给进程

    raise库函数可以发送特定的信号给当前进程

在Linux下运行man kill可以查看此命令的介绍和用法。

kill -- terminate or signal a process
The kill utility sends a signal to the processes specified by the pid operands.
Only the super-user may send signals to other users' processes.

常用信号类型

信号的默认行为:

    term:信号终止进程

    core:产生核心转储文件并退出

    ignore:忽略信号

    stop:信号停止进程

    cont:信号恢复一个已停止的进程

信号默认动作说明
SIGHUP1TermHUP (hang up):终端控制进程结束(终端连接断开)
SIGINT2TermINT (interrupt):用户发送INTR字符(Ctrl+C)触发(强制进程结束)
SIGQUIT3CoreQUIT (quit):用户发送QUIT字符(Ctrl+/)触发(进程结束)
SIGKILL9TermKILL (non-catchable, non-ignorable kill):无条件结束程序(不能被捕获、阻塞或忽略)
SIGUSR130,10,16Term用户自定义信号1
SIGUSR231,12,17Term用户自定义信号2
SIGKILL15KILL (non-catchable, non-ignorable kill)TERM (software termination signal):程序终止信号

信号接收测试

package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    sigs := make(chan os.Signal)
    signal.Notify(sigs, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)

    // 监听所有信号
    log.Println("listen sig")
    signal.Notify(sigs)

    // 打印进程id
    log.Println("PID:", os.Getppid())


    s := <-sigs
    log.Println("退出信号", s)
}
go run main.go

执行 ctrl+c

2022/04/24 20:05:42 listen sig
2022/04/24 20:05:42 PID: 17924
2022/04/24 20:05:45 退出信号 interrupt

实现案例

demo:

package main

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

var (
   server   *http.Server
   listener net.Listener
   graceful = flag.Bool("graceful", false, "listen on fd open 3 (internal use only)")
)

func sleep(w http.ResponseWriter, r *http.Request) {
   duration, err := time.ParseDuration(r.FormValue("duration"))
   if err != nil {
      http.Error(w, err.Error(), 400)
      return
   }
   time.Sleep(duration)
   w.Write([]byte("Hello World"))
}

func main() {
   flag.Parse()

   http.HandleFunc("/sleep", sleep)
   server = &http.Server{Addr: ":5007"}

   var err error
   if *graceful {
      log.Print("main: Listening to existing file descriptor 3.")
      // cmd.ExtraFiles: If non-nil, entry i becomes file descriptor 3+i.
      // when we put socket FD at the first entry, it will always be 3(0+3)
      f := os.NewFile(3, "")
      listener, err = net.FileListener(f)
   } else {
      log.Print("main: Listening on a new file descriptor.")
      listener, err = net.Listen("tcp", server.Addr)
   }

   if err != nil {
      log.Fatalf("listener error: %v", err)
   }

   go func() {
      // server.Shutdown() stops Serve() immediately, thus server.Serve() should not be in main goroutine
      err = server.Serve(listener)
      log.Printf("server.Serve err: %v\n", err)
   }()
   signalHandler()
   log.Printf("signal end")
}

func reload() error {
   tl, ok := listener.(*net.TCPListener)
   if !ok {
      return errors.New("listener is not tcp listener")
   }

   f, err := tl.File()
   if err != nil {
      return err
   }

   args := []string{"-graceful"}
   cmd := exec.Command(os.Args[0], args...)
   cmd.Stdout = os.Stdout
   cmd.Stderr = os.Stderr
   // put socket FD at the first entry
   cmd.ExtraFiles = []*os.File{f}
   return cmd.Start()
}

func signalHandler() {
   ch := make(chan os.Signal, 1)
   signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2)
   for {
      sig := <-ch
      log.Printf("signal: %v", sig)

      // timeout context for shutdown
      ctx, _ := context.WithTimeout(context.Background(), 100*time.Second)
      switch sig {
      case syscall.SIGINT, syscall.SIGTERM:
         // stop
         log.Printf("stop")
         signal.Stop(ch)
         server.Shutdown(ctx)
         log.Printf("graceful shutdown")
         return
      case syscall.SIGUSR2:
         // reload
         log.Printf("reload")
         err := reload()
         if err != nil {
            log.Fatalf("graceful restart error: %v", err)
         }
         server.Shutdown(ctx)
         log.Printf("graceful reload")
         return
      }
   }
}


本文》有 0 条评论

留下一个回复