pinpoint-php-aop 内部原理

pinpoint-php-aop 是一个支持pinpoint-php agent 的库

  1. 自动注入PHP内置函数,比如redis,pdo,mysqli
  2. 自动注入用户类,比如 guzzlehttp, predis

怎样处理内置函数

内置函数解释:

    PHP comes standard with many functions and constructs. There are also functions that require specific 
    PHP extensions compiled in, otherwise fatal "undefined function" errors will appear. For example, to 
    use image functions such as imagecreatetruecolor(), PHP must be compiled with GD support. Or, to use 
    mysqli_connect(), PHP must be compiled with MySQLi support. There are many core functions that are 
    included in every version of PHP, such as the string and variable functions. A call to phpinfo() 
    or get_loaded_extensions() will show which extensions are loaded into PHP. Also note that many 
    extensions are enabled by default and that the PHP manual is split up by extension. ...

> https://www.php.net/manual/en/functions.internal.php#functions.internal
 

通过修改PHP内核中 CG(class_table)

Inspired by https://www.phpinternalsbook.com/php7/extensions_design/hooks.html#overwriting-an-internal-function

PHP内核中提供了全局的 class_table,用户可以可以用来替换原始的函数,然后达到包装该函数的目的:比如插入一些安全的插件代码。

步骤

  1. ext_pinpoint-php 提供内置函数替换功能
// https://github.com/pinpoint-apm/pinpoint-c-agent/blob/9c544f139665dde3a9cee2a244a9c3be2f32bff9/src/PHP/pinpoint_php.cpp#L887
zend_function *func = (zend_function *)zend_hash_str_find_ptr(
      CG(function_table), ZSTR_VAL(name), ZSTR_LEN(name));
  if (func != NULL &&
      func->internal_function.handler == pinpoint_interceptor_handler_entry) {
    pp_trace("function `%s` interceptor already added", ZSTR_VAL(name));
  } else if (func != NULL) {
    pp_interceptor_v_t *interceptor =
        make_interceptor(name, before, end, exception, func);
    // insert into hash
    if (!zend_hash_add_ptr(PPG(interceptors), name, interceptor)) {
      free_interceptor(interceptor);
      pp_trace("added interceptor on `function`: %s failed. reason: already "
               "exist ",
               ZSTR_VAL(name));
      return;
    }
    func->internal_function.handler = pinpoint_interceptor_handler_entry;
    pp_trace("added interceptor on `function`: %s success", ZSTR_VAL(name));
 
  1. 基于第一步的功能,在插入点添加pinpoint的业务逻辑插件
// https://github.com/pinpoint-apm/pinpoint-php-aop/blob/5994253869d516c38d528a8ef784a5c1c18b20f3/lib/Pinpoint/Plugins/SysV2/_curl/curl.php#L78
pinpoint_join_cut(
    ["curl_close"],
    function ($ch) use (&$ch_res) {
        unset($ch_res[(int) $ch]);
        pinpoint_start_trace();
        pinpoint_add_clue(PP_INTERCEPTOR_NAME, "curl_close");
        pinpoint_add_clue(PP_SERVER_TYPE, PP_PHP_METHOD);
    },
    function ($ret) {
        pinpoint_end_trace();
    },
    function ($e) {
    }
);
 
  1. 根据需要,启用插件
// https://github.com/pinpoint-apm/pinpoint-php-aop/blob/5994253869d516c38d528a8ef784a5c1c18b20f3/lib/Pinpoint/Plugins/PinpointPerRequestPlugins.php#L126C12-L126C58
if(sampled){
    require_once __DIR__ . "/SysV2/__init__.php";
}else{
    require_once __DIR__ . "/SysV2/_curl/__init__.php";
}
 

怎样处理用户定义的类

在此之前,你需要了解类加载器

By registering autoloaders, PHP is given a last chance to load the class or interface before it fails with an error.
> https://www.php.net/manual/en/language.oop5.autoload.php 
 

对于PHP,当用户通过 use 等来加载类或者函数的时候,内核会检查这个类是否已经被加载。如果没有,它就会调用auto_loader去调用对应的文件。pinpoint-php-aop 也就是在这个时候开始拦截类的。

  1. 当PHP的类加载器初始完成后,pinpoint-php-aop 的类加载器会拦截所有的加载的类和函数。当发现一个需要被拦截的类被加载的时候,它会把这个类指向一个添加了pinpoint插件的类。

  2. 当pinpoint 加载器发现这个文件没有被pinpoint的插件拦截,它就会生成一个添加了pinpoint插件的类,然后注册到类加载器里面。更重要的是,这些类会被缓存到cache_dir中,当后续的请求到来的时候,这些类文件会被重新使用。这样的好处是,可以节约很多请求时间。

ast_loader

可能有些晕 🥴 我用一个例子 Pinpoint\Plugins\autoload\_MongoPlugin 再来介绍一个整个流程。

步骤

  1. 比如项目里使用了mongodb client
//https://github.com/pinpoint-apm/pinpoint-c-agent/blob/9c544f139665dde3a9cee2a244a9c3be2f32bff9/testapps/SimplePHP/run.php#L92-L93
 $client = new MongoDB\Client("mongodb://$mongodb_host:27017");
 
  1. 通过pinpoint-php-aop 提供的函数,注册拦截 MongoDB\Client类中的__construct方法和对应的插件
//https://github.com/pinpoint-apm/pinpoint-php-aop/blob/5994253869d516c38d528a8ef784a5c1c18b20f3/lib/Pinpoint/Plugins/autoload/_MongoPlugin/__init__.php#L25
$classHandler = new AspectClassHandle(\MongoDB\Client::class);
$classHandler->addJoinPoint('__construct', MongoPlugin::class);
$cls[] = $classHandler;
 
  1. 当pinpoint完成初始化后,你会在这个路径 /tmp/.cache/__class_index.php 找到一个文件

default cache directory is /tmp/

$pinpoint_class_map = array('MongoDB\\Client' => '/tmp/.cache/MongoDB/Client.php', ...);
return $pinpoint_class_map;
 

里面还有加入了pinpoint 插件后的类的文件

//Client.php
namespace MongoDB;
class Client{
...
    // origin methods
    public function __pinpoint____construct(?string $uri = null, array $uriOptions = [], array $driverOptions = [])
    {

    }
    // rendered methods 
    public function __construct(?string $uri = null, array $uriOptions = [], array $driverOptions = [])
    {
        $_pinpoint___construct_var = new \Pinpoint\Plugins\autoload\_MongoPlugin\MongoPlugin(__METHOD__, $this, $uri, $uriOptions, $driverOptions);
        try {
            $_pinpoint___construct_var->onBefore();
            $this->__pinpoint____construct($uri, $uriOptions, $driverOptions);
            $_pinpoint___construct_var->onEnd($_pinpoint___construct_ret);
        } catch (\Exception $e) {
            $_pinpoint___construct_var->onException($e);
            throw $e;
        }
    }
...
}
 
  1. 当上面的类(client.php)被加载到PHP的内核后,就意味着pinpoint 插件已经在你的项目里面正常工作了

i.e. 到此,用户自定义类也被加载pinpoint拦截成功了。

 

From https://github.com/pinpoint-apm/pinpoint-php-aop/wiki/pinpoint-php-aop-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86

posted @ 2024-08-30 14:10  eeliu  阅读(18)  评论(1编辑  收藏  举报