Python 探针的实现原理
探针的实现主要涉及以下几个知识点:
sys.meta_path
sitecustomize.py
sys.meta_path
sys.meta_path这个简单的来说就是可以实现importhook的功能,
当执行import相关的操作时,会触发sys.meta_path列表中定义的对象。
关于sys.meta_path更详细的资料请查阅python文档中sys.meta_path相关内容以及
PEP0302。
sys.meta_path中的对象需要实现一个find_module方法,
这个find_module方法返回None或一个实现了load_module方法的对象
(代码可以从github上下载part1):
importsys classMetaPathFinder: deffind_module(self,fullname,path=None): print('find_module{}'.format(fullname)) returnMetaPathLoader() classMetaPathLoader: defload_module(self,fullname): print('load_module{}'.format(fullname)) sys.modules[fullname]=sys returnsys sys.meta_path.insert(0,MetaPathFinder()) if__name__=='__main__': importhttp print(http) print(http.version_info)
load_module方法返回一个module对象,这个对象就是import的module对象了。
比如我上面那样就把http替换为sys这个module了。
$pythonmeta_path1.py
find_modulehttp
load_modulehttp
sys.version_info(major=3,minor=5,micro=1,releaselevel='final',serial=0)
通过sys.meta_path我们就可以实现importhook的功能:
当import预定的module时,对这个module里的对象来个狸猫换太子,
从而实现获取函数或方法的执行时间等探测信息。
上面说到了狸猫换太子,那么怎么对一个对象进行狸猫换太子的操作呢?
对于函数对象,我们可以使用装饰器的方式来替换函数对象(代码可以从github上下载part2):
importfunctools importtime deffunc_wrapper(func): @functools.wraps(func) defwrapper(*args,**kwargs): print('startfunc') start=time.time() result=func(*args,**kwargs) end=time.time() print('spent{}s'.format(end-start)) returnresult returnwrapper defsleep(n): time.sleep(n) returnn if__name__=='__main__': func=func_wrapper(sleep) print(func(3))
执行结果:
$pythonfunc_wrapper.py startfunc spent3.004966974258423s 3
下面我们来实现一个计算指定模块的指定函数的执行时间的功能(代码可以从github上下载part3)。
假设我们的模块文件是hello.py:
importtime defsleep(n): time.sleep(n) returnn
我们的importhook是hook.py:
importfunctools importimportlib importsys importtime _hook_modules={'hello'} classMetaPathFinder: deffind_module(self,fullname,path=None): print('find_module{}'.format(fullname)) iffullnamein_hook_modules: returnMetaPathLoader() classMetaPathLoader: defload_module(self,fullname): print('load_module{}'.format(fullname)) #``sys.modules``中保存的是已经导入过的module iffullnameinsys.modules: returnsys.modules[fullname] #先从sys.meta_path中删除自定义的finder #防止下面执行import_module的时候再次触发此finder #从而出现递归调用的问题 finder=sys.meta_path.pop(0) #导入module module=importlib.import_module(fullname) module_hook(fullname,module) sys.meta_path.insert(0,finder) returnmodule sys.meta_path.insert(0,MetaPathFinder()) defmodule_hook(fullname,module): iffullname=='hello': module.sleep=func_wrapper(module.sleep) deffunc_wrapper(func): @functools.wraps(func) defwrapper(*args,**kwargs): print('startfunc') start=time.time() result=func(*args,**kwargs) end=time.time() print('spent{}s'.format(end-start)) returnresult returnwrapper
测试代码:
>>>importhook >>>importhello find_modulehello load_modulehello >>> >>>hello.sleep(3) startfunc spent3.0029919147491455s 3 >>>
其实上面的代码已经实现了探针的基本功能。不过有一个问题就是上面的代码需要显示的
执行importhook操作才会注册上我们定义的hook。
那么有没有办法在启动python解释器的时候自动执行importhook的操作呢?
答案就是可以通过定义sitecustomize.py的方式来实现这个功能。
sitecustomize.py
简单的说就是,python解释器初始化的时候会自动importPYTHONPATH下存在的sitecustomize和usercustomize模块:
实验项目的目录结构如下(代码可以从github上下载part4)
$tree
.
├──sitecustomize.py
└──usercustomize.py
sitecustomize.py:
$catsitecustomize.py
print('thisissitecustomize')
usercustomize.py:
$catusercustomize.py
print('thisisusercustomize')
把当前目录加到PYTHONPATH中,然后看看效果:
$exportPYTHONPATH=. $python thisissitecustomize<---- thisisusercustomize<---- Python3.5.1(default,Dec242015,17:20:27) [GCC4.2.1CompatibleAppleLLVM7.0.2(clang-700.1.81)]ondarwin Type"help","copyright","credits"or"license"formoreinformation. >>>
可以看到确实自动导入了。所以我们可以把之前的探测程序改为支持自动执行importhook(代码可以从github上下载part5)。
目录结构:
$tree
.
├──hello.py
├──hook.py
├──sitecustomize.py
sitecustomize.py:
$catsitecustomize.py importhook
结果:
$exportPYTHONPATH=. $python find_moduleusercustomize Python3.5.1(default,Dec242015,17:20:27) [GCC4.2.1CompatibleAppleLLVM7.0.2(clang-700.1.81)]ondarwin Type"help","copyright","credits"or"license"formoreinformation. find_modulereadline find_moduleatexit find_modulerlcompleter >>> >>>importhello find_modulehello load_modulehello >>> >>>hello.sleep(3) startfunc spent3.005002021789551s 3
不过上面的探测程序其实还有一个问题,那就是需要手动修改PYTHONPATH。用过探针程序的朋友应该会记得,使用newrelic之类的探针只需要执行一条命令就可以了:newrelic-adminrun-programpythonhello.py实际上修改PYTHONPATH的操作是在newrelic-admin这个程序里完成的。
下面我们也要来实现一个类似的命令行程序,就叫agent.py吧。
agent
还是在上一个程序的基础上修改。先调整一个目录结构,把hook操作放到一个单独的目录下,方便设置PYTHONPATH后不会有其他的干扰(代码可以从github上下载part6)。
$mkdirbootstrap $mvhook.pybootstrap/_hook.py $touchbootstrap/__init__.py $touchagent.py $tree . ├──bootstrap │├──__init__.py │├──_hook.py │└──sitecustomize.py ├──hello.py ├──test.py ├──agent.py
bootstrap/sitecustomize.py的内容修改为:
$catbootstrap/sitecustomize.py
import_hook
agent.py的内容如下:
<spanclass="kn">import</span><spanclass="nn">os</span> <spanclass="kn">import</span><spanclass="nn">sys</span> <spanclass="n">current_dir</span><spanclass="o">=</span><spanclass="n">os</span><spanclass="o">.</span><spanclass="n">path</span><spanclass="o">.</span><spanclass="n">dirname</span><spanclass="p">(</span><spanclass="n">os</span><spanclass="o">.</span><spanclass="n">path</span><spanclass="o">.</span><spanclass="n">realpath</span><spanclass="p">(</span><spanclass="n">__file__</span><spanclass="p">))</span> <spanclass="n">boot_dir</span><spanclass="o">=</span><spanclass="n">os</span><spanclass="o">.</span><spanclass="n">path</span><spanclass="o">.</span><spanclass="n">join</span><spanclass="p">(</span><spanclass="n">current_dir</span><spanclass="p">,</span><spanclass="s">'bootstrap'</span><spanclass="p">)</span> <spanclass="k">def</span><spanclass="nf">main</span><spanclass="p">():</span> <spanclass="n">args</span><spanclass="o">=</span><spanclass="n">sys</span><spanclass="o">.</span><spanclass="n">argv</span><spanclass="p">[</span><spanclass="mi">1</span><spanclass="p">:]</span> <spanclass="n">os</span><spanclass="o">.</span><spanclass="n">environ</span><spanclass="p">[</span><spanclass="s">'PYTHONPATH'</span><spanclass="p">]</span><spanclass="o">=</span><spanclass="n">boot_dir</span> <spanclass="c">#执行后面的python程序命令</span> <spanclass="c">#sys.executable是python解释器程序的绝对路径``whichpython``</span> <spanclass="c">#>>>sys.executable</span> <spanclass="c">#'/usr/local/var/pyenv/versions/3.5.1/bin/python3.5'</span> <spanclass="n">os</span><spanclass="o">.</span><spanclass="n">execl</span><spanclass="p">(</span><spanclass="n">sys</span><spanclass="o">.</span><spanclass="n">executable</span><spanclass="p">,</span><spanclass="n">sys</span><spanclass="o">.</span><spanclass="n">executable</span><spanclass="p">,</span><spanclass="o">*</span><spanclass="n">args</span><spanclass="p">)</span> <spanclass="k">if</span><spanclass="n">__name__</span><spanclass="o">==</span><spanclass="s">'__main__'</span><spanclass="p">:</span> <spanclass="n">main</span><spanclass="p">()</span>
test.py的内容为:
$cattest.py importsys importhello print(sys.argv) print(hello.sleep(3))
使用方法:
$pythonagent.pytest.pyarg1arg2 find_moduleusercustomize find_modulehello load_modulehello ['test.py','arg1','arg2'] startfunc spent3.005035161972046s 3
至此,我们就实现了一个简单的python探针程序。当然,跟实际使用的探针程序相比肯定是有很大的差距的,这篇文章主要是讲解一下探针背后的实现原理。
如果大家对商用探针程序的具体实现感兴趣的话,可以看一下国外的NewRelic或国内的OneAPM,TingYun等这些APM厂商的商用python探针的源代码,相信你会发现一些很有趣的事情。