Python 探针的实现原理


Posted in Python onApril 23, 2016

探针的实现主要涉及以下几个知识点:

sys.meta_path
sitecustomize.py
sys.meta_path

sys.meta_path 这个简单的来说就是可以实现 import hook 的功能,
当执行 import 相关的操作时,会触发 sys.meta_path 列表中定义的对象。
关于 sys.meta_path 更详细的资料请查阅 python 文档中 sys.meta_path 相关内容以及
PEP 0302 。

sys.meta_path 中的对象需要实现一个 find_module 方法,
这个 find_module 方法返回 None 或一个实现了 load_module 方法的对象
(代码可以从 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 方法返回一个 module 对象,这个对象就是 import 的 module 对象了。
比如我上面那样就把 http 替换为 sys 这个 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 我们就可以实现 import hook 的功能:
当 import 预定的 module 时,对这个 module 里的对象来个狸猫换太子,
从而实现获取函数或方法的执行时间等探测信息。

上面说到了狸猫换太子,那么怎么对一个对象进行狸猫换太子的操作呢?
对于函数对象,我们可以使用装饰器的方式来替换函数对象(代码可以从 github 上下载 part2) :

import functools
import time
 
def func_wrapper(func):
  @functools.wraps(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))

执行结果:

$ python func_wrapper.py
start func
spent 3.004966974258423s
3

下面我们来实现一个计算指定模块的指定函数的执行时间的功能(代码可以从 github 上下载 part3) 。

