Go 实现热重启的详细介绍

最近在优化公司框架 trpc 时发现了一个热重启相关的问题,优化之余也总结沉淀下,对 go 如何实现热重启这方面的内容做一个简单的梳理。

1.什么是热重启?

热重启(Hot Restart),是一项保证服务可用性的手段。它允许服务重启期间,不中断已经建立的连接,老服务进程不再接受新连接请求,新连接请求将在新服务进程中受理。对于原服务进程中已经建立的连接,也可以将其设为读关闭,等待平滑处理完连接上的请求及连接空闲后再行退出。通过这种方式,可以保证已建立的连接不中断,连接上的事务(请求、处理、响应)可以正常完成,新的服务进程也可以正常接受连接、处理连接上的请求。当然,热重启期间进程平滑退出涉及到的不止是连接上的事务,也有消息服务、自定义事务需要关注。

这是我理解的热重启的一个大致描述。热重启现在还有没有存在的必要?我的理解是看场景。

以后台开发为例,假如运维平台有能力在服务升级、重启时自动踢掉流量,服务就绪后又自动加回流量,假如能够合理预估服务 QPS、请求处理时长,那么只要配置一个合理的停止前等待时间,是可以达到类似热重启的效果的。这样的话,在后台服务里面支持热重启就显得没什么必要。但是,如果我们开发一个微服务框架,不能对将来的部署平台、环境做这种假设,也有可能使用方只是部署在一两台物理机上,也没有其他的负载均衡设施,但不希望因为重启受干扰,热重启就很有必要。当然还有一些更复杂、要求更苛刻的场景,也需要热重启的能力。

热重启是比较重要的一项保证服务质量的手段,还是值得了解下的,这也是本文介绍的初衷。

2.如何实现热重启?

如何实现热重启,这里其实不能一概而论,要结合实际的场景来看(比如服务编程模型、对可用性要求的高低等)。大致的实现思路,可以先抛一下。

一般要实现热重启,大致要包括如下步骤:

  • 首先,要让老进程,这里称之为父进程了,先要 fork 出一个子进程来代替它工作;
  • 然后,子进程就绪之后,通知父进程,正常接受新连接请求、处理连接上收到的请求;
  • 再然后,父进程处理完已建立连接上的请求后、连接空闲后,平滑退出。

听上去是挺简单的...

2.1.认识 fork

大家都知道fork()系统调用,父进程调用 fork 会创建一个进程副本,代码中还可以通过 fork 返回值是否为 0 来区分是子进程还是父进程。

int main(char **argv, int argc) {
 pid_t pid = fork();
 if (pid == 0) {
 printf("i am child process");
 } else {
 printf("i am parent process, i have a child process named %d", pid);
 }
}

可能有些开发人员不知道 fork 的实现原理,或者不知道 fork 返回值为什么在父子进程中不同,或者不知道如何做到父子进程中返回值不同……了解这些是要有点知识积累的。

2.2.返回值

简单概括下,ABI 定义了进行函数调用时的一些规范,如何传递参数,如何返回值等等,以 x86 为例,如果返回值是 rax 寄存器能够容的一般都是通过 rax 寄存器返回的。

如果 rax 寄存器位宽无法容纳下的返回值呢?也简单,编译器会安插些指令来完成这些神秘的操作,具体是什么指令,就跟语言编译器实现相关了。

  • c 语言,可能会将返回值的地址,传递到 rdi 或其他寄存器,被调函数内部呢,通过多条指令将返回值写入 rdi 代指的内存区;
  • c 语言,也可能在被调函数内部,用多个寄存器 rax,rdx...一起暂存返回结果,函数返回时再将多个寄存器的值赋值到变量中;
  • 也可能会像 golang 这样,通过栈内存来返回;

2.3.fork 返回值

fork 系统调用的返回值,有点特殊,在父进程和子进程中,这个函数返回的值是不同的,如何做到的呢?

联想下父进程调用 fork 的时候,操作系统内核需要干些什么呢?分配进程控制块、分配 pid、分配内存空间……肯定有很多东西啦,这里注意下进程的硬件上下文信息,这些是非常重要的,在进程被调度算法选中进行调度时,是需要还原硬件上下文信息的。

