Python程序中不同的重启机制

分析典型案例:

  1. Celery 分布式异步任务框架
  2. Gunicorn Web容器

之所以挑这两个,不仅仅是应用广泛,而且两个的进程模型比较类似,都是Master、Worker的形式,在热重启上思路和做法又基本不同,比较有参考意义

知识点:

  • atexit
  • os.execv
  • 模块共享变量
  • 信号处理
  • sleep原理:select
  • 文件描述符共享

这几个知识点不难,区别只在于Celery和Gunicorn的应用方式。如果脑海中有这样的知识点,这篇文章也就是开阔下视野而已。。。

Celery和Gunicorn都会在接收到HUP信号时,进行热重启操作

Celery的重启:关旧Worker,然后execv重新启动整个进程

Gunicorn的重启:建立新Worker,再关旧Worker,Master不动

下面具体的看下它们的操作和核心代码。

对于Celery:


  1. def _reload_current_worker(): 
  2.     platforms.close_open_fds([ 
  3.         sys.__stdin__, sys.__stdout__, sys.__stderr__, 
  4.     ]) 
  5.     os.execv(sys.executable, [sys.executable] + sys.argv) 
  6.   
  7.   
  8.    
  9. def install_worker_restart_handler(worker, sig='SIGHUP'): 
  10.     def restart_worker_sig_handler(*args): 
  11.         """Signal handler restarting the current python program.""" 
  12.         import atexit 
  13.         atexit.register(_reload_current_worker) 
  14.         from celery.worker import state 
  15.         state.should_stop = EX_OK 
  16.     platforms.signals[sig] = restart_worker_sig_handler 

HUP上挂的restart_worker_sig_handler 就做了两件事:

  1. 注册atexit函数
  2. 设置全局变量

考虑到这个执行顺序,应该就能明白Celery 是Master和Worker都退出了,崭新呈现。。

看过APUE的小伙伴,应该比较熟悉 atexit 了,这里也不多说。os.execv还挺有意思,根据Python文档,这个函数会执行一个新的函数用于替换掉 当前进程 ,在Unix里,新的进程直接把可执行程序读进进程,保留同样的PID。

在Python os标准库中,这是一整套基本一毛一样的函数,也许应该叫做函数族了:

  • os. execl ( path , arg0 , arg1 , … )
  • os. execle ( path , arg0 , arg1 , … , env )
  • os. execlp ( file , arg0 , arg1 , … )
  • os. execlpe ( file , arg0 , arg1 , … , env )
  • os. execv ( path , args )
  • os. execve ( path , args , env )
  • os. execvp ( file , args )
  • os. execvpe ( file , args , env )

以exec开头,后缀中的l和v两种,代表命令行参数是否是变长的(前者不可变),p代表是否使用PATH定位程序文件,e自然就是在新进程中是否使用ENV环境变量了

然后给worker的state.should_stop变量设置成False。。。 模块共享变量 这个是 Python FAQ里提到的一种方便的跨模块消息传递的方式,运用了Python module的单例。我们都知道Python只有一个进程,所以单例的变量到处共享

而should_stop这个变量也是简单粗暴,worker在执行任务的循环中判断这个变量,即执行异步任务->查看变量->获得异步任务->继续执行 的循环中,如果True就抛出一个【应该关闭】异常,worker由此退出。

这里面有一个不大不小的坑是:信号的发送对于外部的工具,例如kill,是非阻塞的,所以当HUP信号被发出后,worker可能并没有完成重启(等待正在执行的旧任务完成 才退出和新建),因此如果整个系统中只使用HUP信号挨个灰度各个机器,那么很有可能出现全部worker离线的情况

接下来我们看看Gunicorn的重启机制:

信号实质上挂在在Arbiter上,Arbiter相当于master,守护和管理worker的,管理各种信号,事实上它init的时候就给自己起好Master的名字了,打印的时候会打出来:


  1. class Arbiter(object): 
  2.     def __init__(self, app): 
  3.         #一部分略 
  4.         self.master_name = "Master"    
  5.     def handle_hup(self): 
  6.         """\ 
  7.         HUP handling. 
  8.         - Reload configuration 
  9.         - Start the new worker processes with a new configuration 
  10.         - Gracefully shutdown the old worker processes 
  11.         """ 
  12.         self.log.info("Hang up: %s", self.master_name) 
  13.         self.reload() 

