kurnek学习笔记

PHP性能分析工具 - xdebug profiler

在工作中我们不得不关注的一点就是程序的执行性能,但是面对日益复杂的程序,单纯靠人工code review去分析性能瓶颈,显然不是那么有效率的事情,因此找到合适的工具用来分析性能消耗变得尤为重要,这次就来介绍下PHP性能分析工具XDEBUG。

XDEBUG

官网:https://xdebug.org/

XDEBUG是历史悠久的PHP扩展之一,用于DEBUG和分析PHP程序,该项目一直有在维护,目前支持PHP5到7的版本,基本上涵盖了我们常用版本。

安装XDEBUG

官方文档:https://xdebug.org/docs/install

基本上跟安装其他扩展没有什么区别,下载源码后用phpize + configure + make && make install 即可。

需要注意的是如果安装了多个PHP版本,那么在configure的时候要带上指定版本的php-config可执行文件。

配置XDEBUG

官方文档:https://xdebug.org/docs/all_settings

XDEBUG并不只有profiler功能,因此它有非常多的配置选项,包括函数trace,GC监控等等,本文由于主要将性能分析所以只启用profiler功能,在php.ini中添加以下几行配置:

zend_extension=/pathToExtension/xdebug.so
xdebug.profiler_enable=0
xdebug.profiler_enable_trigger=1 xdebug.profiler_output_dir=/apps/xdebug

xdebug.profiler_enable参数为1时默认对所有请求都采集性能数据,这里我们设置为0,也就是默认不采集。

xdebug.profiler_enable_trigger参数为1时,仅在GET/POST/COOKIE参数中带有XDEBUG_PROFILE时才采集。

配置好以后,重启PHPFPM,通过HTTP或者其他手段访问PHPFPM进程,就会在配置的xdebug.profiler_output_dir目录下找到类似 cachegrind.out.6191 的文件了,文件后面的数字是进程PID。

注意:如果需要跟OPCACHE共用的话,则要在OPCACHE加载后再加载XDEBUG,具体可以查阅官方文档。

分析结果

XDEBUG采用了Cachegrind格式来记录分析结果,该文件格式可以用kcachegrind工具来生成可视化分析视图,除此以外XDEBUG作者还提供了几种不同的分析工具,不过我这边看了下那些工具都不是那么好使,google后发现PHPSTORM也支持XDEBUG的分析结果文件,因此就直接使用PHPSTORM来进行分析。

需要注意的是XDEBUG不同版本的分析结果对PHPSTORM版本也是有要求的,我用XDEBUG 2.8输出的分析结果,用PHPSTORM 2016版无法解析,用PHPSTORM 2019版就可以解析成功,各位如果解析不成功的话,可以尝试更换不同版本的XDEBUG或PHPSTORM。

打开PHPSTORM,菜单上选择Tools,选择Analyze Xdebug Profiler Snapshot,再选择我们之前采集的文件,即可看到可视化的性能分析报告。

如下图所示:

undefined

点击右边的TAB还能以树状来显示(当然像lavaral这样的套娃框架,用树状来分析显然不是一个好主意…)

undefined

在分析报告中,我们可以看到是哪些函数耗时或者消耗的内存较多,进而找到可优化的地方。

当然由于Xdebug的profiling功能还是存在一些局限性,比如只能统计执行时间和内存消耗,无法知道CPU消耗。

如果需要知道更加详细的数据,那就只能用更强大的工具Tideway了。

所以下次文章会介绍如何使用tideway来搭建一套生产可用的性能监控系统。

如何在PHPUNIT中Mock静态方法?

我们写的类、方法常常存在依赖,为这些类编写单元测试的时候,我们要想尽可能地覆盖更多的测试用例(Test Case),难以避免地就要对依赖类进行Mock。

想象一个场景,我们现在编写一个吃货类(Foodaholic),这个吃货只有一个功能,就是吃,但是他自己不会煮,所以要找个厨师给他做饭吃,而且他还很挑剔,只吃蛋糕(cake)、牛排(steak)和披萨(pizza),代码如下:

class Foodaholic {
    private static $eatable = ["cake","steak","pizza"];
    public function eat() {
        $food = Cook::provideMeal();
        if (!in_array($food,self::$eatable)) {
            echo "Not eatable";
        } else {
            echo "Eating ".$food;
        }
    }
}

