golang 服务平滑重启小结


  • 背景
  • golang 程序平滑重启框架
  • supervisor 出现 defunct 原因
  • 使用 master/worker 模式

背景

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

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

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

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

golang 程序平滑重启框架

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

graceful https://github.com/tylerb/graceful
endless https://github.com/fvbock/endless

上面两个是 github 排名靠前的 web host 框架,都是支持平滑重启的,只不过接受的进程信号有点区别 endless 接受 signal HUPgraceful 接受 signal USR2graceful 比较纯粹的 web hostendless 支持一些 routing 的能力。

我们看下 endless 处理信号。(如果对 srv.fork() 内部感兴趣可以品读品读。)

func (srv *endlessServer) handleSignals() {
    var sig os.Signal

    signal.Notify(
        srv.sigChan,
        hookableSignals...,
    )

    pid := syscall.Getpid()
    for {
        sig = <-srv.sigChan
        srv.signalHooks(PRE_SIGNAL, sig)
        switch sig {
        case syscall.SIGHUP:
            log.Println(pid, "Received SIGHUP. forking.")
            err := srv.fork()
            if err != nil {
                log.Println("Fork err:", err)
            }
        case syscall.SIGUSR1:
            log.Println(pid, "Received SIGUSR1.")
        case syscall.SIGUSR2:
            log.Println(pid, "Received SIGUSR2.")
            srv.hammerTime(0 * time.Second)
        case syscall.SIGINT:
            log.Println(pid, "Received SIGINT.")
            srv.shutdown()
        case syscall.SIGTERM:
            log.Println(pid, "Received SIGTERM.")
            srv.shutdown()
        case syscall.SIGTSTP:
            log.Println(pid, "Received SIGTSTP.")
        default:
            log.Printf("Received %v: nothing i care about...\n", sig)
        }
        srv.signalHooks(POST_SIGNAL, sig)
    }
}

supervisor 出现 defunct 原因

使用 supervisor 管理的进程,中间需要加一层代理,原因就是 supervisor 可以管理自己启动的进程,意思就是 supervisor 可以拿到自己启动的进程id(PID),可以检测进程是否还存活,carsh后做自动拉起,退出时能接收到进程退出信号。

但是如果我们用了平滑重启框架,原来被 supervisor 启动的进程发布重启 __fork__子进程之后正常退出,当再次发布重启 fork 子进程后就会变成无主进程就会出现 defunct(僵尸进程) 的问题,原因就是此子进程无法完成退出,没有主进程来接受它退出的信号,退出进程本身的少量数据结构无法销毁。

使用 master/worker 模式

supervisor 本身提供了 pidproxy 程序,我们在配置 supervisor command 时候使用 pidproxy 来做一层代理。由于进程的id会随着不停的发布 fork 子进程而变化,所以需要将程序的每次启动 PID 保存在一个文件中,一般大型分布式软件都需要这样的一个文件,mysqlzookeeper 等,目的就是为了拿到目标进程id。

这其实是一种 master/worker 模式,master 进程交给 supervisor 管理,supervisor 启动 master 进程,也就是 pidproxy 程序,再由 pidproxy 来启动我们目标程序,随便我们目标程序 fork 多少次子进程都不会影响 pidproxy master 进程。

pidproxy 依赖 PID 文件,我们需要保证程序每次启动的时候都要写入当前进程 idPID 文件,这样 pidproxy 才能工作。
supervisor 默认的 pidproxy 文件是不能直接使用的,我们需要适当的修改。

https://github.com/Supervisor/supervisor/blob/master/supervisor/pidproxy.py

#!/usr/bin/env python

""" An executable which proxies for a subprocess; upon a signal, it sends that
signal to the process identified by a pidfile. """

import os
import sys
import signal
import time