这里的函数文档里写了处理HUP信号的过程了,简单的三行:

  1. 读取配置
  2. 开启新worker
  3. 优雅关闭旧Worker

reload函数的实现本身没什么复杂的,因为Gunicorn 是个Web容器,所以master里面是没有业务逻辑的,worker都是master fork出来的,fork是可以带着文件描述符(自然也包括socket)过去的。这也是Gunicorn可以随意增减worker的根源

master只负责两件事情:

  1. 拿着被Fork的worker的PID,以供关闭和处理
  2. 1秒醒来一次,看看有没有worker超时了需要被干掉

  1. while True: 
  2.     sig = self.SIG_QUEUE.pop(0) if len(self.SIG_QUEUE) else None 
  3.     if sig is None: 
  4.         self.sleep() 
  5.         self.murder_workers() 
  6.         self.manage_workers() 
  7.         continue 
  8.     else: 
  9.         #处理信号 

在sleep函数中,使用了select.select+timeout实现,和time.sleep的原理是一样的,但不同之处在于select监听了自己创建的一个PIPE,以供wakeup立即唤醒

总结

以上就介绍了Celery和Gunicorn的重启机制差异。

从这两者的设计来看,可以理解他们这样实现的差异。

Celery是个分布式、异步的任务队列,任务信息以及排队信息实质上是持久化在外部的MQ中的,例如RabbitMQ和redis,其中的持久化方式,这应该另外写一篇《高级消息队列协议AMQP介绍》,就不在这里说了,对于Celery的Master和Worker来说,可以说是完全没有状态的。由Celery的部署方式也可以知道,近似于一个服务发现的框架,下线的Worker不会对整个分布式系统带来任何影响。唯一的例外可能是Beat组件,作为Celery定时任务的节拍器,要做少许改造以适配分布式的架构,并且实现Send Once功能。

Gunicorn作为Python的Web容器之一,会接收用户的请求,虽然我们通常都会使用nginx放在Gunicorn前方做反向代理,通常也可以使用nginx来做upstream offline、online的热重启,但那就不是一个层次的事情了

这里回头来再吐槽Golang

项目中使用到了Golang的一个Web框架,Golang在1.8中也已经支持Http.Server的热关闭了,但是一是因为刚出不就(竟然现在才出),二是因为Golang的进程模型和Python大相近庭,go协程嘛,目前还没有看到那个Web框架中真正实现Gunicorn类似的热重启。

Golang 在fcgi的操作应该就类似Python之于wsgi了。。我感觉我是选择错了一个web框架

也没看见有人用syscall.Exec来用一下execve系统调用,Golang也没看见有人用socket REUSE。。作为一个懒惰的人感觉很蛋疼。。。

先让nginx大法做这件事情好了。

本文作者:佚名

来源:51CTO

时间: 2024-09-20 04:05:50

Python程序中不同的重启机制的相关文章

在Python程序中实现分布式进程的教程

  这篇文章主要介绍了在Python程序中实现分布式进程的教程,在多进程编程中十分有用,示例代码基于Python2.x版本,需要的朋友可以参考下 在Thread和Process中,应当优选Process,因为Process更稳定,而且,Process可以分布到多台机器上,而Thread最多只能分布到同一台机器的多个CPU上. Python的multiprocessing模块不但支持多进程,其中managers子模块还支持把多进程分布到多台机器上.一个服务进程可以作为调度者,将任务分布到其他多个进

在Python程序中操作文件之flush()方法的使用

  这篇文章主要介绍了在Python程序中操作文件之flush()方法的使用教程,是Python入门学习中的基础知识,需要的朋友可以参考下 flush()方法刷新内部缓冲区,像标准输入输出的fflush.这类似文件的对象,无操作. Python关闭时自动刷新文件.但是可能要关闭任何文件之前刷新数据. 语法 以下是flush()方法的语法: ? 1 fileObject.flush(); 参数 NA 返回值 此方法不返回任何值. 例子 下面的例子显示了flush()方法的使用. ? 1 2 3 4

