探究如何給Python程序做hotfix
使用Python來寫服務器端程序,很大的一個優勢就是可以進行熱更新,即在不停機的情況下,使改動后的程序生效。在開發階段,這個功能可以大大提高開發效率(寫代碼–啟動服務器–看效果–改代碼–hotfix–看效果–提交~);而在生產環境中,可以以最小的代價(不停機)修復線上的bug。
我在項目中使用hotfix功能很長世間了,大概了解它是利用了Python的import/reload功能,但是并沒有去自己研究過。也覺得應該研究一下Python的hotfix機制,畢竟是跟了自己這么久的小伙伴嘛。
import
說到hotfix就要從import語句說起。
首先建立這樣一個簡單的文件用作測試。
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_refreshas tr
1.0
>>> import test_refreshas tr
>>>> # edit version=2.0
>>> import test_refreshas tr
>>> tr.version
1.0
重新import一個已經import過的模塊,并不會重新執行文件(第二個import之后沒有輸出)。后面修改源文件并重新import后,對內存中tr.version的檢查也驗證了這一點。
為了能夠重新加載修改后的源文件,我們需要明確的告訴Python解釋器這一點。在Python中,sys.modules保存了已經加載過的模塊。所以
>>> del sys.modules['test_refresh']
>>> import test_refreshas 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的交互式命令行中,嘗試動態改變一個類的行為邏輯。
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()
RefreshClassvalue: 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()
RefreshClassvalue: 1 ver1.0
輸出并沒有如預期一樣輸出ver2.0……
那我們重新創建一個對象試試。
>>> b = tr.RefreshClass()
>>> b.value
2
>>> b.print_info()
RefreshClassvalue: 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()
RefreshClassvalue: 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
<functionprint_infoat 0x7f50beeb2c08>
>>> c = tr.RefreshClass()
>>> c.print_info()
RefreshClassvalue: 3 ver3.0
>>> c.print_info.im_func
<functionprint_infoat 0x7f50beeb2cf8>
>>> a.__class__ = tr.RefreshClass
>>> a.print_info.im_func
<functionprint_infoat 0x7f50beeb2cf8>
>>> a.print_info()
RefreshClassvalue: 1 ver3.0
觸發hotfix
上面的操作都是在Python的交互式解釋器中運行的。下面我們將嘗試使一個運行中的Python程序進行熱更新。
這里遇到一個問題:作為Python程序入口的那個文件,不是以module的形式存在的,因此不能用上面的方式進行hotfix。所以我們需要保持入口文件的盡量簡潔,而將絕大多數的邏輯功能交給其他的模塊執行。
要觸發一個正在運行中的Python程序進行熱更新,我們需要有一種方式和Python程序通信。直接使用OS的標識文件是一個簡單易行的方法。
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程序通信。
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等互相嵌套的情況),不過只要實現完整后,就是一勞永逸的事情了。
來自:http://python.jobbole.com/87106/