zoco

如何进行一次简单的性能分析

2017-04-03


基于xhprof开发php性能优化系统全文

安装

安装php msgpack扩展

安装php xhprof扩展

php5 https://github.com/phacility/xhprof

php7 https://github.com/Yaoguais/phpng-xhprof

安装supervisor

打桩

这一步是将xhprof产生的数据存入redis中。

在程序入口文件,比如index.php中添加下列代码:

// xhprof打点频率
define('XHPROF_FREQUENCY',1000);
if ((mt_rand(1, XHPROF_FREQUENCY) === 1) && function_exists('xhprof_enable')) {
    xhprof_enable(XHPROF_FLAGS_MEMORY|XHPROF_FLAGS_CPU);
}

register_shutdown_function(function () {
    $uname = php_uname('n');
    $time = time();
    $xhprofData = xhprof_disable();
    $log = array(
        'REQUEST_URI' => parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),
        'HTTP_HOST'	  => $uname,
        'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'],
        'REQUEST_TIME' => $time,
        'xhprof_data' => $xhprofData,
    );

    // 压缩日志
    $log = msgpack_pack($log);
    $key = 'chan-xhprof-log';

    $redis = new Redis();
    $redis->connect('127.0.0.1',6379);
    $redis->publish($key, $log);
});

产生的数据会按照pub-sub的方式存入redis中。

转储

这一步是将redis的数据存入到mysql中,来持久化。

建表

为便于之后操作,这里将mysql分库分表,按业务分库,业务内按天分表,具体的怎么存按自己业务的规模来操作,规模小的话就放到一个表里面都是没有问题的。

这里我们假设有两个业务,一个是内网的请求,一个是外网的请求,分别为inner_request和api_request。

新建两个database:

api_request_2016_11_03
inner_request_2016_11_03

在每个数据库中建对应的表:

create table api_request_2016_11_03 (
  id            INT(10) PRIMARY KEY NOT NULL AUTO_INCREMENT,
  uri           VARCHAR(100),
  time          INT(10),
  wt            INT(10),
  ct            INT(10),
  mu            INT(10),
  pmu           INT(10),
  log           TEXT,
  host          VARCHAR(30)
);

create table inner_request_2016_11_03 (
  id            INT(10) PRIMARY KEY NOT NULL AUTO_INCREMENT,
  uri           VARCHAR(100),
  time          INT(10),
  wt            INT(10),
  ct            INT(10),
  mu            INT(10),
  pmu           INT(10),
  log           TEXT,
  host          VARCHAR(30)
);

supervisor启动服务

这一步是启动一个常驻进程,来将redis中的数据转储到mysql中。

<?php

date_default_timezone_set('Asia/Shanghai');
ini_set('default_socket_timeout', -1);

/**
 * Class DB
 */
class DB {

    /**
     * @var array
     */
    public $pdo = array();

    /**
     * @var
     */
    private static $_instance;

    /**
     * DB constructor.
     */
    private function __construct() {
        $this->pdo['inner_request'] = new Pdo("mysql:host=127.0.0.1;dbname=inner_request;", "root", "123456789");
        $this->pdo['api_request'] = new Pdo("mysql:host=127.0.0.1;dbname=api_request;", "root", "123456789");
    }

    /**
     * @return DB
     */
    public static function getInstance() {
        if (!(self::$_instance instanceof self)) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }
}

