一、总体流程
PHP 重要几个生命周期说明,先后顺序为 PHP_MI、PHP_RI、PHP_EXECUTE、PHP_RS、PHP_MS,每个模块的作用如下
- PHP_MI,模块初始化阶段,主要进行 PHP 框架、Zend 引擎的初始化工作。重要的几个工作如下:
- 全局状态信息的初始化,如 SG、CG、EG等
- 启动 Zend 引擎,内存池启动、注册虚拟机的各项执行句柄
- 解析 php.ini 配置文件
- 注册拓展,包括静态编译拓展和动态编译拓展
- 回调拓展定义的 MI 函数,即
PHP_MINIT_FUNCTION
- PHP_RI,请求初始化阶段,CLI 模式下,该函数执行一次。如果是 php-fpm 模式下,会在 PHP_RI 和 PHP_RS 之间循环。该阶段需要关注的有如下事宜:
- 激活 zend 引擎,包括:重置垃圾回收器、初始化编译器、初始化执行器、初始化词法扫描器
- 回调各拓展定义的 RI 函数,即
PHP_RINIT_FUNCTION
- PHP_EXECUTE,脚本执行阶段。通过拦截 zend 引擎的 execute 函数,我们可以捕获用户执行的每一条语句,我们可以在此阶段,进行 MySQL、Redis、CURL等代码的捕获,从而生成对应的 span,进而构建 trace。
- PHP_RS,请求关闭阶段。该阶段主要进行请求资源的释放动作,同时这个是 fpm 请求的最后一个阶段,我们可以在此阶段,把本次请求获取的 segment 信息上报至 sidecar
- PHP_MS,模块关闭阶段。各项资源的释放
SkyWalking PHP 内核的主要处理流程整理如下:
二、PHP_MI 阶段,自定义函数执行器替换Zend内核执行器
该阶段的入口函数 PHP_MINIT_FUNCTION (skywalking)
2.1 代码命名约定
该阶段主要是进行 Zend 执行器的 assign 动作,变量的命名有如下规则:
ori_
打头的,是 Zend 引擎的原函数,这里做备份,便于 hack 后,恢复原来的执行zend_
打头的,就是 Zend 引擎的内置函数sky_
打头的,是我们计划在对应阶段进行的自定义动作。一般在自定义动作的最后,都会使用ori_
来交还函数控制权,恢复原来正常流程函数的执行动作
部分代码如下:
1 | PHP_MINIT_FUNCTION (skywalking) { |
2.2 拦截的函数分类和意义
其拦截的函数主要分三类:
- zend_execute_ex,拦截用户态函数,即我们平时写的 .php 文件里面的代码。这里可以捕获到 class name 类名、function name 函数名
- zend_execute_internal,拦截 PHP 内置的函数和类等,比如 PDO、mysqli 等
- CURL相关的函数句柄,这样我们就可以捕获函数的上下游 http 调用信息。其拦截的函数包括:curl_exec、curl_setopt、curl_setopt_array、curl_close
三、PHP_RI 阶段,请求初始化,注册 sky-agent,构造原始 segment
入口函数 PHP_RINIT_FUNCTION(skywalking)
3.1 主要流程
PHP_RI 阶段在每一个 fpm 请求时都会触发一次,在此阶段,主要进行以下两件事
static int sky_register()
,通过 unix sock 通信,注册 agent,同时根据返回的握手信息来确定 app、service、instance信息static void request_init()
,构造 segement 信息,这里包含了以下重要信息- 生成 traceId
- 根据 header 的 sw8 字段来解析上游信息,进而构造 span 信心,refs 信息
3.2 traceId 生成规则,三段格式 instance.pid.second
其核心代码如下
1 | static void generate_context() { |
其中 sky_increment_id 是 0~9999。MSP 平台中显示的 TraceId 信息,类似如下:
我们来简短分析下,按点「.」分割
1
。因为在 sky_register 阶段,application_instance 固定为 1393
,即 pid16109522148050
这个就是时间戳 + sky_increment_id 构成的了
3.3 header 里的 HTTP_SW8 数据含义
1 | static void generate_context() { |
我们去网关日志里,取一个样本分析,得到的原始 header 信息如下
1 | 1-QzBBODNDMzEtMTYxMDk1MzUyODM0MC0xMTc3OTctQS0xMjcw-QzBBODNDMzEtMTYxMDk1MzUyODM0MC0xMTc3OTctQS0xMjcw-1-xxxxxxxxxxxx-YjcyOWU0MzUtNjA5Zi00YzMwLWI4MjctNjZmMmUyYWZjNmM2-xxxxxxxxxx==-xxxxx.service |
我们按中横线(-)分割,得到
1 | 1 |
我们对其中 1,2,4,5,6,7 进行 base64 解码,得到
1 | 1 |
3.4 RI 周期主要流程如下
四、PHP_EXECUTE 阶段,拦截代码执行语句,分析后恢复执行
在 MI 阶段,我们替换了 zend 引擎的函数执行指向,所以所有语句的执行会被我们接管。我们在执行完自己需要的动作后,还原原来的执行即可。
需要注意的是,我们接管的函数会被多次触发,每执行一条 opline,就会被触发一次(存疑,待指正)。
4.1 ZEND_API void sky_execute_ex(zend_execute_data *execute_data)
核心的流程如下
- 获取当前代码执行的信息,包括:类名、函数名
- 对类名和函数名进行判断,看是否需要拦截。目前拦截的是 Predis SDK
- 如果需要拦截,根据拦截信息构造 span
- 把构造好的 span 插入到当前 segment 的 spans 数组里
- 恢复函数原来的执行,调用
ori_
即可
4.2 ZEND_API void sky_execute_internal(zend_execute_data execute_data, zval return_value)
其核心流程和上面 4.1 分析的类似,其不同就在于这里拦截的是 PHP 内置的一些类,即编译时就安装的类。流程主要包括
- 获取类名和函数名
- 根据类名判断是否需要拦截,目前需要拦截:PDO、PDOStatement、mysqli、Yar_Client、Reids、Memecached
- 构造 span,插入到当前 segment 的 spans 数组里
- 恢复函数执行
4.3 CURL HOOK
这里主要是调用链的传递,如果发现有 curl 请求,则根据规则生成当前的 sw8 信息,塞到 http header 的 sw8 字段里,传递给下游。
更多的流程,以后补充。
五、PHP_RS 请求结束阶段,发送 segment 信息至 sidecar
PHP_RS 阶段主要做两件事儿:
通过 unix sock 发送本请求构建的 segment 信息至 sidecar
释放当前请求里的全局状态存储。由于其他的 int、char、boolean 类型的不涉及到内存管理,所以就是四个 zval 的释放
1
2
3
4
5
6
7
8
9
10
11ZEND_BEGIN_MODULE_GLOBALS(skywalking)
char *sock_path;
char *app_code; //app_name eg:skywalking.app_code = MyProjectName
char *app_code_env_key; //app_name 环境变量地址:环境变量->默认KEY:APM_APP_CODE
zend_bool enable;
zval UpstreamSegment; //全局上报数据段
zval context;
zval curl_header; //curl header数据
zval curl_header_send; //记录当前R周期 是否已经send过curl_header
int version;
ZEND_END_MODULE_GLOBALS(skywalking)
六、PHP_MS 模块结束阶段
这个阶段没啥好说的,没啥特殊操作