Linux fork 的时候,会对子进程的硬件上下文进行一定的修改,我就是让你 fork 之后拿到的 pid 是 0,怎么办呢?前面 2.2 节提过了,对于那些小整数,rax 寄存器存下绰绰有余,fork 返回时就是将操作系统分配的 pid 放到 rax 寄存器的。

那,对于子进程而言,我只要在 fork 的时候将它的硬件上下文 rax 寄存器清 0,然后等其他设置全 ok 后,再将其状态从不可中断等待状态修改为可运行状态,等其被调度器调度时,会先还原其硬件上下文信息,包括 PC、rax 等等,这样 fork 返回后,rax 中值为 0,最终赋值给 pid 的值就是 0。

因此,也就可以通过这种判断 “pid 是否等于 0” 的方式来区分当前进程是父进程还是子进程了。

2.4.局限性

很多人清楚 fork 可以创建一个进程的副本并继续往下执行,可以根据 fork 返回值来执行不同的分支逻辑。如果进程是多线程的,在一个线程中调用 fork 会复制整个进程吗?

fork 只能创建调用该函数的线程的副本,进程中其他运行的线程,fork 不予处理。这就意味着,对于多线程程序而言,寄希望于通过 fork 来创建一个完整进程副本是不可行的。

前面我们也提到了,fork 是实现热重启的重要一环,fork 这里的这个局限性,就制约着不同服务编程模型下的热重启实现方式。所以我们说具体问题具体分析,不同编程模型下实际上可以采用不同的实现方式。

3.单进程单线程模型

单进程单线程模型,可能很多人一听觉得它已经被淘汰了,生产环境中不能用,真的么?强如 redis,不就是单线程。强调下并非单线程模型没用,ok,收回来,现在关注下单进程单线程模型如何实现热重启。

单进程单线程,实现热重启会比较简单些:

  • fork 一下就可以创建出子进程,
  • 子进程可以继承父进程中的资源,如已经打开的文件描述符,包括父进程的 listenfd、connfd,
  • 父进程,可以选择关闭 listenfd,后续接受连接的任务就交给子进程来完成了,
  • 父进程,甚至也可以关闭 connfd,让子进程处理连接上的请求、回包等,也可以自身处理完已建立的连接上的请求;
  • 父进程,在合适的时间点选择退出,子进程开始变成顶梁柱。

核心思想就是这些,但是具体到实现,就有多种方法:

  • 可以选择 fork 的方式让子进程拿到原来的 listenfd、connfd,
  • 也可以选择 unixdomain socket 的方式父进程将 listenfd、connfd 发送给子进程。

有同学可能会想,我不传递这些 fd 行吗?

  • 比如我开启了 reuseport,父进程直接处理完已建立连接 connfd 上的请求之后关闭,子进程里 reuseport.Listen 直接创建新的 listenfd。

也可以!但是有些问题必须要提前考虑到:

  • reuseport 虽然允许多个进程在同一个端口上多次 listen,似乎满足了要求,但是要知道只要 euid 相同,都可以在这个端口上 listen!是不安全的!
  • reuseport 实现和平台有关系,在 Linux 平台上在同一个 address+port 上 listen 多次,多个 listenfd 底层可以共享同一个连接队列,内核可以实现负载均衡,但是在 darwin 平台上却不会!

当然这里提到的这些问题,在多线程模型下肯定也存在。

4.单进程多线程模型

前面提到的问题,在多线程模型中也会出现:

  • fork 只能复制 calling thread,not whole process!
  • reuseport 多次在相同地址+端口 listen 得到的多个 fd,不同平台有不同的表现,可能无法做到接受连接时的 load banlance!
  • 非 reuseport 情况下,多次 listen 会失败!
  • 不传递 fd,直接通过 reuseport 来重新 listen 得到 listenfd,不安全,不同服务进程实例可能会在同一个端口上监听,gg!
  • 父进程平滑退出的逻辑,关闭 listenfd,等待 connfd 上请求处理结束,关闭 connfd,一切妥当后,父进程退出,子进程挑大梁!