function loop() {
    $redisKey = "chan-xhprof-log";
    $redis = new Redis();
    $res = $redis->connect('127.0.0.1', '6379',10);
    while (true) {
        $redis->subscribe(array($redisKey), function ($instance, $channelName, $data) {
            $type = 'inner_request';
            $data = msgpack_unpack($data);
            if (empty($data) || !is_array($data)) {
                return;
            }

            $uri = preg_replace('![0-9\\.]{4,}!', '$id$', $data['REQUEST_URI']);
            if (strstr($uri, '.jpg')) {
                return;
            }

            $time = (int)$data['REQUEST_TIME'];
            $host = DB::getInstance()->pdo[$type]->quote($data['HTTP_HOST']);

            $wt = (int)$data["xhprof_data"]["main()"]["wt"];
            $ct = (int)$data["xhprof_data"]["main()"]["ct"];
            $mu = (int)(empty($data["xhprof_data"]["main()"]["mu"]) ? 0 : $data["xhprof_data"]["main()"]["mu"]);
            $pmu = (int)(empty($data["xhprof_data"]["main()"]["pmu"]) ? 0 : $data["xhprof_data"]["main()"]["pmu"]);
            $log = DB::getInstance()->pdo[$type]->quote(base64_encode(gzcompress(serialize($data["xhprof_data"]), 9)));

            $table_suffix = '_' . date('Y_m_d', time());
            if (preg_match("#^\/inner\/#", $uri)) {
                $uri = DB::getInstance()->pdo[$type]->quote($uri);
                $sql = "INSERT INTO inner_request{$table_suffix}(uri, time, wt, ct, mu, pmu, log, host) VALUES({$uri}, {$time}, {$wt}, {$ct}, {$mu}, {$pmu}, {$log}, {$host})";
                $type = 'inner_request';
            } else {
                $uri = DB::getInstance()->pdo[$type]->quote($uri);
                $sql = "INSERT INTO api_request{$table_suffix}(uri, time, wt, ct, mu, pmu, log, host) VALUES({$uri}, {$time}, {$wt}, {$ct}, {$mu}, {$pmu}, {$log}, {$host})";
                $type = 'api_request';
            }
            DB::getInstance()->pdo[$type]->query($sql);
        });
    }
}

try {
    loop();
} catch (Exception $e) {
    echo $e->getMessage() . PHP_EOL;
}

这时mysql数据库就会有数据进来。

如图所示

NnM7rR.png

说明数据已经打到数据库中了。

分析

这个时候准备工作就都做完了,现在就可以进行分析了。

定时脚本

因为现在数据太大,所以mysql中的数据都是经过压缩的。所以在每天的凌晨需要跑定时脚本来处理这些数据存到分析的数据库中,供页面展示。

创建分析数据库

这个可以按业务分库也可以放到一个库中,为了简便,这里就建立一个分析数据库。

数据库为:

create database xhprof_analyze;

在xhprof_analyze中新建表:

-- email总数据
CREATE TABLE IF NOT EXISTS `api_email_all`(
    `date` DATE NOT NULL,
    `ct_sum` INT(10) NOT NULL DEFAULT 0,
    `wt_sum` INT(10) NOT NULL DEFAULT 0,
    `wt_avg` DECIMAL(10,4) NOT NULL DEFAULT 0,
    PRIMARY KEY (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 根据ct排名数据
CREATE TABLE IF NOT EXISTS `api_email_ct_sum_rank` (
    `date` DATE NOT NULL,
    `uri` VARCHAR(100) NOT NULL,
    `ct_sum` INT(10) NOT NULL DEFAULT 0,
    `wt_sum` INT(10) NOT NULL DEFAULT 0,
    `wt_avg` INT(10) NOT NULL DEFAULT 0,
    `rate` DECIMAL(5,2)NOT NULL DEFAULT 0,
    PRIMARY KEY (`date`,`uri`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 根据wt_sum排名数据
CREATE TABLE IF NOT EXISTS `api_email_wt_sum_rank` (
    `date` DATE NOT NULL,
    `uri` VARCHAR(100) NOT NULL,
    `ct_sum` INT(10) NOT NULL DEFAULT 0,
    `wt_sum` INT(10) NOT NULL DEFAULT 0,
    `wt_avg` INT(10) NOT NULL DEFAULT 0,
    `rate` DECIMAL(5,2)NOT NULL DEFAULT 0,
    PRIMARY KEY (`date`,`uri`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 根据wt_avg排名数据
CREATE TABLE IF NOT EXISTS `api_email_wt_avg_rank` (
    `date` DATE NOT NULL,
    `uri` VARCHAR(100) NOT NULL,
    `ct_sum` INT(10) NOT NULL DEFAULT 0,
    `wt_sum` INT(10) NOT NULL DEFAULT 0,
    `wt_avg` INT(10) NOT NULL DEFAULT 0,
    `rate` DECIMAL(5,2)NOT NULL DEFAULT 0,
    PRIMARY KEY (`date`,`uri`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这三个表是为主页准备的。