class PidProxy:
    pid = None
    def __init__(self, args):
        self.setsignals()
        try:
            self.pidfile, cmdargs = args[1], args[2:]
            self.command = os.path.abspath(cmdargs[0])
            self.cmdargs = cmdargs
        except (ValueError, IndexError):
            self.usage()
            sys.exit(1)

    def go(self):
        self.pid = os.spawnv(os.P_NOWAIT, self.command, self.cmdargs)
        while 1:
            time.sleep(5)
            try:
                pid = os.waitpid(-1, os.WNOHANG)[0]
            except OSError:
                pid = None
            if pid:
                break

    def usage(self):
        print("pidproxy.py <pidfile name> <command> [<cmdarg1> ...]")

    def setsignals(self):
        signal.signal(signal.SIGTERM, self.passtochild)
        signal.signal(signal.SIGHUP, self.passtochild)
        signal.signal(signal.SIGINT, self.passtochild)
        signal.signal(signal.SIGUSR1, self.passtochild)
        signal.signal(signal.SIGUSR2, self.passtochild)
        signal.signal(signal.SIGQUIT, self.passtochild)
        signal.signal(signal.SIGCHLD, self.reap)

    def reap(self, sig, frame):
        # do nothing, we reap our child synchronously
        pass

    def passtochild(self, sig, frame):
        try:
            with open(self.pidfile, 'r') as f:
                pid = int(f.read().strip())
        except:
            print("Can't read child pidfile %s!" % self.pidfile)
            return
        os.kill(pid, sig)
        if sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
            sys.exit(0)

def main():
    pp = PidProxy(sys.argv)
    pp.go()

if __name__ == '__main__':
    main()

我们重点看下这个方法:

def go(self):
        self.pid = os.spawnv(os.P_NOWAIT, self.command, self.cmdargs)
        while 1:
            time.sleep(5)
            try:
                pid = os.waitpid(-1, os.WNOHANG)[0]
            except OSError:
                pid = None
            if pid:
                break

go 方法是守护方法,会拿到启动进程的id,然后做 waitpid ,但是当我们 fork 进程的时候主进程会退出,os.waitpid 会收到退出信号,然后就退出了,但是这是个正常的切换逻辑。

可以两个办法解决,第一个就是让 go 方法纯粹是个守护进程,去掉退出逻辑,在信号处理方法中处理:

    def passtochild(self, sig, frame):
        pid = self.getPid()
        os.kill(pid, sig)
        time.sleep(5)
        try:
            pid = os.waitpid(self.pid, os.WNOHANG)[0]
        except OSError:
            print("wait pid null pid %s", self.pid)
        print("pid shutdown.%s", pid)
        self.pid = self.getPid()

        if self.pid == 0:
            sys.exit(0)

        if sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
            print("exit:%s", sig)
            sys.exit(0)

还有一个方法就是修改原有go方法:

    def go(self):
        self.pid = os.spawnv(os.P_NOWAIT, self.command, self.cmdargs)
        while 1:
            time.sleep(5)
            try:
                pid = os.waitpid(-1, os.WNOHANG)[0]
            except OSError:
                pid = None
            try:
                with open(self.pidfile, 'r') as f:
                    pid = int(f.read().strip())
            except:
                print("Can't read child pidfile %s!" % self.pidfile)
            try:
                os.kill(pid, 0)
            except OSError:
                sys.exit(0)

当然还可以用其他方法或者思路,这里只是抛出问题。如果你想知道真正问题在哪里,可以直接在本地 debug pidproxy 脚本文件,还是比较有意思的,知道真正问题在哪里如何修改,就完全由你来发挥了。

作者:王清培 (趣头条 Tech Leader)

原文地址:https://www.cnblogs.com/wangiqngpei557/p/11704747.html

时间: 2024-12-20 01:29:00

golang 服务平滑重启小结的相关文章

Gong服务实现平滑重启分析

平滑重启是指能让我们的程序在重启的过程不中断服务,新老进程无缝衔接,实现零停机时间(Zero-Downtime)部署: 平滑重启是建立在优雅退出的基础之上的,之前一篇文章介绍了相关实现:Golang中使用Shutdown特性对http服务进行优雅退出使用总结 目前实现平滑重启的主要策略有两种: 方案一:我们的服务如果是多机器部署,可以通过网关程序,将即将重启服务的机器从网关下线,重启完成后再重新上线,该方案适合多机器部署的企业级应用: 方案二:让我们的程序实现自启动,重启子进程来实现平滑重启,核

