非常干貨:Python 探針實(shí)現(xiàn)原理

大家好,我是貓哥。
關(guān)于 Python 中探針的運(yùn)用,我之前寫過一篇《由淺入深:Python 中如何實(shí)現(xiàn)自動(dòng)導(dǎo)入缺失的庫?》,最近看到一篇文章專門寫這個(gè)內(nèi)容,特分享一下~~
本文呢,將簡(jiǎn)單講述一下 Python 探針的實(shí)現(xiàn)原理。同時(shí)為了驗(yàn)證這個(gè)原理,我們也會(huì)一起來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的統(tǒng)計(jì)指定函數(shù)執(zhí)行時(shí)間的探針程序。
探針的實(shí)現(xiàn)主要涉及以下幾個(gè)知識(shí)點(diǎn):
sys.meta_path sitecustomize.py
sys.meta_path
sys.meta_path 這個(gè)簡(jiǎn)單的來說就是可以實(shí)現(xiàn) import hook 的功能, 當(dāng)執(zhí)行 import 相關(guān)的操作時(shí),會(huì)觸發(fā) sys.meta_path 列表中定義的對(duì)象。關(guān)于 sys.meta_path 更詳細(xì)的資料請(qǐng)查閱 python 文檔中 sys.meta_path 相關(guān)內(nèi)容以及 PEP 0302 。
sys.meta_path 中的對(duì)象需要實(shí)現(xiàn)一個(gè) find_module 方法, 這個(gè) find_module 方法返回 None 或一個(gè)實(shí)現(xiàn)了 load_module 方法的對(duì)象 (代碼可以從 github 上下載 part1_) :
import?sys
class?MetaPathFinder:
????def?find_module(self,?fullname,?path=None):
????????print('find_module?{}'.format(fullname))
????????return?MetaPathLoader()
class?MetaPathLoader:
????def?load_module(self,?fullname):
????????print('load_module?{}'.format(fullname))
????????sys.modules[fullname]?=?sys
????????return?sys
sys.meta_path.insert(0,?MetaPathFinder())
if?__name__?==?'__main__':
????import?http
????print(http)
????print(http.version_info)
load_module 方法返回一個(gè) module 對(duì)象,這個(gè)對(duì)象就是 import 的 module 對(duì)象了。比如我上面那樣就把 http 替換為 sys 這個(gè) module 了。
$?python?meta_path1.py
find_module?http
load_module?http
sys.version_info(major=3,?minor=5,?micro=1,?releaselevel='final',?serial=0)
通過 sys.meta_path 我們就可以實(shí)現(xiàn) import hook 的功能:當(dāng) import 預(yù)定的 module 時(shí),對(duì)這個(gè) module 里的對(duì)象來個(gè)貍貓換太子, 從而實(shí)現(xiàn)獲取函數(shù)或方法的執(zhí)行時(shí)間等探測(cè)信息。
上面說到了貍貓換太子,那么怎么對(duì)一個(gè)對(duì)象進(jìn)行貍貓換太子的操作呢?對(duì)于函數(shù)對(duì)象,我們可以使用裝飾器的方式來替換函數(shù)對(duì)象(代碼可以從 github 上下載 part2) :
import?functools
import?time
def?func_wrapper(func):
[email protected](func)
????def?wrapper(*args,?**kwargs):
????????print('start?func')
????????start?=?time.time()
????????result?=?func(*args,?**kwargs)
????????end?=?time.time()
????????print('spent?{}s'.format(end?-?start))
????????return?result
????return?wrapper
def?sleep(n):
????time.sleep(n)
????return?n
if?__name__?==?'__main__':
????func?=?func_wrapper(sleep)
????print(func(3))
執(zhí)行結(jié)果:
$?python?func_wrapper.py
start?func
spent?3.004966974258423s
3
下面我們來實(shí)現(xiàn)一個(gè)計(jì)算指定模塊的指定函數(shù)的執(zhí)行時(shí)間的功能(代碼可以從 github 上下載 part3) 。
假設(shè)我們的模塊文件是 hello.py:
import?time
def?sleep(n):
????time.sleep(n)
????return?n
我們的 import hook 是 hook.py:
import?functools
import?importlib
import?sys
import?time
_hook_modules?=?{'hello'}
class?MetaPathFinder:
????def?find_module(self,?fullname,?path=None):
????????print('find_module?{}'.format(fullname))
????????if?fullname?in?_hook_modules:
????????????return?MetaPathLoader()
class?MetaPathLoader:
????def?load_module(self,?fullname):
????????print('load_module?{}'.format(fullname))
????????#?``sys.modules``?中保存的是已經(jīng)導(dǎo)入過的?module
????????if?fullname?in?sys.modules:
????????????return?sys.modules[fullname]
????????#?先從?sys.meta_path?中刪除自定義的?finder
????????#?防止下面執(zhí)行?import_module?的時(shí)候再次觸發(fā)此?finder
????????#?從而出現(xiàn)遞歸調(diào)用的問題
????????finder?=?sys.meta_path.pop(0)
????????#?導(dǎo)入?module
????????module?=?importlib.import_module(fullname)
????????module_hook(fullname,?module)
????????sys.meta_path.insert(0,?finder)
????????return?module
sys.meta_path.insert(0,?MetaPathFinder())
def?module_hook(fullname,?module):
????if?fullname?==?'hello':
????????module.sleep?=?func_wrapper(module.sleep)
def?func_wrapper(func):
[email protected](func)
????def?wrapper(*args,?**kwargs):
????????print('start?func')
????????start?=?time.time()
????????result?=?func(*args,?**kwargs)
????????end?=?time.time()
????????print('spent?{}s'.format(end?-?start))
????????return?result
????return?wrapper
測(cè)試代碼:
>>>?import?hook
>>>?import?hello
find_module?hello
load_module?hello
>>>
>>>?hello.sleep(3)
start?func
spent?3.0029919147491455s
3
>>>
其實(shí)上面的代碼已經(jīng)實(shí)現(xiàn)了探針的基本功能。不過有一個(gè)問題就是上面的代碼需要顯示的 執(zhí)行 import hook 操作才會(huì)注冊(cè)上我們定義的 hook。
那么有沒有辦法在啟動(dòng) python 解釋器的時(shí)候自動(dòng)執(zhí)行 import hook 的操作呢?答案就是可以通過定義 sitecustomize.py 的方式來實(shí)現(xiàn)這個(gè)功能。
sitecustomize.py
簡(jiǎn)單的說就是,python 解釋器初始化的時(shí)候會(huì)自動(dòng) import PYTHONPATH 下存在的 sitecustomize 和 usercustomize 模塊:
實(shí)驗(yàn)項(xiàng)目的目錄結(jié)構(gòu)如下(代碼可以從 github 上下載 part4) :
$?tree
.
├──?sitecustomize.py
└──?usercustomize.py
sitecustomize.py:
$?cat?sitecustomize.py
print('this?is?sitecustomize')
usercustomize.py:
$?cat?usercustomize.py
print('this?is?usercustomize')
把當(dāng)前目錄加到 PYTHONPATH 中,然后看看效果:
$?export?PYTHONPATH=.
$?python
this?is?sitecustomize???????<----
this?is?usercustomize???????<----
Python?3.5.1?(default,?Dec?24?2015,?17:20:27)
[GCC?4.2.1?Compatible?Apple?LLVM?7.0.2?(clang-700.1.81)]?on?darwin
Type?"help",?"copyright",?"credits"?or?"license"?for?more?information.
>>>
可以看到確實(shí)自動(dòng)導(dǎo)入了。所以我們可以把之前的探測(cè)程序改為支持自動(dòng)執(zhí)行 import hook (代碼可以從 github 上下載 part5) 。
目錄結(jié)構(gòu):
$?tree
.
├──?hello.py
├──?hook.py
├──?sitecustomize.py
sitecustomize.py:
$?cat?sitecustomize.py
import?hook
結(jié)果:
$?export?PYTHONPATH=.
$?python
find_module?usercustomize
Python?3.5.1?(default,?Dec?24?2015,?17:20:27)
[GCC?4.2.1?Compatible?Apple?LLVM?7.0.2?(clang-700.1.81)]?on?darwin
Type?"help",?"copyright",?"credits"?or?"license"?for?more?information.
find_module?readline
find_module?atexit
find_module?rlcompleter
>>>
>>>?import?hello
find_module?hello
load_module?hello
>>>
>>>?hello.sleep(3)
start?func
spent?3.005002021789551s
3
不過上面的探測(cè)程序其實(shí)還有一個(gè)問題,那就是需要手動(dòng)修改 PYTHONPATH 。用過探針程序的朋友應(yīng)該會(huì)記得, 使用 newrelic 之類的探針只需要執(zhí)行一條命令就 可以了:newrelic-admin run-program python hello.py 實(shí)際上修改 PYTHONPATH 的操作是在newrelic-admin 這個(gè)程序里完成的。
下面我們也要來實(shí)現(xiàn)一個(gè)類似的命令行程序,就叫 agent.py 吧。
agent
還是在上一個(gè)程序的基礎(chǔ)上修改。先調(diào)整一個(gè)目錄結(jié)構(gòu),把 hook 操作放到一個(gè)單獨(dú)的目錄下, 方便設(shè)置 PYTHONPATH 后不會(huì)有其他的干擾(代碼可以從 github 上下載 part6 )。
$?mkdir?bootstrap
$?mv?hook.py?bootstrap/_hook.py
$?touch?bootstrap/__init__.py
$?touch?agent.py
$?tree
.
├──?bootstrap
│???├──?__init__.py
│???├──?_hook.py
│???└──?sitecustomize.py
├──?hello.py
├──?test.py
├──?agent.py
bootstrap/sitecustomize.py 的內(nèi)容修改為:
$?cat?bootstrap/sitecustomize.py
import?_hook
agent.py 的內(nèi)容如下:
import?os
import?sys
current_dir?=?os.path.dirname(os.path.realpath(__file__))
boot_dir?=?os.path.join(current_dir,?'bootstrap')
def?main():
????args?=?sys.argv[1:]
????os.environ['PYTHONPATH']?=?boot_dir
????#?執(zhí)行后面的?python?程序命令
????#?sys.executable?是?python?解釋器程序的絕對(duì)路徑?``which?python``
????#?>>>?sys.executable
????#?'/usr/local/var/pyenv/versions/3.5.1/bin/python3.5'
????os.execl(sys.executable,?sys.executable,?*args)
if?__name__?==?'__main__':
????main()
test.py 的內(nèi)容為:
$?cat?test.py
import?sys
import?hello
print(sys.argv)
print(hello.sleep(3))
使用方法:
$?python?agent.py?test.py?arg1?arg2
find_module?usercustomize
find_module?hello
load_module?hello
['test.py',?'arg1',?'arg2']
start?func
spent?3.005035161972046s
3
至此,我們就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的 python 探針程序。當(dāng)然,跟實(shí)際使用的探針程序相比肯定是有 很大的差距的,這篇文章主要是講解一下探針背后的實(shí)現(xiàn)原理。
文中的代碼:https://github.com/mozillazg/apm-python-agent-principle

近期熱門文章推薦:

