Go 如何实现热重启

作者:zhijiezhang,腾讯 PCG 后台开发工程师

最近在优化公司框架 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 这里的这个局限性,

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值