5. 其他线程模型

其他线程都基本上避不开上述 3、4 的实现或者组合,对应问题相仿,不再赘述。

6. go 实现热重启:触发时机

需要选择一个时机来触发热重启,什么时候触发呢?操作系统提供了信号机制,允许进程做出一些自定义的信号处理。

杀死一个进程,一般会通过kill -9发送 SIGKILL 信号给进程,这个信号不允许捕获,SIGABORT 也不允许捕获,这样可以允许进程所有者或者高权限用户控制进程生死,达到更好的管理效果。

kill 也可以用来发送其他信号给进程,如发送 SIGUSR1、SIGUSR2、SIGINT 等等,进程中可以接收这些信号,并针对性的做出处理。这里可以选择 SIGUSR1 或者 SIGUSR2 来通知进程热重启。

go func() {
 ch := make(chan os.Signal, 1)
 signal.Notify(ch, os.SIGUSR2)
 <- ch

 //接下来就可以做热重启相关的逻辑了
 ...
}()

7. 如何判断热重启

那一个 go 程序重新启动之后,所有运行时状态信息都是新的,那如何区分自己是否是子进程呢,或者说我是否要执行热重启逻辑呢?父进程可以通过设置子进程初始化时的环境变量,比如加个 HOT_RESTART=1。

这就要求代码中在合适的地方要先检测环境变量 HOT_RESTART 是否为 1,如果成立,那就执行热重启逻辑,否则就执行全新的启动逻辑。

8. ForkExec

假如当前进程收到 SIGUSR2 信号之后,希望执行热重启逻辑,那么好,需要先执行 syscall.ForkExec(...)来创建一个子进程,注意 go 不同于 cc++,它本身就是依赖多线程来调度协程的,天然就是多线程程序,只不过是他没有使用 NPTL 线程库来创建,而是通过 clone 系统调用来创建。

前面提过了,如果单纯 fork 的话,只能复制调用 fork 函数的线程,对于进程中的其他线程无能为力,所以对于 go 这种天然的多线程程序,必须从头来一遍,再 exec 一下。所以 go 标准库提供的函数是 syscall.ForkExec 而不是 syscall.Fork。

9. go 实现热重启: 传递 listenfd

go 里面传递 fd 的方式,有这么几种,父进程 fork 子进程的时候传递 fd,或者后面通过 unix domain socket 传递。需要注意的是,我们传递的实际上是 file description,而非 file descriptor。

附上一张类 unix 系统下 file descriptor、file description、inode 三者之间的关系图:

fd 分配都是从小到大分配的,父进程中的 fd 为 10,传递到子进程中之后有可能就不是 10。那么传递到子进程的 fd 是否是可以预测的呢?可以预测,但是不建议。所以我提供了两种实现方式。

9.1 ForkExec+ProcAttr{Files: []uintptr{}}

要传递一个 listenfd 很简单,假如是类型 net.Listener,那就通过tcpln := ln.(*net.TCPListener); file, _ := tcpln.File(); fd := file.FD()来拿到 listener 底层 file description 对应的 fd。

需要注意的是,这里的 fd 并非底层的 file description 对应的初始 fd,而是被 dup2 复制出来的一个 fd(调用 tcpln.File()的时候就已经分配了),这样底层 file description 引用计数就会+1。如果后面想通过 ln.Close()关闭监听套接字的话,sorry,关不掉。这里需要显示的执行 file.Close() 将新创建的 fd 关掉,使对应的 file description 引用计数-1,保证 Close 的时候引用计数为 0,才可以正常关闭。

试想下,我们想实现热重启,是一定要等连接上接收的请求处理完才可以退出进程的,但是这期间父进程不能再接收新的连接请求,如果这里不能正常关闭 listener,那我们这个目标就无法实现。所以这里对 dup 出来的 fd 的处理要慎重些,不要遗忘。

OK,接下来说下 syscall.ProcAttr{Files: []uintptr{}},这里就是要传递的父进程中的 fd,比如要传递 stdin、stdout、stderr 给子进程,就需要将这几个对应的 fd 塞进去 os.Stdin.FD(), os.Stdout.FD(), os.Stderr.FD(),如果要想传递刚才的 listenfd,就需要将上面的file.FD()返回的 fd 塞进去。

