使用 Python 来写服务器端程序,很大的一个优势就是可以进行热更新,即在不停机的情况下,使改动后的程序生效。在开发阶段,这个功能可以大大提高开发效率(写代码–启动服务器–看效果–改代码–hotfix–看效果–提交~);而在生产环境中,可以以最小的代价(不停机)修复线上的 bug。
我在项目中使用 hotfix 功能很长世间了,大概了解它是利用了 Python 的 import/reload 功能,但是并没有去自己研究过。最近看了云风大大写的一篇文章: ,收获很多。也觉得应该研究一下 Python 的 hotfix 机制,毕竟是跟了自己这么久的小伙伴嘛。
说到 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
当我们面对的是一个之前已经 import 过的模块时,可以直接使用 reload 进行重新加载。
- # edit version = 4.0
- >>> reload(tr)
- 4.0
- <module 'test_refresh' from 'test_refresh.py'>
- >>> tr.version
- 4.0
知道了模块重新加载的方法后,我们在 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 _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
上面的操作都是在 Python 的交互式解释器中运行的。下面我们将尝试使一个运行中的 Python 程序进行热更新。
这里遇到一个问题:作为 Python 程序入口的那个文件,不是以 module 的形式存在的,因此不能用上面的方式进行 hotfix。所以我们需要保持入口文件的尽量简洁,而将绝大多数的逻辑功能交给其他的模块执行。
要触发一个正在运行中的 Python 程序进行热更新,我们需要有一种方式和 Python 程序通信。直接使用 OS 的标识文件是一个简单易行的方法。
test_refresh.pyrefresh_class.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()
- 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 中,输入:
即可向 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, 详见) 来实现这个替换。当然,这种方法的实现细节较多(因为 module 中可能存在 module,class,function 等互相嵌套的情况),不过只要实现完整后,就是一劳永逸的事情了。
相关代码可以在 GitHub 上找到 。
欢迎使用微信扫描下方二维码,关注我的微信公众号 TechTalking,技术 · 生活 · 思考:
来源: