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使用urllib2提交http post请求的方法
May 26 Python
浅析Python中元祖、列表和字典的区别
Aug 17 Python
TensorFlow打印tensor值的实现方法
Jul 27 Python
使用PM2+nginx部署python项目的方法示例
Nov 07 Python
Python文件常见操作实例分析【读写、遍历】
Dec 10 Python
Python二叉搜索树与双向链表转换算法示例
Mar 02 Python
python代码编写计算器小程序
Mar 30 Python
Python集合操作方法详解
Feb 09 Python
对python中各个response的使用说明
Mar 28 Python
Python使用pyyaml模块处理yaml数据
Apr 14 Python
python使用dlib进行人脸检测和关键点的示例
Dec 05 Python
Python基于Tkinter开发一个爬取B站直播弹幕的工具
May 06 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 遍历XP文件夹下所有文件
2008/11/27 PHP
PHP 抓取网页图片并且另存为的实现代码
2010/03/24 PHP
php file_put_contents()功能函数(集成了fopen、fwrite、fclose)
2011/05/24 PHP
PHP生成等比缩略图类和自定义函数分享
2014/06/25 PHP
ci检测是ajax还是页面post提交数据的方法
2014/11/10 PHP
PHP自定义函数格式化json数据示例
2016/09/14 PHP
php实现用户登陆简单实例
2017/04/04 PHP
php探针不显示内存解决方法
2019/09/17 PHP
jQuery 使用手册(五)
2009/09/23 Javascript
多浏览器兼容的获取元素和鼠标的位置的js代码
2009/12/15 Javascript
kmock javascript 单元测试代码
2011/02/06 Javascript
jquery获取特定name所有选中的checkbox,支持IE9标准模式
2013/03/18 Javascript
javascript中怎么做对象的类型判断
2013/11/11 Javascript
使用AngularJS处理单选框和复选框的简单方法
2015/06/19 Javascript
javascript的 {} 语句块详解
2016/02/27 Javascript
js获取元素的标签名实现方法
2016/10/08 Javascript
浅谈vue-lazyload实现的详细过程
2017/08/22 Javascript
template.js前端模板引擎使用详解
2017/10/10 Javascript
AngularJS中scope的绑定策略实例分析
2017/10/30 Javascript
vue使用echarts图表的详细方法
2018/10/22 Javascript
vue-better-scroll 的使用实例代码详解
2018/12/03 Javascript
微信小程序通过js实现瀑布流布局详解
2019/08/28 Javascript
简单了解Vue computed属性及watch区别
2020/07/10 Javascript
LRUCache的实现原理及利用python实现的方法
2017/11/21 Python
python复制文件到指定目录的实例
2018/04/27 Python
深入解析神经网络从原理到实现
2019/07/26 Python
Python中文分词库jieba,pkusegwg性能准确度比较
2020/02/11 Python
Python识别html主要文本框过程解析
2020/02/18 Python
Python 调用 ES、Solr、Phoenix的示例代码
2020/11/23 Python
Sport-Thieme荷兰:购买体育用品
2019/08/25 全球购物
德国BA保镖药房中文网:Bodyguard Apotheke
2021/03/09 全球购物
上海期货面试题
2014/01/31 面试题
高校毕业生登记表自我鉴定
2013/11/03 职场文书
中学运动会广播稿
2014/01/19 职场文书
医生学习党的群众路线教育实践活动心得体会
2014/11/03 职场文书
校运会宣传稿大全
2015/07/23 职场文书