分享
  1. 首页
  2. 文章

golang 服务平滑重启小结

王清培 · · 3470 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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 HUP,graceful 接受 signal USR2graceful 比较纯粹的 web host,endless 支持一些 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)


有疑问加站长微信联系(非本文作者)

本文来自:51CTO博客

感谢作者:王清培

查看原文:golang 服务平滑重启小结

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
3470 次点击
被以下专栏收入,发现更多相似内容
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