好了,我们现在打算对吃货类的eat方法编写单元测试,测试他是不是真的只吃蛋糕、牛排和披萨,理所当然地,我们开始编写厨师类(Cook)的Mock类。

但是此时我们发现,厨师类提供了一个静态方法,而吃货类是调用该静态方法来对厨师类进行依赖,那么我们如何mock呢?

最简单的方法就是在AutoLoader找到Cook类之前就手动引入我们编写好的Mock厨师类,参考以下代码:

//mockStaticCakeCook.php
class Cook {
    public static function provideMeal() {
        return "cake";
    }
}
require_once "mockStaticCakeCook.php";

use PHPUnit\Framework\TestCase;

class FoodaholicTest extends TestCase {
    public function testEat() {
        $this->expectOutputString('Eating cake');
        $f = new Foodaholic();
        $f->eat();
    }
}

这方法肯定是可行的,我们mock了一个做蛋糕的厨师,并且测试了吃蛋糕的用例,但这个方法实在称不上优雅。

那么PHPUNIT有没有提供mock功能呢,答案是肯定的,看下面代码:

// 修改自phpunit文档的示例
use PHPUnit\Framework\TestCase;

class SubjectTest extends TestCase
{
    public function test()
    {
        // Create a mock object
        $mockObject = $this->getMockBuilder("Cook")->getMock();

        // Configure
        $mockObject ->method("provideMeal")->willReturn("cake");
        
        $this->assertEquals("cake", $mockObject->provideMeal());
    }
}

上面的代码是参考官方范例,结合本文场景改写的,我们mock了一个厨师类对象,并且mock了它的provideMeal方法返回cake。

眼尖的小伙伴估计一下就发现问题了:“可这mock的不像是静态方法呀?”

没错,上面确实mock的不是静态方法,因为phpunit并不支持mock静态方法,在官方文档也明确提到这一点。

那么估计这里就有小伙伴要骂我了:“搞半天,原来PHPUNIT不支持啊!你这不是标题党吗?!”

好吧,事已至此,我也只能承认本文确实有点标题党…… 不过各位不好奇为什么PHPUNIT不支持mock静态方法吗?

接下来,我就从本人理解来解读下为什么PHPUNIT不去支持mock静态方法。


首先,我们沿着PHPUNIT的思路走,如果我们要用PHPUNIT的mock方式来对我们的代码进行单元测试,那么我们要怎么改写吃货类呢?我先来举个例子,看以下代码:

单元测试

use PHPUnit\Framework\TestCase;
class FoodaholicTest extends TestCase {
    public function testEat() {
        // Create a mock object
        $mockCook = $this->getMockBuilder("Cook")->getMock();

        // Configure
        $mockCook->method("provideMeal")->willReturn("cake");

        //test
        $foodaholic = new Foodaholic($mockCook);
        $this->expectOutputString('Eating cake');
        $foodaholic->eat();
    }
}

吃货类

class Foodaholic {
    private static $eatable = ["cake","steak","pizza"];
    private $cook;
    // 因为我们的吃货没有厨师做饭就活不下去了
    // 所以在构造的时候就要传入一个厨师
    public function __construct(Cook $cook) {
        $this->cook = $cook;
    }
    public function eat() {
        $food = $this->cook->provideMeal();
        if (!in_array($food,self::$eatable)) {
            echo "Not eatable";
        } else {
            echo "Eating ".$food;
        }
    }
}

我们将厨师作为吃货类构造方法的参数传入,这样就可以顺利使用PHPUNIT的mock功能去对我们的吃货类进行测试了。

那么改写后的代码跟原本的调用静态方法的吃货类最根本的区别是什么呢?

没错,是依赖的引入方式。

使用静态方法调用时,实际上是将吃货类跟厨师类强耦合在一起了,也就是说,假如哪天我们想给吃货换个厨师,那么就必须要改吃货类的代码。

而如果采用构造方法传参的方式,那么依赖就是从外部传入的,我们想要更换厨师,只要给吃货传入另外一个厨师就行了,不需要修改吃货的代码,这时候吃货类跟厨师类就是松耦合了。

说到这里,可能又有小伙伴要问了,在构造方法要求传入Cook类,那不还是耦合了Cook类吗?没错,确实是这样,所以让我们再稍微改一下这个构造方法:

