pinpoint-php-aop 內部原理

eeliu發表於2024-08-30

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

相關文章