子进程中接收到这些 fd 之后,在类 unix 系统下一般会按照从 0、1、2、3 这样递增的顺序来分配 fd,那么传递过去的 fd 是可以预测的,假如除了 stdin, stdout, stderr 再传两个 listenfd,那么可以预测这两个的 fd 应该是 3,4。在类 unix 系统下一般都是这么处理的,子进程中就可以根据传递 fd 的数量(比如通过环境变量传递给子进程 FD_NUM=2),来从 3 开始计算,哦,这两个 fd 应该是 3,4。

父子进程可以通过一个约定的顺序,来组织传递的 listenfd 的顺序,以方便子进程中按相同的约定进行处理,当然也可以通过 fd 重建 listener 之后来判断对应的监听 network+address,以区分该 listener 对应的是哪一个逻辑 service。都是可以的!

需要注意的是,file.FD()返回的 fd 是非阻塞的,会影响到底层的 file description,在重建 listener 先将其设为 nonblock, syscall.SetNonBlock(fd),然后file, _ := os.NewFile(fd); tcplistener := net.FileListener(file),或者是udpconn := net.PacketConn(file),然后可以获取 tcplistener、udpconn 的监听地址,来关联其对应的逻辑 service。

前面提到 file.FD()会将底层的 file description 设置为阻塞模式,这里再补充下,net.FileListener(f), net.PacketConn(f)内部会调用 newFileFd()->dupSocket(),这几个函数内部会将 fd 对应的 file description 重新设置为非阻塞。父子进程中共享了 listener 对应的 file description,所以不需要显示设置为非阻塞。

有些微服务框架是支持对服务进行逻辑 service 分组的,google pb 规范中也支持多 service 定义,这个在腾讯的 goneat、trpc 框架中也是有支持的。

当然了,这里我不会写一个完整的包含上述所有描述的 demo 给大家,这有点占篇幅,这里只贴一个精简版的实例,其他的读者感兴趣可以自己编码测试。须知纸上得来终觉浅,还是要多实践。

package main

import (
 "fmt"
 "io/ioutil"
 "log"
 "net"
 "os"
 "strconv"
 "sync"
 "syscall"
 "time"
)

const envRestart = "RESTART"
const envListenFD = "LISTENFD"

func main() {

 v := os.Getenv(envRestart)

 if v != "1" {

 ln, err := net.Listen("tcp", "localhost:8888")
 if err != nil {
 panic(err)
 }

 wg := sync.WaitGroup{}
 wg.Add(1)
 go func() {
 defer wg.Done()
 for {
 ln.Accept()
 }
 }()

 tcpln := ln.(*net.TCPListener)
 f, err := tcpln.File()
 if err != nil {
 panic(err)
 }

 os.Setenv(envRestart, "1")
 os.Setenv(envListenFD, fmt.Sprintf("%d", f.Fd()))

 _, err = syscall.ForkExec(os.Args[0], os.Args, &syscall.ProcAttr{
 Env: os.Environ(),
 Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), f.Fd()},
 Sys: nil,
 })
 if err != nil {
 panic(err)
 }
 log.Print("parent pid:", os.Getpid(), ", pass fd:", f.Fd())
 f.Close()
 wg.Wait()

 } else {

 v := os.Getenv(envListenFD)
 fd, err := strconv.ParseInt(v, 10, 64)
 if err != nil {
 panic(err)
 }
 log.Print("child pid:", os.Getpid(), ", recv fd:", fd)

 // case1: 理解上面提及的file descriptor、file description的关系
 // 这里子进程继承了父进程中传递过来的一些fd,但是fd数值与父进程中可能是不同的
 // 取消注释来测试...
 //ff := os.NewFile(uintptr(fd), "")
 //if ff != nil {
 // _, err := ff.Stat()
 // if err != nil {
 // log.Println(err)
 // }
 //}

 // case2: 假定父进程中共享了fd 0\1\2\listenfd给子进程,那再子进程中可以预测到listenfd=3
 ff := os.NewFile(uintptr(3), "")
 fmt.Println("fd:", ff.Fd())
 if ff != nil {
 _, err := ff.Stat()
 if err != nil {
 panic(err)
 }

 // 这里pause, 运行命令lsof -P -p $pid,检查下有没有listenfd传过来,除了0,1,2,应该有看到3
 // ctrl+d to continue
 ioutil.ReadAll(os.Stdin)

 fmt.Println("....")
 _, err = net.FileListener(ff)
 if err != nil {
 panic(err)
 }

 // 这里pause, 运行命令lsof -P -p $pid, 会发现有两个listenfd,
 // 因为前面调用了ff.FD() dup2了一个,如果这里不显示关闭,listener将无法关闭
 ff.Close()

 time.Sleep(time.Minute)
 }

 time.Sleep(time.Minute)
 }
}