interface ICook {
    public function provideMeal();
}
class Foodaholic {
    private $cook;
    public function __construct(ICook $cook) {
        $this->cook = $cook;
    }
    ...
}

这样一来,只要是会做饭的人(实现了provideMeal方法的类),我们都认为他可以是一个厨师(也就是ICook接口的实现类),这时候我们的吃货对厨师的依赖关系就变得非常合理了,我们可以随时给吃货换厨师,这个厨师可以是保姆,妈妈,爸爸等等,而且在更换厨师的过程中,不需要修改吃货的代码,吃货只要专注吃这件事就可以了,可谓是非常地高内聚了。

同时,我们的单元测试也变得非常简单,以下代码测试吃货是不是只吃蛋糕、牛排和披萨:

class FoodaholicTest extends TestCase {
    /**
    * @dataProvider dp
    */
    public function testEat($food,$expectOutput) {
        // Create a mock object
        $mockCook = $this->getMockBuilder("ICook")->getMock();
        // Configure
        $mockCook->method("provideMeal")->willReturn($food);
        //test
        $foodaholic = new Foodaholic($mockCook);
        $this->expectOutputString($expectOutput);
        $foodaholic->eat();
    }

    public function dp() {
        return [
            ["cake","Eating cake"],
            ["steak","Eating steak"],
            ["pizza","Eating pizza"],
            ["rice","Not eatable"]
        ];
    }
}

结合PHPUNIT提供的 dataProvider 注解,我们轻松优雅地完成了吃货类的测试,真是可喜可贺~


回到问题,为什么PHPUNIT不支持mock静态方法?

我想到的答案是,PHPUNIT希望我们在引入依赖的时候,都能遵循“依赖倒置原则”(Dependence Inversion Principle, DIP)。DIP的原文表达得比较含蓄,我站在PHP这门语言(跟JAVA应该也一样)的角度来解读如何遵循DIP:

  1. 类之间的依赖应该通过抽象来进行,实现类之间不应该直接发生依赖,而是通过接口或抽象类来产生依赖
  2. 接口或抽象类不能依赖实现类
  3. 实现类依赖接口或抽象类

看完上面3句话,再想想前面讲了半天的吃货和厨师的例子,应该明白DIP是个怎么回事了吧!

实际上如果用过最优雅的PHP框架“Laravel”的开发者,都会发现官方给出的大量示例代码都遵循了依赖倒置原则,这也是实现服务容器(Service Container)的基础,当然这是另外一个故事了。

所以,PHPUNIT不去支持静态类的mock,我个人认为是很合理的,反而是我们在实际编写代码的过程中,如果过分贪图快速,而不分场景地去编写静态类,依赖静态类,最后我们的代码大概只会越来越难测试和维护了吧。

从零开始的典型Linux PHP7服务器安装部署

服务器环境:阿里云ECS

Linux发行版本:CentOS 7

安装软件:Mysql-Server Memcached Redis Nginx PHP

需要注意的要点:

1、依赖库需要优先安装如zlib,openssl,pcre等,PHP放在最后安装。

2、不推荐使用yum安装PHP,因为编译PHP的时候有许多参数其实需要自己按需调整,此外自己编译也能理解PHP需要依赖的库。

哟西~前言说到这,让我们赶紧开始吧w

安装依赖库

#zlib 数据压缩库,许多软件都需要使用
yum install zlib
yum install zlib-devel
#libevent 网络库,memcached需要使用
yum install libevent
yum install libevent-devel
#openssl 加密库
yum install openssl
yum install openssl-devel
#tcl脚本库 redis需要
yum install tcl
yum install tcl-devel
#pcre 正则表达式解释库
yum install pcre
yum install pcre-devel
#xml 解析库
yum install libxml2
yum install libxml2-devel
#curl库
yum install libcurl-devel
#mcrypt加密库
yum install mcrypt
#m4 以及 autoconf库,PHP编译扩展时需要用到
yum install m4
yum install m4-devel
yum install autoconf
#memcached库,PHP的memcached扩展需要
yum install libmemcached
yum install libmemcached-devel
#GD 图片处理库
yum install gd
yum install gd-devel
#freetype 字体引擎,PHP GD库需要
yum install freetype
yum install freetype-devel
#libpng PNG图片处理库,PHP GD库需要
yum install libpng
yum install libpng-devel
#libjpeg JPEG图片处理库,PHP GD库需要
wget http://www.ijg.org/files/jpegsrc.v9b.tar.gz
tar xzvf jpegsrc.v9b.tar.gz
cd jpeg-9b
./configure --prefix=/etc/libjpeg
make && make install
cd ..

