探究如何给Python程序做hotfix

标签: tuicool | 发表时间:2016-12-17 08:00 | 作者:
出处:http://itindex.net/admin/pagedetail

使用Python来写服务器端程序,很大的一个优势就是可以进行热更新,即在不停机的情况下,使改动后的程序生效。在开发阶段,这个功能可以大大提高开发效率(写代码–启动服务器–看效果–改代码–hotfix–看效果–提交~);而在生产环境中,可以以最小的代价(不停机)修复线上的bug。

我在项目中使用hotfix功能很长世间了,大概了解它是利用了Python的import/reload功能,但是并没有去自己研究过。最近看了云风大大写的一篇文章: 如何让 lua 做尽量正确的热更新,收获很多。也觉得应该研究一下Python的hotfix机制,毕竟是跟了自己这么久的小伙伴嘛。

import

说到hotfix就要从import语句说起。

首先建立这样一个简单的文件用作测试。

test_refresh.py
from __future__ import print_function

class RefreshClass(object):
    def __init__(self):
        self.value = 1

    def print_info(self):
        print('RefreshClass value: {} ver1.0'.format(self.value))

version = 1.0
  
print(version)

下面启动一个python解释器。

>>> import test_refresh as tr
1.0
>>> import test_refresh as tr
>>>> # edit  version=2.0
>>> import test_refresh as tr
>>> tr.version
1.0

重新import一个已经import过的模块,并不会重新执行文件(第二个import之后没有输出)。后面修改源文件并重新import后,对内存中tr.version的检查也验证了这一点。

为了能够重新加载修改后的源文件,我们需要明确的告诉Python解释器这一点。在Python中,sys.modules保存了已经加载过的模块。所以

>>> del sys.modules['test_refresh']
>>> import test_refresh as tr
2.0
>>> tr.version
2.0

在将test_refresh从sys.modules中删除之后再进行import操作,就会重新加载源文件了。

另外,如果我们只能拿到模块的字符串名字,可以使用__import__函数。

# edit version=3.0
>>> del sys.modules['test_refresh']
>>> tr = __import__('test_refresh')
3.0
>>> tr.version
3.0

reload

当我们面对的是一个之前已经import过的模块时,可以直接使用reload进行重新加载。

# edit version = 4.0
>>> reload(tr)          
4.0
<module 'test_refresh' from 'test_refresh.py'>
>>> tr.version
4.0

初步尝试hotfix

知道了模块重新加载的方法后,我们在Python的交互式命令行中,尝试动态改变一个类的行为逻辑。

test_refresh.py
from __future__ import print_function

class RefreshClass(object):
    def __init__(self):
        self.value = 1

    def print_info(self):
        print('RefreshClass value: {} ver1.0'.format(self.value))

这是测试类的当前状态。

我们创建一个该类的对象,验证下它的行为。

>>> a = tr.RefreshClass()
>>> a.value
1
>>> a.print_info()
RefreshClass value: 1 ver1.0

符合预期。

接下来,修改类的print_info函数为ver2.0,并reload模块。

# edit print_info ver2.0
>>> reload(tr)
4.0
<module 'test_refresh' from 'test_refresh.py'>
>>> a.value
1
>>> a.print_info()
RefreshClass value: 1 ver1.0

输出并没有如预期一样输出ver2.0……

那我们重新创建一个对象试试。

>>> b = tr.RefreshClass()
>>> b.value
2
>>> b.print_info()
RefreshClass value: 2 ver2.0

新对象b的行为是符合重新加载后的逻辑的。这说明,reload确实更新了RefreshClass类的行为,但是对于已经实例化的RefreshClass类的对象,却没有进行更新。对象a中的行为还是指向了旧的RefreshClass类。

在Python中,一切皆是对象。不仅实例a是对象,a的类RefreshClass也是对象。

这时,要修改a的行为,就需要用到a的__class__属性,来强制使a的类行为指向重新加载后的RefreshClass对象。

>>> a.__class__ = tr.RefreshClass
>>> a.value
1
>>> a.print_info()
RefreshClass value: 1 ver2.0

由于value是绑定在实例a上的,所以它的值并不会随RefreshClass的改变而改变。这也符合hotfix的预期逻辑:更新内存中实例的行为逻辑,但是不更新它们的数据。

