PHP

实现PHP代码执行的记录与分析

Posted by WGrape的博客 on November 15, 2020

文章内容更新请以 WGrape GitHub博客 : 实现PHP代码执行的记录与分析 为准

前言

本文原创,著作权归WGrape所有,未经授权,严禁转载

一、概览

在项目流程中,测试环节是避免错误的最后一道保障,后续环节中的各种服务监控、安全措施都只能减轻错误,而无法避免,所以测试环节的重要性值得高度重视。针对此环节进行优化,也便是本文的主要目的,文章会尽力用最精简的语言和最简单的图画来论述表达主旨。 image 下文会首先从测试角度介绍两个目标问题,并剖析二者的本质。最后在提出解决方案的同时,规划PHP代码执行记录与分析的作用价值,以实现更多切实可用的目标。 image

二、目标问题

1. 日志无法定位原因

如何获取程序的执行记录,解决日志无法定位的问题,提高排查效率。 image

2. 测试缺少代码覆盖率

如何在不需要任何额外人力投入的情况下,无论什么类型的测试,也能实现自动输出代码覆盖率 ? image

三、问题剖析

1. 分析本质

上述两个目标问题的本质是,如何在不需要任何额外人力投入的情况下记录代码的执行,包括已执行的函数、代码等。 image

2. 如何解决

Xdebug是PHP程序调试级别扩展,其提供的get_code_coverage方法会返回程序执行记录,其中1表示此行代码已执行,-1表示此行代码未执行,-2表示此行代码无法执行。

xdebug_mosaic

由于Xdebug返回的数据过于简单,无法快速获知已执行代码所在的目录、文件、函数、行号等信息。所以需要在其基础上进行较复杂的数据处理过程,直到转化为高级数据,高级数据是描述代码执行记录的最终状态。 image

3. 业务投射

在业务中应用上述方案需特殊处理,实现对于每个业务请求,都自动生成对应的描述代码执行记录的高级数据,这样的数据也称为RunInfo数据。 image 可以从生命周期、存储位置、内容信息等方面认识RunInfo数据,此外应该明确其不是日志且二者区别界限明显。 image 在实际应用中日志数据与RunInfo数据的关系是并列共存的关系,且都以logID为索引。不过因技术限制,RunInfo数据未使用收集等此类过程,暂仅以普通文件读写的方式存储在业务目录下。 image

4. 归纳总结

问题本质的可解性和可应用性已经确定,先暂告一段落。现重新回归文章的标题和中心思想,强调对PHP代码的执行增加两个操作,分别是记录和分析。其中记录部分在前文得到的高级数据中已经实现,剩下的分析部分,简言之就是对记录部分的数据使用分类、统计、计算等处理,下文会继续表述。 image

四、设计方案

1. 思路总览

(1) 思想

为实现文章中心思想的最终结果,本方案的设计根本思想也是基于数据处理和数据分析,其中数据处理的产物是记录性信息,数据分析的产物是分析性信息。 image

(2) 架构

按流程分为三端,在测试端正常进行日常测试工作的同时,业务端的Code Coverage模块此时会根据请求的Logid自动生成对应的RunInfo数据储存下来,并提供代理接口来完成对业务代码的读取和对RunInfo数据的检索,实现PHP代码执行的记录与分析,最终通过平台端把一切相关的输出都可视化展示给用户。 image

(3) 测试端

在测试端的任何测试行为,可以照例如常,不需要发生任何变化。

(4) 业务端

业务端新增的Code Coverage模块,是整个业务端的核心枢纽,一方面会生成最重要的RunInfo数据,另一方面负责对RunInfo数据分析。 image

(5) 平台端

在平台端提供基于Logid查询的Run Trace平台,为了实现将每次请求的代码执行记录和分析信息可视化展示给用户,需要业务端需要提供代理接口来解决无法直接读取业务代码的问题。

image

2. 业务端原理

(1) 配置化

模块以配置化的方式驱动,配置项主要如下:

配置项 配置类型 描述 说明
switch String 控制Code Coverage模块开启的开关 设置在各个环境是否开启
allowlist Array 控制是否统计的白名单,支持目录、文件、函数三种级别 设置不需要被记录的代码
denylist Array 控制是否统计的黑名单,支持目录、文件、函数三种级别 设置不需要被记录的代码
valid_line_expr Array 无效行匹配,支持正则表达式 设置影响统计的代码,如空白行、注释等

(2) 数据处理

① 如何生成

在每个接口主任务的前后会分别执行Code Coverage模块提供的start和end方法,其中start方法完成模块启动相关的预处理工作,end方法生成每次请求对应的RunInfo数据。 image

② 如何转化

现低级数据到高级数据的转化处理主要有如下两个级别过程:

  • 文件级别的数据处理首先进行,使用双层循环处理Xdebug返回的数据,把所有已执行、未执行、无法执行的行号统一整理
  • 函数级别的处理会在文件级别处理的基础上,通过反射获取每个类的所有函数及函数所在的行号范围,最终只记录已执行的函数信息

③ 如何组成

RunInfo数据主要由Business、Coverage、Trace三部分构成,但由于技术限制导致Trace数据暂无法实现。 image