这里用简单的代码大致解释了如何用 ProcAttr 来传递 listenfd。这里有个问题,假如后续父进程中传递的 fd 修改了呢,比如不传 stdin, stdout, stderr 的 fd 了,怎么办?服务端是不是要开始预测应该从 0 开始编号了?我们可以通过环境变量通知子进程,比如传递的 fd 从哪个编号开始是 listenfd,一共有几个 listenfd,这样也是可以实现的。

这种实现方式可以跨平台。

感兴趣的话,可以看下 facebook 提供的这个实现grace。

9.2 unix domain socket + cmsg

另一种,思路就是通过 unix domain socket + cmsg 来传递,父进程启动的时候依然是通过 ForkExec 来创建子进程,但是并不通过 ProcAttr 来传递 listenfd。

父进程在创建子进程之前,创建一个 unix domain socket 并监听,等子进程启动之后,建立到这个 unix domain socket 的连接,父进程此时开始将 listenfd 通过 cmsg 发送给子进程,获取 fd 的方式与 9.1 相同,该注意的 fd 关闭问题也是一样的处理。

子进程连接上 unix domain socket,开始接收 cmsg,内核帮子进程收消息的时候,发现里面有一个父进程的 fd,内核找到对应的 file description,并为子进程分配一个 fd,将两者建立起映射关系。然后回到子进程中的时候,子进程拿到的就是对应该 file description 的 fd 了。通过 os.NewFile(fd)就可以拿到 file,然后再通过 net.FileListener 或者 net.PacketConn 就可以拿到 tcplistener 或者 udpconn。

剩下的获取监听地址,关联逻辑 service 的动作,就与 9.1 小结描述的一致了。

这里我也提供一个可运行的精简版的 demo,供大家了解、测试用。

package main

import (
 "fmt"
 "io/ioutil"
 "log"
 "net"
 "os"
 "strconv"
 "sync"
 "syscall"
 "time"

 passfd "github.com/ftrvxmtrx/fd"
)

const envRestart = "RESTART"
const envListenFD = "LISTENFD"
const unixsockname = "/tmp/xxxxxxxxxxxxxxxxx.sock"