Nginx平滑升级和平滑重启

如果要对当前的Nginx服务器进行版本升级,应用新模块,如果用户访问量比较大的时候,如果需要在不影响客户的情况下进行升级的话,这时候就得考虑平滑升级了. 平滑升级的过程,Nginx服务器接受到USR2信号后,首先将旧的nginx.pid文件添加后缀.oldbin,变为nginx.pid.oldbin文件,然后执行新版本的Nginx服务器的二进制的文件启动服务,这个时候需要提前将编译好的新版本的二进制实现复制到sbin文件夹中.如果新的服务启动成功,系统中将有新旧两个Nginx服务共同提供Web服

node.js cluster多进程、负载均衡和平滑重启

1 cluster多进程 cluster经过好几代的发展,现在已经比较好使了.利用cluster,可以自动完成子进程worker分配request的事情,就不再需要自己写代码在master进程中robin式给每个worker分配任务了. const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster

Nginx的启动、停止、平滑重启

在Linux下,Nginx服务的主要操作就是启动.停止和重启. 1.Nginx的启动: 如果nginx安装在/usr/local/nginx/ 目录中,那么启动nginx的命令就是:/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf  #"-c"指定了配置文件的路径,如果不加该参数,Nginx会默认加载其安装目录的conf子目录中的nginx.conf文件. 2. Nginx的停止: 我们可以通过ps命令来查找

nginx 的停止和平滑重启

Nginx的停止 停止操作是通过向nginx进程发送信号来进行的步骤1:查询nginx主进程号ps -aux | grep nginx在进程列表里 面找master进程,它的编号就是主进程号了.步骤2:发送信号从容停止Nginx:kill -QUIT 主进程号快速停止Nginx:kill -TERM 主进程号强制停止Nginx:pkill -9 nginx另外, 若在nginx.conf配置了pid文件存放路径则该文件存放的就是Nginx主进程号,如果没指定则放在nginx的logs目录下.有了

Nginx的启动、关闭和平滑重启

在完成对nginx.conf文件的配置后,就可以启动服务了.Nginx自身提供了一些用于日常维护的命令,下面进行详细的介绍. 1. Nginx基本信息检查 (1)检查Nginx配置文件的正确性 Nginx提供的配置文件调试功能非常有用,可以快速定位配置文件存在的问题.执行如下命令可检测配置文件的正确性: /opt/nginx/sbin/nginx –t 或者 /opt/nginx/sbin/nginx -t -c /opt/nginx/conf/nginx.conf 其中,"-t"参数

使用PowerShell操作Windows服务的命令小结

PowerShell在处理Windows服务方面,提供了强大的功能,很多方便.强大的cmdlet等着你去发掘. Get-Service,别名gsv,获取服务对象. 举例:gsv eventlog 或 $evtlog = gsv eventlog Start-Service,启动服务. Stop-Service,停止服务. Restart-Service,重启服务. Suspend-Service,挂起/暂停服务. Resume-Service,继续服务. Set-Service,设置服务的属性.

Nginx常用的平滑重启

之前在做运维工作中,经常需要添加虚拟主机,或者添加修改配置文件,但是测试环境还好,随便玩,如果是生产环境的话,既要保证配置不出问题,有不能中断服务.如果是这样的话,就需要对配置文件进行语法检测以及平滑重启. nginx的语法检测 /usr/local/nginx/sbin/nginx -t 如果语法检测没有报错的话,需要平滑加载配置文件 /usr/local/nginx/sbin/nginx -s reload 注:上述nginx平滑重启的方式只有版本大于等于 0.7.53才能使用,否则的话需要

使用第三方库来支持平滑重启

平滑重启的第三方库overseer package main import ( "crypto/md5" "encoding/hex" "flag" "fmt" "github.com/jpillora/overseer" "gopkg.in/ini.v1" "io" "log" "net/http" "os"