假设我们的模块文件是 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`` 中保存的是已经导入过的 module
    if fullname in sys.modules:
      return sys.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)
    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):
  @functools.wraps(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

测试代码:

>>> import hook
>>> import hello
find_module hello
load_module hello
>>>
>>> hello.sleep(3)
start func
spent 3.0029919147491455s
3
>>>

其实上面的代码已经实现了探针的基本功能。不过有一个问题就是上面的代码需要显示的
执行 import hook 操作才会注册上我们定义的 hook。

那么有没有办法在启动 python 解释器的时候自动执行 import hook 的操作呢?
答案就是可以通过定义 sitecustomize.py 的方式来实现这个功能。

sitecustomize.py
简单的说就是,python 解释器初始化的时候会自动 import PYTHONPATH 下存在的 sitecustomize 和 usercustomize 模块:

实验项目的目录结构如下(代码可以从 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')
把当前目录加到 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.
>>>

可以看到确实自动导入了。所以我们可以把之前的探测程序改为支持自动执行 import hook (代码可以从 github 上下载part5) 。

目录结构:

$ tree
.
├── hello.py
├── hook.py
├── sitecustomize.py
sitecustomize.py:

$ cat sitecustomize.py
import hook

结果:

$ 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

不过上面的探测程序其实还有一个问题,那就是需要手动修改 PYTHONPATH 。 用过探针程序的朋友应该会记得, 使用 newrelic 之类的探针只需要执行一条命令就 可以了: newrelic-admin run-program python hello.py 实际上修改PYTHONPATH 的操作是在 newrelic-admin 这个程序里完成的。

下面我们也要来实现一个类似的命令行程序,就叫 agent.py 吧。

agent
还是在上一个程序的基础上修改。先调整一个目录结构,把 hook 操作放到一个单独的目录下, 方便设置 PYTHONPATH后不会有其他的干扰(代码可以从 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 的内容修改为:

$ cat bootstrap/sitecustomize.py
import _hook
agent.py 的内容如下:

<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">import</span> <span class="nn">sys</span>
 
<span class="n">current_dir</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">realpath</span><span class="p">(</span><span class="n">__file__</span><span class="p">))</span>
<span class="n">boot_dir</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">current_dir</span><span class="p">,</span> <span class="s">'bootstrap'</span><span class="p">)</span>
 
<span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
  <span class="n">args</span> <span class="o">=</span> <span class="n">sys</span><span class="o">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
  <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s">'PYTHONPATH'</span><span class="p">]</span> <span class="o">=</span> <span class="n">boot_dir</span>
  <span class="c"># 执行后面的 python 程序命令</span>
  <span class="c"># sys.executable 是 python 解释器程序的绝对路径 ``which python``</span>
  <span class="c"># >>> sys.executable</span>
  <span class="c"># '/usr/local/var/pyenv/versions/3.5.1/bin/python3.5'</span>
  <span class="n">os</span><span class="o">.</span><span class="n">execl</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">executable</span><span class="p">,</span> <span class="n">sys</span><span class="o">.</span><span class="n">executable</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">)</span>
 
<span class="k">if</span> <span class="n">__name__</span> <span class="o">==</span> <span class="s">'__main__'</span><span class="p">:</span>
  <span class="n">main</span><span class="p">()</span>

test.py 的内容为:

$ 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

至此,我们就实现了一个简单的 python 探针程序。当然,跟实际使用的探针程序相比肯定是有 很大的差距的,这篇文章主要是讲解一下探针背后的实现原理。

如果大家对商用探针程序的具体实现感兴趣的话,可以看一下国外的 New Relic 或国内的 OneAPM, TingYun 等这些 APM 厂商的商用 python 探针的源代码,相信你会发现一些很有趣的事情。

Python 相关文章推荐
Python程序设计入门(2)变量类型简介
Jun 16 Python
Python实现单词拼写检查
Apr 25 Python
详解Django缓存处理中Vary头部的使用
Jul 24 Python
Python使用SQLite和Excel操作进行数据分析
Jan 20 Python
TensorFlow实现RNN循环神经网络
Feb 28 Python
Python3.6简单反射操作示例
Jun 14 Python
Linux下python与C++使用dlib实现人脸检测
Jun 29 Python
ipython和python区别详解
Jun 26 Python
使用python 写一个静态服务(实战)
Jun 28 Python
使用Python和OpenCV检测图像中的物体并将物体裁剪下来
Oct 30 Python
Python读写csv文件流程及异常解决
Oct 20 Python
分享7个 Python 实战项目练习
Mar 03 Python
一键搞定python连接mysql驱动有关问题(windows版本)
Apr 23 #Python
Linux 发邮件磁盘空间监控(python)
Apr 23 #Python
web.py 十分钟创建简易博客实现代码
Apr 22 #Python
在windows下快速搭建web.py开发框架方法
Apr 22 #Python
基于python实现的抓取腾讯视频所有电影的爬虫
Apr 22 #Python
Python开发之快速搭建自动回复微信公众号功能
Apr 22 #Python
Django小白教程之Django用户注册与登录
Apr 22 #Python
You might like
用php实现的获取网页中的图片并保存到本地的代码
2010/01/05 PHP
PHP获取文件的MD5值并判断是否被修改的例子
2014/06/19 PHP
thinkphp区间查询、统计查询与SQL直接查询实例分析
2014/11/24 PHP
php使用Cookie实现和用户会话的方法
2015/01/21 PHP
php实现简单爬虫的开发
2016/03/28 PHP
thinkPHP+phpexcel实现excel报表输出功能示例
2017/06/06 PHP
PHP使用redis位图bitMap 实现签到功能
2019/10/08 PHP
jquery复选框CHECKBOX全选、反选
2008/08/30 Javascript
Cookie 注入是怎样产生的
2009/04/08 Javascript
JavaScript中“+”的陷阱深刻理解
2012/12/04 Javascript
用js设置下拉框为只读的小技巧
2014/04/10 Javascript
深入理解JavaScript系列(19):求值策略(Evaluation strategy)详解
2015/03/05 Javascript
Jquery中的$.each获取各种返回类型数据的使用方法
2015/05/03 Javascript
JavaScript中的this关键字使用详解
2015/08/14 Javascript
纯javascript实现图片延时加载方法
2015/08/21 Javascript
js 判断所选时间(或者当前时间)是否在某一时间段的实现代码
2015/09/05 Javascript
Javascript实现单例模式
2016/01/24 Javascript
vue-cli入门之项目结构分析
2017/04/20 Javascript
使用Nodejs连接mongodb数据库的实现代码
2017/08/21 NodeJs
JavaScript常用数学函数用法示例
2018/05/14 Javascript
VUE项目中加载已保存的笔记实例方法
2019/09/14 Javascript
微信小程序返回上一页传参并刷新过程解析
2019/12/13 Javascript
[48:41]VP vs VG Supermajor小组赛 B组胜者组决赛 BO3 第二场 6.2
2018/06/03 DOTA
Python实现抢购IPhone手机
2018/02/07 Python
Python基于pyCUDA实现GPU加速并行计算功能入门教程
2018/06/19 Python
python使用xlrd和xlwt读写Excel文件的实例代码
2018/09/05 Python
django+echart绘制曲线图的方法示例
2018/11/26 Python
Python使用reportlab模块生成PDF格式的文档
2019/03/11 Python
检测python爬虫时是否代理ip伪装成功的方法
2019/07/12 Python
Django Path转换器自定义及正则代码实例
2020/05/29 Python
在python中list作函数形参,防止被实参修改的实现方法
2020/06/05 Python
Foot Locker澳洲官网:美国运动服和鞋类零售商
2019/10/11 全球购物
SOA面试题:如何在SOA中实现松耦合
2013/07/21 面试题
委托证明模板
2014/09/16 职场文书
公司承诺函范文
2015/01/21 职场文书
python中opencv实现图片文本倾斜校正
2021/06/11 Python