func main() {

 v := os.Getenv(envRestart)

 if v != "1" {

 ln, err := net.Listen("tcp", "localhost:8888")
 if err != nil {
 panic(err)
 }

 wg := sync.WaitGroup{}
 wg.Add(1)
 go func() {
 defer wg.Done()
 for {
 ln.Accept()
 }
 }()

 tcpln := ln.(*net.TCPListener)
 f, err := tcpln.File()
 if err != nil {
 panic(err)
 }

 os.Setenv(envRestart, "1")
 os.Setenv(envListenFD, fmt.Sprintf("%d", f.Fd()))

 _, err = syscall.ForkExec(os.Args[0], os.Args, &syscall.ProcAttr{
 Env: os.Environ(),
 Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), /*f.Fd()*/}, // comment this when test unixsock
 Sys: nil,
 })
 if err != nil {
 panic(err)
 }
 log.Print("parent pid:", os.Getpid(), ", pass fd:", f.Fd())

 os.Remove(unixsockname)
 unix, err := net.Listen("unix", unixsockname)
 if err != nil {
 panic(err)
 }
 unixconn, err := unix.Accept()
 if err != nil {
 panic(err)
 }
 err = passfd.Put(unixconn.(*net.UnixConn), f)
 if err != nil {
 panic(err)
 }

 f.Close()
 wg.Wait()

 } else {

 v := os.Getenv(envListenFD)
 fd, err := strconv.ParseInt(v, 10, 64)
 if err != nil {
 panic(err)
 }
 log.Print("child pid:", os.Getpid(), ", recv fd:", fd)

 // case1: 有同学认为以通过环境变量传fd,通过环境变量肯定是不行的,fd根本不对应子进程中的fd
 //ff := os.NewFile(uintptr(fd), "")
 //if ff != nil {
 // _, err := ff.Stat()
 // if err != nil {
 // log.Println(err)
 // }
 //}

 // case2: 如果只有一个listenfd的情况下,那如果fork子进程时保证只传0\1\2\listenfd,那子进程中listenfd一定是3
 //ff := os.NewFile(uintptr(3), "")
 //if ff != nil {
 // _, err := ff.Stat()
 // if err != nil {
 // panic(err)
 // }
 // // pause, ctrl+d to continue
 // ioutil.ReadAll(os.Stdin)
 // fmt.Println("....")
 // _, err = net.FileListener(ff) //会dup一个fd出来,有多个listener
 // if err != nil {
 // panic(err)
 // }
 // // lsof -P -p $pid, 会发现有两个listenfd
 // time.Sleep(time.Minute)
 //}
 // 这里我们暂停下,方便运行系统命令来查看进程当前的一些状态
 // run: lsof -P -p $pid,检查下listenfd情况

 ioutil.ReadAll(os.Stdin)
 fmt.Println(".....")

 unixconn, err := net.Dial("unix", unixsockname)
 if err != nil {
 panic(err)
 }

 files, err := passfd.Get(unixconn.(*net.UnixConn), 1, nil)
 if err != nil {
 panic(err)
 }

 // 这里再运行命令:lsof -P -p $pid再检查下listenfd情况

 f := files[0]
 f.Stat()

 time.Sleep(time.Minute)
 }
}

这种实现方式,仅限类 unix 系统。

如果有服务混布的情况存在,需要考虑下使用的 unix domain socket 的文件名,避免因为重名所引起的问题,可以考虑通过”进程名.pid“来作为 unix domain socket 的名字,并通过环境变量将其传递给子进程。

10. go 实现热重启: 子进程如何通过 listenfd 重建 listener

前面已经提过了,当拿到 fd 之后还不知道它对应的是 tcp 的 listener,还是 udpconn,那怎么办?都试下呗。

file, err := os.NewFile(fd)
// check error

tcpln, err := net.FileListener(file)
// check error

udpconn, err := net.PacketConn(file)
// check error

11. go 实现热重启:父进程平滑退出

父进程如何平滑退出呢,这个要看父进程中都有哪些逻辑要平滑停止了。

11.1. 处理已建立连接上请求

可以从这两个方面入手:

  • shutdown read,不再接受新的请求,对端继续写数据的时候会感知到失败;
  • 继续处理连接上已经正常接收的请求,处理完成后,回包,close 连接;

也可以考虑,不进行读端关闭,而是等连接空闲一段时间后再 close,是否尽快关闭更符合要求就要结合场景、要求来看。

如果对可用性要求比较苛刻,可能也会需要考虑将 connfd、connfd 上已经读取写入的 buffer 数据也一并传递给子进程处理。

11.2. 消息服务

  • 确认下自己服务的消息消费、确认机制是否合理
  • 不再收新消息
  • 处理完已收到的消息后,再退出

11.3. 自定义 AtExit 清理任务

有些任务会有些自定义任务,希望进程在退出之前,能够执行到,这种可以提供一个类似 AtExit 的注册函数,让进程退出之前能够执行业务自定义的清理逻辑。

不管是平滑重启,还是其他正常退出,对该支持都是有一定需求的。

12. 其他

有些场景下也希望传递 connfd,包括 connfd 上对应的读写的数据。