当然以上有些库可能系统本身就已经安装好了,各位看情况安装即可。

安装mysql

在mysql官网的这个地址可以选择需要的版本:https://downloads.mysql.com/archives/community/

此处使用5.7为例子,只安装需要的包(当然也可以直接安装bundle包)

#阿里云某些centOS自带了mariadb-libs,会跟mysql冲突,先卸载
yum erase mariadb-libs
wget https://downloads.mysql.com/archives/get/file/mysql-community-common-5.7.17-1.el7.x86_64.rpm
rpm -iv mysql-community-common-5.7.17-1.el7.x86_64.rpm
wget https://downloads.mysql.com/archives/get/file/mysql-community-libs-5.7.17-1.el7.x86_64.rpm
rpm -iv mysql-community-libs-5.7.17-1.el7.x86_64.rpm
wget https://downloads.mysql.com/archives/get/file/mysql-community-client-5.7.17-1.el7.x86_64.rpm
rpm -iv mysql-community-client-5.7.17-1.el7.x86_64.rpm
wget https://downloads.mysql.com/archives/get/file/mysql-community-server-5.7.17-1.el7.x86_64.rpm
rpm -iv mysql-community-server-5.7.17-1.el7.x86_64.rpm
wget https://downloads.mysql.com/archives/get/file/mysql-community-devel-5.7.17-1.el7.x86_64.rpm
rpm -iv mysql-community-devel-5.7.17-1.el7.x86_64.rpm

安装nginx

#下载openssl 1.0.2k的源码,用于编译nginx的http2模块时候使用
wget https://www.openssl.org/source/openssl-1.0.2k.tar.gz
tar xzvf openssl-1.0.2k.tar.gz
wget http://nginx.org/download/nginx-1.12.0.tar.gz
tar xzvf nginx-1.12.0.tar.gz
cd nginx-1.12.0
mv ../openssl-1.0.2k ./
./configure --prefix=/etc/nginx --sbin-path=/etc/nginx/sbin/nginx --modules-path=/etc/nginx/modules --conf-path=/etc/nginx/conf/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --http-client-body-temp-path=/var/lib/nginx/tmp/client_body --http-proxy-temp-path=/var/lib/nginx/tmp/proxy --http-fastcgi-temp-path=/var/lib/nginx/tmp/fastcgi --http-uwsgi-temp-path=/var/lib/nginx/tmp/uwsgi --http-scgi-temp-path=/var/lib/nginx/tmp/scgi --pid-path=/run/nginx.pid --lock-path=/run/lock/subsys/nginx --user=nobody --group=nobody --with-file-aio --with-ipv6 --with-http_ssl_module --with-http_v2_module --with-http_realip_module --with-http_flv_module --with-http_mp4_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_stub_status_module --with-pcre --with-pcre-jit --with-openssl=openssl-1.0.2k
make && make install
cd ..

安装完毕后的nginx会在/etc/nginx目录下~

当然也可以自行修改以上编译参数中的路径将nginx编译到指定的位置去

下载openssl 1.0.2的目的主要是为了编译nginx的http2模块,详细可以参考我的另一篇博文

安装memcached

wget http://memcached.org/files/memcached-1.4.36.tar.gz
tar xzvf memcached-1.4.36.tar.gz
cd memcached-1.4.36
./configure --prefix=/etc/memcached
make && make install
cd ..

安装后的memcached会在/etc/memcached目录下,同理这里也是可以自行修改的~

安装redis

wget http://download.redis.io/releases/redis-3.2.8.tar.gz
tar xzvf redis-3.2.8.tar.gz
cd redis-3.2.8
make
cd ..
mv redis-3.2.8 redis
mv redis /etc/
chmod +x /etc/redis/src/redis-server
chmod +x /etc/redis/src/redis-cli
ln -s /etc/redis/src/redis-cli /usr/bin/redis-cli
ln -s /etc/redis/src/redis-server /usr/bin/redis-server

redis的安装是直接编译以后将目录移动到你想要放置的位置,然后给redis-cli和redis-server加上可执行权限就可以啦~

此处是将redis放到/etc/redis下面,为了方便还做了两个软连接