在Python程序中操作文件之isatty()方法的使用

  这篇文章主要介绍了在Python程序中操作文件之isatty()方法的使用教程,是Python入门学习中的基础知识,需要的朋友可以参考下 如果文件已连接(与终端设备相关联)到一个tty(状)的设备,isatty()方法返回True,否则返回False. 语法 以下是isatty()方法的语法: ? 1 fileObject.isatty(); 参数 NA 返回值 如果该文件被连接(与终端设备相关联)到一个tty(类似终端)设备此方法返回true,否则返回false. 例子 下面的例子显示了i

在Python程序中进行文件读取和写入操作的教程

  这篇文章主要介绍了在Python程序中进行文件读取和写入操作的教程,是Python学习当中的基础知识,需要的朋友可以参考下 读写文件是最常见的IO操作.Python内置了读写文件的函数,用法和C是兼容的. 读写文件前,我们先必须了解一下,在磁盘上读写文件的功能都是由操作系统提供的,现代操作系统不允许普通的程序直接操作磁盘,所以,读写文件就是请求操作系统打开一个文件对象(通常称为文件描述符),然后,通过操作系统提供的接口从这个文件对象中读取数据(读文件),或者把数据写入这个文件对象(写文件).

Python程序中设置HTTP代理_python

0x00 前言 大家对HTTP代理应该都非常熟悉,它在很多方面都有着极为广泛的应用.HTTP代理分为正向代理和反向代理两种,后者一般用于将防火墙后面的服务提供给用户访问或者进行负载均衡,典型的有Nginx.HAProxy等.本文所讨论的是正向代理. HTTP代理最常见的用途是用于网络共享.网络加速和网络限制突破等.此外,HTTP代理也常用于Web应用调试.Android/IOS APP 中所调用的Web API监控和分析,目前的知名软件有Fiddler.Charles.Burp Suite和mi

在Go程序中实现服务器重启的方法_Golang

Go被设计为一种后台语言,它通常也被用于后端程序中.服务端程序是GO语言最常见的软件产品.在这我要解决的问题是:如何干净利落地升级正在运行的服务端程序. 目标:     不关闭现有连接:例如我们不希望关掉已部署的运行中的程序.但又想不受限制地随时升级服务.     socket连接要随时响应用户请求:任何时刻socket的关闭可能使用户返回'连接被拒绝'的消息,而这是不可取的.     新的进程要能够启动并替换掉旧的. 原理 在基于Unix的操作系统中,signal(信号)是与长时间运行的进程交

请教下高人:.net程序中的模板读取机制

问题描述 最近写了几个小程序,想用模板实现样式,看了下DZ NT的程序,很好很强大,用上了,一时半会也没看明白,我想请教下高人,这个模板如何读取的?比如我做好了数据库ASPX设计好了cs文件也写好了本来也可以发布了但是我想做一套模板让程序读取模板(大多数程序中的templates目录下的) 然后以模板的方式显示在前台请教一下DZ NT的模板读取机制是怎么样的?在线等啦

从Python程序中访问Java类的简单示例_python

from jnius import autoclass >>> Stack = autoclass('java.util.Stack') >>> stack = Stack() >>> stack.push('hello') >>> stack.push('world') >>> stack.pop() 'world' >>> stack.pop() 'hello' 上面的代码中,我们使用 auto

Python程序中使用SQLAlchemy时出现乱码的解决方案_python

今天对clubot进行了升级, 但是导入数据后中文乱码, 一开是找资料说是在创建引擎的时候添加编码信息: engine = create_engine("mysql://root:@localhost:3306/clubot?charset=utf8") 但是这并不行, 然后查看表信息: > show create table clubot_members; clubot_members | CREATE TABLE `clubot_members` ( `id` int(11)