比如连接复用的场景,客户端可能会通过同一个连接发送多个请求,假如在中间某个时刻服务端执行热重启操作,服务端如果直接连接读关闭会导致后续客户端的数据发送失败,客户端关闭连接则可能导致之前已经接收的请求也无法正常响应。这种情况下,可以考虑服务端继续处理连接上请求,等连接空闲再关闭。会不会一直不空闲呢?有可能。

其实服务端不能预测客户端是否会采用连接复用模式,选择一个更可靠的处理方式会更好些,如果场景要求比较苛刻,并不希望通过上层重试来解决的话。这种可以考虑将 connfd 以及 connfd 上读写的 buffer 数据一并传递给子进程,交由子进程来处理,这个时候需要关注的点更多,处理起来更复杂,感兴趣的可以参考下 mosn 的实现。

13. 总结

热重启作为一种保证服务平滑重启、升级的实现方式,在今天看来依然非常有价值。本文描述了实现热重启的一些大致思路,并且通过 demo 循序渐进地描述了在 go 服务中如何予以实现。虽然没有提供一个完整的热重启实例给大家,但是相信大家读完之后应该已经可以亲手实现了。

由于作者本人水平有限,难免会有描述疏漏之处,欢迎大家指正。

参考文章

Unix 高级编程:进程间通信,Steven Richards

mosn 启动流程: https://mosn.io/blog/code/mosn-startup/