安装PHP

安装完一大堆依赖以及其他需要用到的软件以后,终于来到安装PHP的环节了~

其实安装PHP除了依赖的库特别多以外,也没什么特别的,也是配置(configure),编译(make),安装(make install)三步走即可~

强烈建议大家安装之前使用./configure --help来仔细看一遍php的编译提供的参数,再来选择自己需要的模块~

此处给出的编译参数只是一个范例~

wget http://am1.php.net/distributions/php-7.1.4.tar.gz
tar xzvf php-7.1.4.tar.gz
cd php-7.1.4
./configure --prefix=/etc/php --enable-fpm --with-openssl --with-pcre-jit --with-zlib --with-curl --with-gd --with-jpeg-dir=/etc/libjpeg --with-png-dir=/usr/include/libpng15 --with-freetype-dir=/usr/include/freetype2/freetype --enable-mbstring --enable-mysqlnd --with-mysqli --with-pdo-mysql --enable-sockets
make && make install
cp php.ini-production /etc/php/lib/php.ini
cd ..

这里将PHP安装到了/etc/php下,并且拷贝了一份预设的php.ini到/etc/php/lib/下~当然啦,路径依然是可以自行修改的,详细参考配置参数。

编译redis以及memcached的PHP扩展

#编译redis扩展
wget http://pecl.php.net/get/redis-3.1.2.tgz
tar xzvf redis-3.1.2.tgz
cd redis-3.1.2
/etc/php/bin/phpize
./configure  --with-php-config=/etc/php/bin/php-config
make && make install
cd ..
#编译memcached扩展
wget http://pecl.php.net/get/memcached-3.0.3.tgz
tar xzvf memcached-3.0.3.tgz
cd memcached-3.0.3/
/etc/php/bin/phpize
./configure  --with-php-config=/etc/php/bin/php-config
make && make install
cd ..

编译好这两个扩展以后,一定不要忘记将这两个扩展添加到php.ini中去哦~

End

好了~软件安装到此结束~至此我们就拥有了一个可以正式进行PHP开发的服务器啦~ww

接下来就是修改PHP和NGINX的配置就完成啦~修改配置的话,网上范例特别多此处就不再赘述(有需要也可以留言,日后可能补上)

PHP OPCACHE 解读

本文译自:http://jpauli.github.io/2015/03/05/opcache.html

由于本人C语言水平英文水平计算机水平等等各种水平有限,因此有些部分可能理解和翻译得不是特别到位,总之尽力啦。

OPCodes 缓存的起源

PHP是一个脚本语言,在通常情况下,PHP会编译任何你让它运行的文件。通过编译获得OPCodes,然后运行并且在结束之后立刻废弃掉这些OPCodes。可以说PHP是被设计成这个样子的:在上一个请求中做过的所有事情,到了下一个请求,它全都忘记了。

在生产环境的服务器上,PHP代码一般不会频繁变动,因此这个编译环节基本上都是在读取同样的源代码,生成并且运行同样的OPCodes。每次请求都需要调用PHP的编译器来编译每一个PHP脚本,这无疑浪费了大量的时间和资源。

undefined

因为编译确实太过花时间了,各种OPCode缓存扩展被设计了出来。它们的目标是让每一个PHP脚本都只编译一次,将编译出来的OPCodes缓存进共享内存,之后的每个PHP worker进程池(通常是PHP-FPM)中的worker都可以直接从内存中读取和执行OPCodes。

这样做给PHP带来的提升是十分巨大的,因为不需要一遍又一遍地编译PHP脚本,通常能够将运行一个PHP脚本的时间降低一半甚至更多(当然也需要看PHP脚本具体执行了什么代码)。

越是复杂的PHP应用,通过这样的方式获得的提升就越大。如果你的PHP应用在每次请求都需要运行成吨的脚本(比方说基于各种大型框架的应用或者类似wordpress这样的PHP应用),你能够体验到10到15倍的速度提升。这是因为PHP的编译器跟其他编译器一样,都是很慢的,毕竟编译器需要将高级语言的语法转换成机器能明白的语法,它要尝试理解代码到底需要机器做些什么,编译器通常还需要为生成后的代码进行优化处理,以便之后的运行性能可以更好;所以编译一个PHP脚本真的很慢,并且很吃内存。类似Blackfire这样的性能分析器可以让你知道编译时间。

undefined

Home