• 背景
  • 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)

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

  1. Golang学习--平滑重启

    在上一篇博客介绍TOML配置的时候,讲到了通过信号通知重载配置.我们在这一篇中介绍下如何的平滑重启server. 与重载配置相同的是我们也需要通过信号来通知server重启,但关键在于平滑重启,如果只 ...

  2. Gong服务实现平滑重启分析

    平滑重启是指能让我们的程序在重启的过程不中断服务,新老进程无缝衔接,实现零停机时间(Zero-Downtime)部署: 平滑重启是建立在优雅退出的基础之上的,之前一篇文章介绍了相关实现:Golang中 ...

  3. yarn关于app max attempt深度解析,针对长服务appmaster平滑重启

    在YARN上开发长服务,需要注意fault-tolerance,本篇文章对appmaster的平滑重启的一个参数做了解析,如何设置可以有助于达到appmaster平滑重启. 在yarn-site.xm ...

  4. 【学习笔记】启动Nginx、查看nginx进程、查看nginx服务主进程的方式、Nginx服务可接受的信号、nginx帮助命令、Nginx平滑重启、Nginx服务器的升级

     1.启动nginx的方式: cd /usr/local/nginx ls ./nginx -c nginx.conf 2.查看nginx的进程方式: [root@localhost nginx] ...

  5. Centos--swoole平滑重启服务

    平滑重启: 已经打开的服务: 首先在server服务中为进程添加名字: /** * @param $server */ public function onStart($server) { swool ...

  6. 通过Nginx、Consul、Upsync实现动态负载均衡和服务平滑发布

    前提 前段时间顺利地把整个服务集群和中间件全部从UCloud迁移到阿里云,笔者担任了架构和半个运维的角色.这里详细记录一下通过Nginx.Consul.Upsync实现动态负载均衡和服务平滑发布的核心 ...

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

    1 cluster多进程 cluster经过好几代的发展,现在已经比较好使了.利用cluster,可以自动完成子进程worker分配request的事情,就不再需要自己写代码在master进程中rob ...

  8. Nginx的平滑重启和平滑升级

    一,Nginx的平滑重启如果改变了Nginx的配置文件(nginx.conf),想重启Nginx,可以发送系统信号给Nginx主进程的方式来进行.在重启之前,要确认Nginx配置文件的语法是正确的. ...

  9. Nginx 的启动、停止、平滑重启、信号控制和平滑升级

    Nginx 的启动         假设 nginx 安装在 /usr/local/nginx 目录中,那么启动 nginx 的命令就是: [root@localhost ~]# /usr/local ...

随机推荐

  1. centos7 kubernetes单机安装

    单机版的kubernetes 适合初学者,对kuber有个很好的入门. 因为centos系统内置了安装源.我们可以直接安装 1.yum install -y etco kubernetes 2.whe ...

  2. Filter过滤器学习

    一.Filter简介 Filter也称之为过滤器,它是Servlet技术中最激动人心的技术,WEB开发人员通过Filter技术,对web服务器管理的所有web资源:例如Jsp, Servlet, 静态 ...

  3. 开发必配的Finder设置

    1.显示标签页.显示路径栏.显示状态栏的设置位置,在访达->显示-> 显示状态栏 个人三个都设置了,但是觉得显示状态栏用的并不多,反而多一行,下面是显示状态栏的效果,主要可以一眼看出有多少 ...

  4. 阿里雷卷:Reactive 基金会的成立将对开发方式带来哪些影响?

    作者 | 赵钰莹 近日,Linux 基金会宣布成立 Reactive 基金会.对于 Reactive,各位开发者应该并不陌生,尤其是 Node.js 开发者,但真正了解并意识到这件事情对开发方式带来的 ...

  5. Spring boot运行原理-自定义自动配置类

    在前面SpringBoot的文章中介绍了SpringBoot的基本配置,今天我们将给大家讲一讲SpringBoot的运行原理,然后根据原理我们自定义一个starter pom. 本章对于后续继续学习S ...

  6. UGUI_冻结技能键盘点击触发

    1.在某一张image图上添加Button组件,使其具有点击触发事件的功能: 2.outline组件 3.SkillItem脚本 using System.Collections; using Sys ...

  7. Oracle创建自增主键表

    1.创建表 /*第一步:创建表格*/ create table t_user( id int primary key, --主键,自增长 username varchar(), password va ...

  8. 使用java程序作为celery的工作节点

    celery是python实现的分布式调度框架,有时候想用celery去调用java服务,正好有一个celery-java的库可以使用,能达到这个效果,记录一下: 先添加依赖: <depende ...

  9. Spring Boot 面试题总结

    1.什么是spring boot 答案:springboot是用来简化spring应用的初始搭建和开发过程,使用特定的配置文件来配置,例如application.properties,简化来maven ...

  10. MySQL实现Oracle rank()排序

    一.Oracle写法介绍 MySQL5.7版本没有提供类似Oracle的分析函数,比如开窗函数over(...),oracle开窗函数over(...)使用的话一般是和order.partition ...