④ 如何清理

  • 在QA环境由于是Docker部署,每次发布都会重新更新环境,所以对应的RunInfo数据也会被清理掉,不需要特殊处理
  • 在DEV环境,需要用脚本自动清理或者人工维护定时清理

    (3) 数据分析

    数据分析主要包括分析RunInfo数据和业务代码的读取,通常使用二者结合的方式来提高分析的精度和与业务代码相关的展示。 image

    3. 预期目标

    (1) 分析接口业务信息

    通过在平台端输入Logid,业务端的代理接口检索对应的business数据,实现接口的业务分析。 image

    (2) 分析接口内部执行

    通过在平台端输入Logid,业务端的代理接口检索对应的Coverage数据和Trace数据( 暂未实现 ),实现接口的内部执行分析。 image

    (3) 分析接口的测试覆盖率

    通过在平台端输入同一接口的多个不同logid,业务端的代理接口检索后合并所有的Coverage数据,最终生成多个请求合并后的最终代码执行记录,实现测试用例的代码覆盖率计算。 image

    五、实际应用

    1. 前言

    根据下列应用实际场景,以新增的Test_coverage接口为例,分别从问题描述、处理过程、最终结论三方面来阐述如何解决现实问题。

    2. 分析接口加载

    (1) 问题描述

    Test_coverage接口内部加载了很多无用的类库,现需要对Test_coverage接口进行加载代价分析,明确可以把哪些未使用的类库删除,精简代码。

    <?php
    class Test_coverage extends Base_controller
    {
      protected $testLib1;
      protected $testLib2;
      protected $testLib3;
    
      protected function _preload()
      {
          $this->testLib1 = &load_app('test_lib1', 'libraries');
          $this->testLib2 = &load_app('test_lib2', 'libraries');
          $this->testLib3 = &load_app('test_lib3', 'libraries');
      }
    
      public function _doaction(){
          // do nothing
      }
    }
    

    (2) 处理过程

    请求Test_coverage接口,在“平台端-接口性能”模块,输入请求对应的logid,查询得到如下信息。经分析发现接口共加载了3个冗余类库,即加载了这个类但是从未调用过。 image

    (3) 最终结论

    通过接口性能查询服务,可以简便的获取到接口所加载的所有文件和冗余加载信息,为优化代码提供可用的分析方案。

    3. 高效排查问题

    (1) 问题描述

    业务代码和接口输出如下所示,Test_coverage接口代码中已经是最新的逻辑,本地调试也输出 a <= b,但为什么QA环境的执行却输出 a < b 呢?

    <?php
    class Test_coverage extends Base_controller
    {
      public function _doaction(){
          $this->calculate(17, 27);
      }
    
      public function calculate($a, $b){
          if ($a > $b) {
              $this->_result['result'] = 'a > b';
          } else {
              $this->_result['result'] = 'a <= b';
          }
      }
    }
    
    {
      "errno": 0,
      "errmsg": "SUCCESS",
      "status": 0,
      "result": "a < b",
      "user_msg": ""
    }
    

    (2) 处理过程

    根据请求产生的logid,在“平台端-执行记录”模块,找到或输入对应的logid,会得到如下信息。 image 点击logid对应的一条请求记录,会跳转到如下页面,视图中会展示出源码,以下被高亮展示的部分就是在QA环境中已执行过的代码。 image 从中能够清晰的看到在QA环境中执行的代码是 'a < b' 而不是 'a <= b' ,所以可以断定此问题一定是QA环境未正确部署代码所致。

    (3) 最终结论

    上述用最简单的一个例子来论证一个事实:如果能正确获知代码的执行记录,无论什么环境,都可以显著并大幅度提高排查问题的效率。

    4. 评估测试用例

    (1) 问题描述

    现需要测试Test_coverage接口,必传a、b两个参数,测试用例和代码如下,当前面临的问题是无法评估此组用例的测试覆盖率。

测试用例 参数
Case 1 a = 17 、b = 27
Case 2 a = 27 、b = 17
<?php
class Test_coverage extends Base_controller
{
    public function _checkparams()
    {
        if (!isset($this->_params['a']) || !isset($this->_params['b'])) {
            throw new exception("param error", ERRNO_PARAMS_ERR);
        }
    }

    public function _doaction()
    {
        $a = $this->_params['a'];
        $b = $this->_params['b'];
        $this->calculate($a, $b);
    }

    public function calculate($a, $b)
    {
        if ($a > $b) {
            $this->_result['result'] = 'a > b';
        } elseif ($a == $b) {
            $this->_result['result'] = 'a = b';
        } else {
            $this->_result['result'] = 'a < b';
        }
    }
}

(2) 处理过程

使用上述两个Case分别测试一次,在“平台端-执行记录”模块,把这两次测试对应的logid输入,系统会合并这两次请求的代码执行信息。然后得到的函数覆盖情况如下所示,总覆盖率为 :(50+100+66.7)/3 = 72.2%。 image 由于覆盖率还较低,故再新增下列3种用例重新测试,重复上述输入logid的过程,最终得到下图函数覆盖信息,总覆盖率为 (100+100+100)/3 = 100%。

附加测试用例 参数
测试用例3 a=17(无参数b)
测试用例4 b=27(无参数a)
测试用例5 a=17、b=17

image

(3) 最终结论

通过测试覆盖率结果,可以评估当前测试用例的好坏,通过不断丰富和优化测试用例,最终可设计出一组高测试覆盖率的用例。

六、结束与规划

本文主要介绍了通过使用基于数据处理、数据分析的技术方案,解决测试环节中的两个目标问题,在优化环节质量的同时,实现PHP代码执行记录与分析,以解决未来面对的更多问题。