接下来,我们还可以通过print_info函数的im func属性,验证在更改了\_class__属性后,函数确实更新成了新版本。

# edit print_info ver3.0
>>> reload(tr)
4.0
<module 'test_refresh' from 'test_refresh.py'>
>>> a.print_info.im_func
<function print_info at 0x7f50beeb2c08>
>>> c = tr.RefreshClass()
>>> c.print_info()
RefreshClass value: 3 ver3.0
>>> c.print_info.im_func
<function print_info at 0x7f50beeb2cf8>
>>> a.__class__ = tr.RefreshClass
>>> a.print_info.im_func
<function print_info at 0x7f50beeb2cf8>
>>> a.print_info()
RefreshClass value: 1 ver3.0

触发hotfix

上面的操作都是在Python的交互式解释器中运行的。下面我们将尝试使一个运行中的Python程序进行热更新。

这里遇到一个问题:作为Python程序入口的那个文件,不是以module的形式存在的,因此不能用上面的方式进行hotfix。所以我们需要保持入口文件的尽量简洁,而将绝大多数的逻辑功能交给其他的模块执行。

要触发一个正在运行中的Python程序进行热更新,我们需要有一种方式和Python程序通信。直接使用OS的标识文件是一个简单易行的方法。

test_refresh.py
from __future__ import print_function

import os
import time
import refresh_class


rc = refresh_class.RefreshClass()
while True:
    if os.path.exists('refresh.signal'):
        reload(refresh_class)
        rc.__class__ = refresh_class.RefreshClass
    time.sleep(5)
    rc.print_info()
refresh_class.py
class RefreshClass(object):
    def __init__(self):
        self.value = 1

    def print_info(self):
        print('RefreshClass value: {} ver1.0'.format(self.value))

每次我们修改完refresh_class.py文件,就创建一个refresh.signal文件。当refresh执行完毕,删除此文件即可。

这种做法一般来讲,会导致多次重新加载(因为一般不能及时的删除refresh.signal文件)。

所以,我们考虑使用Linux下的信号量,来同Python程序通信。

test_refresh.py
from __future__ import print_function

import time
import signal

import refresh_class

rc = refresh_class.RefreshClass()


def handl_refresh(signum, frame):
    reload(refresh_class)
    rc.__class__ = refresh_class.RefreshClass


signal.signal(signal.SIGUSR1, handl_refresh)
while True:
    time.sleep(5)
    rc.print_info()

我们在Python中注册了信号量SIGUSR1的handler,在其中热更新RefreshClass。

那么只需在另一个terminal中,输入:

kill -SIGUSR1 pid

即可向pid进程发送信号量SIGUSR1。

当然,还有其他方法可以触发hotfix,比如使用PIPE,或者直接开一个socket监听,自己设计消息格式来触发hotfix。

总结

以上进行Python热更新的方式,原理简单明了,就是利用了Python提供的import/reload机制。但是这种方式,需要去替换每一个类的实例的__class__成员。这就往往需要在某处保存目前内存中存在的所有对象(或者能够索引到所有活动对象的根对象),并且在类的设计上,需要所有类的基类提供一个通用的refresh方法,在其中进行__class__的替换工作。对于复杂的类组合方式,这种方法比较容易在热更新的时候漏掉某些实例。

其实还有一种途径可以代替__class__的替换工作。我们知道,如果不替换__class__的话,即使我们重新加载进来了新的module,但是所有的__class__还将指向旧的module的class。那么,我们不妨将新的module的内容插入到旧的module中。这样我们就可以不用费劲去更新每一个__class__了。一般的,我们会利用import hook(sys.meta_path,详见 PEP302)来实现这个替换。当然,这种方法的实现细节较多(因为module中可能存在module,class,function等互相嵌套的情况),不过只要实现完整后,就是一劳永逸的事情了。

相关代码可以在GitHub上找到 py-refresh

转载请注明出处: http://blog.guoyb.com/2016/12/13/py-hotfix/

欢迎使用微信扫描下方二维码,关注我的微信公众号TechTalking,技术·生活·思考:

相关 [python 程序 hotfix] 推荐:

探究如何给Python程序做hotfix