到此这篇关于Go 实现热重启的详细介绍的文章就介绍到这了,更多相关go热重启内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解如何热重启golang服务器

    服务端代码经常需要升级,对于线上系统的升级常用的做法是,通过前端的负载均衡(如nginx)来保证升级时至少有一个服务可用,依次(灰度)升级. 而另一种更方便的方法是在应用上做热重启,直接升级应用而不停服务. 原理 热重启的原理非常简单,但是涉及到一些系统调用以及父子进程之间文件句柄的传递等等细节比较多. 处理过程分为以下几个步骤: 监听信号(USR2) 收到信号时fork子进程(使用相同的启动命令),将服务监听的socket文件描述符传递给子进程 子进程监听父进程的socket,这个时候父进程和

  • Go 实现热重启的详细介绍

    最近在优化公司框架 trpc 时发现了一个热重启相关的问题,优化之余也总结沉淀下,对 go 如何实现热重启这方面的内容做一个简单的梳理. 1.什么是热重启? 热重启(Hot Restart),是一项保证服务可用性的手段.它允许服务重启期间,不中断已经建立的连接,老服务进程不再接受新连接请求,新连接请求将在新服务进程中受理.对于原服务进程中已经建立的连接,也可以将其设为读关闭,等待平滑处理完连接上的请求及连接空闲后再行退出.通过这种方式,可以保证已建立的连接不中断,连接上的事务(请求.处理.响应)

  • Idea配置热部署的详细教程

    一.概念 热部署就是正在运行状态的应用,修改了他的源码之后,在不重新启动的情况下能够自动把增量内容编译并部署到服务器上,使得修改立即生效.热部署为了解决的问题有两个, 一是在开发的时候,修改代码后不需要重启应用就能看到效果,大大提升开发效率:二是生产上运行的程序,可以在不停止运行的情况下进行升级,不影响用户使用. 二.Idea开启热部署 本篇文章主要是介绍Idea这款开发工具的热部署,而用Idea的人大多数都是用来开发java程序,当前流行的java程序主要有两种,第一种是传统的Web应用,依赖

  • IDEA热部署配置详细教程

    一.解释 热部署,即应用正属于运行状态时,我们对应用源码进行了修改更新,在不重新启动应用的情况下,可以能够自动的把更新的内容重新进行编译并部署到服务器上,使修改立即生效. 二.好处 在开发过程中,修改代码后不需要重启项目,就能看到效果,大大提高了开发效率. 在生产环境上运行的程序,可以在不停止运行的情况下进行升级,不影响用户的使用,提升了用户体验感. Tomcat运行多个项目时,不会因Tomcat的停止,而停止了其他的项目. 三.IDEA热部署配置 当前流行的JAVA程序主要有: ①传统的Web

  • mysql zip archive 版本(5.7.19)安装教程详细介绍

    1.  从官网下载zip archive版本http://dev.mysql.com/downloads/mysql/ MySQL v5.7.19 官方正式版(32/64位 安装版与zip解压版) 2. 解压缩至相应目录,并配置环境变量(将*\bin添加进path中): 3. 理论上现在这样就可以直接安装服务了,但是因为是默认配置,我们使用的时候会出现很多问题.比如里面的汉字全是乱码之类的,所以建议先配置一下默认文件.在解压的mysql目录下,新建个my.ini,//在根目录新建my.ini文件

  • 微信 小程序开发环境搭建详细介绍

    微信小程序可谓是今天最火的一个名词了,一经出现真是轰炸了整个开发人员,当然很多App开发人员有了一个担心,微信小程序的到来会不会给移动端App带来一个寒冬,身为一个Android开发者我是不相信的,即使有,那也是很遥远的未来. 不管微信小程序是否能颠覆当今的开发格局,我们都要以好奇的心态去接收,去学习.不排斥新技术,所以,心动不如行动,赶紧先搭建一个微信小程序开发工具.那么接下来就让我们一起来开始吧. 先放一张Github上demo的动态图 开发工具下载是看到GitHub上的分享.那么你可以直接

  • Centos7 网络配置详细介绍

    Centos7 网路配置详细介绍 1.查看当前网卡信息 [root@localhost ~]# nmcli connection show NAME UUID TYPE DEVICE enp0s3 5d58d8cc-8caf-458b-a672-ed0cdf58292e 802-3-ethernet --- CentOS7中对网上的命名规则有所变更,具体规则如下: eno1 :代表由主板 BIOS 內建的网卡 ens1 :代表由主板 BIOS 內建的 PCI-E 界面的网卡 enp2s0 :代表

  • SQL Server 数据库的备份详细介绍及注意事项

    SQL Server 备份 前言 为什么要备份?理由很简单--为了还原/恢复.当然,如果不备份,还可以通过磁盘恢复来找回丢失的文件,不过SQL Server很生气,后果很严重.到时候你就知道为什么先叫你备份一次再开始看文章了.∩__∩.本系列将介绍SQL Server所有可用的备份还原功能,并尽可能用实例说话. 什么是备份?SQL Server基于Windows,以文件形式存放资料,所以备份就是Windows上SQL Server相关文件的一个某个时间点的副本.根据备份类型的不同,副本的种类和内

  • CentOS Linux系统搭建Android开发环境详细介绍

    CentOS Linux系统搭建Android开发环境详细介绍 很多人都是在Windows下进行Android开发,但是对于Linux,Android开发环境方面的资料比较少,今天在网上找到了一位网友分享的在CentOS Linux系统中搭建Android开发环境的过程.下面就是其介绍的配置的详细步骤原文: 由于我最近每天使用的是CentOS 5.5,所以选择CentOS5.5作为我的开发环境. 主要包括以下步骤: 1.JDK安装 2.Eclipse安装 3.ADT安装 4.Android SD

  • IISExpress 配置允许外部访问详细介绍

     IISExpress 配置允许外部访问详细介绍 1.找到IISExpress的配置文件,位于 <文档>/IISExpress/config文件夹下,打开applicationhost.config,找到如下代码: <site name="WebSite1" id="1" serverAutoStart="true"> <application path="/"> <virtualDi

  • Linux启动过程详细介绍

    Linux启动过程详细介绍 启动第一步--加载BIOS 当你打开计算机电源,计算机会首先加载BIOS信息,BIOS信息是如此的重要,以至于计算机必须在最开始就找到它.这是因为BIOS中包含了CPU的相关信息.设备启动顺序信息.硬盘信息.内存信息.时钟信息.PnP特性等等.在此之后,计算机心里就有谱了,知道应该去读取哪个硬件设备了. 启动第二步--读取MBR 众所周知,硬盘上第0磁道第一个扇区被称为MBR,也就是Master Boot Record,即主引导记录,它的大小是512字节,别看地方不大

随机推荐