UDN-企业互联网技术人气社区

板块导航

浏览  : 1210
回复  : 0

[干货] 探究如何给Python程序做hotfix

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

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

  import

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

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

  1. from __future__ import print_function

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

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

  7. version = 1.0
  8.   
  9. print(version)
复制代码


  下面启动一个python解释器。

  1. >>> import test_refresh as tr
  2. 1.0
  3. >>> import test_refresh as tr
  4. >>>> # edit  version=2.0
  5. >>> import test_refresh as tr
  6. >>> tr.version
  7. 1.0
复制代码


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

  为了能够重新加载修改后的源文件,我们需要明确的告诉Python解释器这一点。在Python中,sys.modules保存了已经加载过的模块。所以
  1. >>> del sys.modules['test_refresh']
  2. >>> import test_refresh as tr
  3. 2.0
  4. >>> tr.version
  5. 2.0
复制代码


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

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

  1. # edit version=3.0
  2. >>> del sys.modules['test_refresh']
  3. >>> tr = __import__('test_refresh')
  4. 3.0
  5. >>> tr.version
  6. 3.0
复制代码


  reload

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

  1. # edit version = 4.0
  2. >>> reload(tr)         
  3. 4.0
  4. <module 'test_refresh' from 'test_refresh.py'>
  5. >>> tr.version
  6. 4.0
复制代码


  初步尝试hotfix

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

  1. from __future__ import print_function

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

  5.     def print_info(self):
  6.         print('RefreshClass value: {} ver1.0'.format(self.value))
复制代码

  这是测试类的当前状态。

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

  1. >>> a = tr.RefreshClass()
  2. >>> a.value
  3. 1
  4. >>> a.print_info()
  5. RefreshClass value: 1 ver1.0
复制代码

  符合预期。

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

  1. # edit print_info ver2.0
  2. >>> reload(tr)
  3. 4.0
  4. <module 'test_refresh' from 'test_refresh.py'>
  5. >>> a.value
  6. 1
  7. >>> a.print_info()
  8. RefreshClass value: 1 ver1.0
复制代码


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

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

  1.  >>> b = tr.RefreshClass()
  2. >>> b.value
  3. 2
  4. >>> b.print_info()
  5. RefreshClass value: 2 ver2.0
复制代码


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

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

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

  1. >>> a.__class__ = tr.RefreshClass
  2. >>> a.value
  3. 1
  4. >>> a.print_info()
  5. RefreshClass value: 1 ver2.0
复制代码


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

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

  1. # edit print_info ver3.0
  2. >>> reload(tr)
  3. 4.0
  4. <module 'test_refresh' from 'test_refresh.py'>
  5. >>> a.print_info.im_func
  6. <function print_info at 0x7f50beeb2c08>
  7. >>> c = tr.RefreshClass()
  8. >>> c.print_info()
  9. RefreshClass value: 3 ver3.0
  10. >>> c.print_info.im_func
  11. <function print_info at 0x7f50beeb2cf8>
  12. >>> a.__class__ = tr.RefreshClass
  13. >>> a.print_info.im_func
  14. <function print_info at 0x7f50beeb2cf8>
  15. >>> a.print_info()
  16. RefreshClass value: 1 ver3.0
复制代码


  触发hotfix

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

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

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

  1. from __future__ import print_function

  2. import os
  3. import time
  4. import refresh_class


  5. rc = refresh_class.RefreshClass()
  6. while True:
  7.     if os.path.exists('refresh.signal'):
  8.         reload(refresh_class)
  9.         rc.__class__ = refresh_class.RefreshClass
  10.     time.sleep(5)
  11.     rc.print_info()
复制代码

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

  4.     def print_info(self):
  5.         print('RefreshClass value: {} ver1.0'.format(self.value))
复制代码


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

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

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

  1. from __future__ import print_function

  2. import time
  3. import signal

  4. import refresh_class

  5. rc = refresh_class.RefreshClass()


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


  9. signal.signal(signal.SIGUSR1, handl_refresh)
  10. while True:
  11.     time.sleep(5)
  12.     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。

原文作者:一根笨茄子  来源:开发者头条

相关帖子

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部