- - IT瘾-tuicool
使用Python来写服务器端程序,很大的一个优势就是可以进行热更新,即在不停机的情况下,使改动后的程序生效. 在开发阶段,这个功能可以大大提高开发效率(写代码–启动服务器–看效果–改代码–hotfix–看效果–提交~);而在生产环境中,可以以最小的代价(不停机)修复线上的bug. 我在项目中使用hotfix功能很长世间了,大概了解它是利用了Python的import/reload功能,但是并没有去自己研究过.

Python程序员培训计划

- 敏 - 我的宝贝孙秀楠 ﹣C++, Lua, 大连,程序员
Python程序员很幸福,因为他们不需要像C#程序员那样东一榔头西一棒子的找资源学习. 把下面资源按顺序学完,代码都自己手敲过,应该就是初级以上水平了. 之所以选择py3k,是感觉新版本的生命力应该更长久一些. 学完tutorial,再看看这个Python竞赛,花个一两周时间搞定它. 最后看看这本书,了解一些有趣的话题.

提高 Python 程序的运行速度

- Ken - python.cn(jobs, news)
尝试了一下用Python实现的K-Means Clustering算法,抽样了10000篇百科词条,分为1000个类,分词后词语总数为130000左右. 如果把1000个类定义为1000个向量,每个向量的元素个数为130000,K-Means Clustering算法的第一步是初始化这1000个向量的值,如果每个向量元素的值用float型存储,则需要的内存为:.

提高Python程序的运行速度

- Guancheng(冠诚) - 冬天里的酒吧
尝试了一下用Python实现的K-Means Clustering算法,抽样了10000篇百科词条,分为1000个类,分词后词语总数为130000左右. 如果把1000个类定义为1000个向量,每个向量的元素个数为130000,K-Means Clustering算法的第一步是初始化这1000个向量的值,如果每个向量元素的值用float型存储,则需要的内存为:.

Python程序的执行原理

- - 非技术 - ITeye博客
Python先把代码(.py文件)编译成字节码,交给字节码虚拟机,然后虚拟机一条一条执行字节码指令,从而完成程序的执行. 字节码在Python虚拟机程序里对应的是PyCodeObject对象. .pyc文件是字节码在磁盘上的表现形式. PyCodeObject对象的创建时机是模块加载的时候,即import.

每个程序员都应该学习使用Python或Ruby

- Kings - 开源中国社区最新新闻
本文是从 Why every programmer should learn Python or Ruby 这篇文章翻译而来. 如 果你是个学生,你应该会C,C++和Java. 还会一些VB,或C#/.NET. 多少你还可能开发过一些Web网页,你知道一些HTML,CSS和 JavaScript知识.

Python程序语言快速上手教程

- - SEM WATCH
本文是面向SEO人群的Python程序语言入门教程,也适用于其他没有程序基础但想学习些程序,以解决简单的实际应用需求的人群. 在后面会尽量用最基础的角度来介绍这门语言. 本来打算从网上找一篇入门教程,但因为Python很少是程序员的第一次接触程序所学的语言,所以网上现有的教程多不是很基础,还是决定自己写下这些.

全面解读python web 程序的9种部署方式

- - 鲁塔弗的博客
python有很多web 开发框架,代码写完了,部署上线是个大事,通常来说,web应用一般是三层结构. 主流的web server 一个巴掌就能数出来,apache,lighttpd,nginx,iis. application,中文名叫做应用服务,就是你基于某个web framework写的应用代码.

Python 程序员应该知道的 10 个库

- - 博客 - 伯乐在线
Python是优雅的,使用这些库可以使你的代码更简洁,并保持持久性. 抛弃 optparse和 argparse吧,使用 docstrings来构建优雅的,可读性强的,并且复杂(如果你需要的话)的命令行界面. IMO2013年创建的最好的库. Requests,或称为人类使用的HTTP,是一个处理HTTP请求更为pythonic 的方法,比 urllib2更更更好用.

让你的python程序同时兼容python2和python3

- - python.cn(jobs, news)
python邮件列表里有人发表言论说“python3在10内都无法普及”. 在我看来这样的观点有些过于悲观,python3和python2虽然不兼容,但他们之间差别并没很多人想像的那么大. 你只需要对自己的代码稍微做些修改就可以很好的同时支持python2和python3的. 下面我将简要的介绍一下如何让自己的python代码如何同时支持python2和python3.