Nginx 使用requestid 对应用程序日志及性能追踪

问题描述:业务侧经常会遇到一个问题,可以拿到某个慢查询的SQL(比如mysql slowlog, 或是使用的云产品打开实例页面查看对应的慢日志),但是确很难找到对应的业务代码在哪里?(除非SQL本身具有特殊识别性,或是对业务系统非常之熟悉外,如果很雷同的,找起来确实很痛苦,亲测是这样)

针对上面出现的这个问题,nginx request_id 可以完美解决

nginx 从1.11 之后支持生成request_id

部署方案:

1.nginx 接入层关键配置

map $http_x_log_request_id $log_request_id {
    default $http_x_log_request_id;
    -       $request_id;
    ""      $request_id;
}

location ~ .*\.(php|php5)?$ {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index   index.php;
    fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param   LOG_REQUEST_ID $log_request_id;
    include         fastcgi_params;
    fastcgi_intercept_errors on;
    error_page      500 502 503 504  /50x_php.html;
}

2.业务层WEB框架入口代码:设置全局变量

$GLOBALS['LOG_REQUEST_ID'] = !empty($_SERVER['LOG_REQUEST_ID']) ? $_SERVER['LOG_REQUEST_ID'] : $_SERVER['REQUEST_TIME_FLOAT'];

3.业务层JOB框架入口代码:设置全局变量,由于job不经过nginx层,所以request_id自己生成

<?php
$job_request_id = md5("{$script_name}." . microtime(true));
$GLOBALS['LOG_JOB_REQUEST_ID'] = $job_request_id;
?>

4.业务日志收集到文件

<?php
....
$log['controller'] = controller值
$log['action'] = action值
$log['url'] = 当前请求的url, 可以用$_SERVER['REQUEST_URI']获取;
$log['refer'] = //refer, 可以从$_SERVER获取;
$log['user_agent'] = user_agent值,可以从$_SERVER获取;
$log['ip'] = ip获取函数
$log['http_status'] = http状态码,可以从$_SERVER获取;
$log['log_request_id'] = isset($GLOBAL['LOG_REQUEST_ID']) ? isset($GLOBAL['LOG_REQUEST_ID']) : '';
write_log(json_encode($log));
....
?>

5.将第三步写的log导入到es中,便于查询log_request_id对应的请求来源

6.框架SQL执行处代码修改:

$comment_token = '';
if(defined('IS_JOB') && IS_JOB) {
	if(isset($GLOBALS['LOG_JOB_REQUEST_ID']) && !empty($GLOBALS['LOG_JOB_REQUEST_ID'])) {
		$comment_token = '/*job_' . $GLOBALS['LOG_JOB_REQUEST_ID'] . '*/';
	}
} else {
	if(isset($GLOBALS['LOG_REQUEST_ID']) && !empty($GLOBALS['LOG_REQUEST_ID'])) {
		$comment_token = '/*web_' . $GLOBALS['LOG_REQUEST_ID'] . '*/';
	}
}
$sql = $sql . $comment_token;

$this->_result = $this->_execute($sql);

这里在执行的SQL语句后面接上/*request_id*/,并不影响SQL本身的执行,与此同时还能知道SQL的来源,下图为慢日志查询中显示的SQL语句,带上了request_id, 这样是不是就可以得容易知SQL语句来自业务哪了呢?

根据上述步骤1,2,3,4,5,就可以根据慢日志查询的SQL, 反查到对应的业务请求来源了,是不是很帅?

PHP性能优化实战

背景

最近在做平台的性能优化,源于前一段时间平台的性能掉的很严重,后台页面的平均响应时间一度掉到500ms左右

4.2号的性能

变差前正常的性能(320ms左右):

3.18号的性能

4.1号的性能邮件,展示的是3.31号平台性能,而3.30号是迭代发布之日,所以断定在3.30发布之后(之前的一个迭代开发中),引入了低效代码,导致性能下降

然而,最近平台在升级php7,在4.17号php7升级之后,平台的性能如下:

4.17号的性能

由此可以看到,php7升级,让平台后端性能从450ms提升到210ms,平台性能提升了50%+。

那么问题来了,假设我们是从原来正常的性能(320ms)提升的话,或许效果会更好,于是开始了我们这篇文章的正文,这篇文章记录的是我试图找回之前迭代开发损失的性能的一些方法。

安装性能定位工具

工欲善其事,必先利其器,安装xhprof php profiler工具,具体安装方法见:
https://juejin.im/post/5a1d507751882531ba10b0e9

之前平台升php5.6.36之后,用的是tideways扩展(不能直观的查看页面的性能),前些年当时xhprof还不支持php这个版本, 但是目前按上面文章的方法安装,我试了下,已经可以支持到php7版本,可以直接页面点击链接查看当前页面的性能

我修改了下平台框架入口代码:

$xhprof_on = false;
//for xhprof starting----

if(extension_loaded('xhprof') && defined('xhprof_enable') && xhprof_enable == 1) {
        xhprof_enable(XHPROF_FLAGS_CPU + XHPROF_FLAGS_MEMORY);
        $xhprof_on = true;
}

if($xhprof_on){
        $xhprof_data = xhprof_disable();
        $xhprof_root = '/data/www/xhprof_db/';
        include_once $xhprof_root."xhprof_lib/config.php";

        include_once $xhprof_root."xhprof_lib/utils/xhprof_lib.php";
        include_once $xhprof_root."xhprof_lib/utils/xhprof_runs.php";
        $xhprof_runs = new XHProfRuns_Default();
        $run_id = $xhprof_runs->save_run($xhprof_data, "hx");
        if(体验环境) {
                echo '<a href="http://xhprof.oa.com/index.php?run='.$run_id.'&source=hx" target="_blank">xhprof</a>';
        }
}

同时nginx 配置要支持xhprof.oa.com这个域名:

server {
        listen 80;
        root /data/www/xhprof/xhprof_html/;
        server_name xhprof.oa.com;
        location = / {
                index index.php;
        }
        location ~ \.php {
                fastcgi_pass 127.0.0.1:9000;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                include fastcgi_params;
        }
}

最终的效果:

xhprof profiler

xhprof profiler图展示的是当前页面,从框架入口开始,页面各个方法的调用次数和执行时间,具体字段含义可以看上面安装链接中的介绍,这里我主要关注calls(方法调用次数),Incl.Wall Time( 方法执行花费的时间,包括子方法的执行时间 ),IWall%(花费时间占比)这三个字段。Incl.Wall Time值大的,IWall% 占比大的,就是当前页面比较耗时的方法。calls多的,表示方法次数比较多(可能是因为此方法置于循环内)。

在介绍具体优化代码之前,先介绍下我的思路

起初我想从3.16~3.26这段时间内的迭代变更的文件里去试图查找性能差的代码,但是实操起来,我放弃了,这段时间内提交的代码次数太多,代码量也比较大,一个个看,两个规模点的需求,基本上很难在一两天内完成任务。

于是我换了个思路,优化访问量大的页面和框架入口基类beforeFilter方法(基本所有页面都会执行到这个方法),这样有利于削平平均值。

于是我从点击流(一个用户画像操作记录的表)中统计了下性能变差时间段内访问量大,且group by后台响应时间平均值大的页面

PS:如果没有这样一张表, 要得到哪个请求访问量大, 其实也很简单, 可以通过nginx access log 加上shell 命令, 也是可以统计出来的,方法总比问题多.

有了上面页面的访问量+时长排序,再加上xhprof 工具,感觉就成功了一半了,接下来就是对具体页面的代码优化。

以下是我优化的一些代码片断

/**
foreach里减少数据库查询IO,减少 IO 次数
IO永远是数据库最容易瓶颈的地方,这是由数据库的职责所决定的,大部分数据库操作中超过90%的时间都是 IO 操作所占用的,减少 IO 次数是 SQL 优化中需要第一优先考虑,当然,也是收效最明显的优化手段。
*/

-foreach ($app_ids as $app_id) {
-    if (查数据库是否启用($app_id)) {
-        $match_app_ids[] = $app_id;
-    }
-}

+$match_app_ids = 批量查一批id是否满足($app_ids)
//大体就是用一条sql 去获取满足要求的app id
/**
缓存没用上,而且这个方法是beforefilter里每个请求都会调用到的
*/
$cache_key = ("xxxxxx").$user_id;
$cache_data = $cache->get($cache_key);
-if (true || empty($cache_data)) {
+if (empty($cache_data)) {
	.....
/**
实例化类多次,单例不需要实例化多次,用一个变量存储先
*/
-g('aaaa')->xxxx($this);
+$aaaa = g('aaaa');
+$aaaa->xxxx($this);
……

+$aaaa->yyyy();
-g('aaaa')->yyyy();
/**
每个请求都会读取的配置,利用缓存存储,只读一次db
利用缓存降低db读取
*/
public function test($user='') {
        ………
	-return !empty($config->find(['type'=>'xxxconfig','name'=>$user]));

	+$cache_key = "config_list";
	+$config_list = Cache::get($cache_key);
	+if (empty($config_list) || !is_array($config_list)) {
	+	$config_rules = $config->findAll(['type'=>'xxxconfig']);
	+	$config_list = array();
	+	foreach ($config_rules as $key => $config_rule) {
	+		$config_list[] = $config['Setting']['name']; 
	+	}
	+	Cache::add($cache_key, $config_list, '+1 year');
	+} 

	return in_array($user, $config_list);
}
/**
一个方法类,同一个变量值,反复计算,只计算一次
*/
+$x = $a->b($z);
-if ($a->b($z)) {
+if ($x) {
       .....

-if ($a->b($z)) {
+if ($x) {
}
/**
foreach循环里多次计算,其实没必要,虽然耗时不长,但是调用了几十次,也增加了一定开销
*/
foreach($as as $a){
    -$a=$a[g('A')->name];
    +$a=$a['A'];
	$a_data[$a['id']]=$a;
}

以上只是举了几个优化的例子,其实这次优化的地方有很多, 下面提炼下一些优化方法

  • 减少数据库io(这个效果很不错,循环次数越多,性能就越明显)
  • 同一个计算值在方法内多次复制粘贴,可以用一个变量存储,计算一次
  • 实例化多次,用变量存储一次,如果实例化与状态无关的话
  • 数据库查询语句要注意是否使用到索引(这个效果很明显,这次优化有张表没加索引,加上后,性能杠杠的)
  • 判断元素是否在数组中, 如果数组比较大的话 , isset比in_array效率高
  • 合理使用内存,及时unset掉或是释放掉内存或文件/数据库句柄
  • 少用正则表达式,正则用起来爽,但是效率很低,越复杂的正则效率越低
  • 如果能将类的方法定义成static,就尽量定义成static,它的速度会提升将近4倍
  • row[‘id’]的性能是row[id]的七倍
  • 尽量避免使用__get,__set,__autoload
  • include文件时尽量使用绝对路径,因为避免了PHP去include_path里查找文件的速度,解析操作系统路径所需的时间会更少
  • 尽量做缓存,可使用redis, memcached。缓存可用来加速动态Web应用程序,减轻数据库负载。对运算码 (OP code)的缓存很有用,使得脚本不必为每个请求做重新编译
  • 当执行变量的递增或递减时,i++会比++i慢一些。这种差异是特有的,并不适用于其他语言
  • 尽量采用大量的PHP内置函数
  • foreach效率更高,尽量用foreach代替while和for循环
  • 用单引号替代双引号引用字符串 (双引号要解析判断有无变量)
  • 对global变量,应该用完就unset()掉
  • 如果在代码中存在大量耗时的函数,你可以考虑用C扩展的方式实现它们
  • 尽量少进行文件操作,虽然PHP的文件操作效率也不低

期待优化后的代码上线,希望能降到200ms 以下, 提升20ms的性能(10%的提升)。别看只有20ms,当平台性能达到一定阶段后,几毫秒都是难以优化的。提升机器配置和换语言除外

优化后的效果(未来随着不断优化会越来越好):

优化后:后台响应时间低于200ms(10%的提升)

其实上面聊的都是些代码层的优化方案,除此之外,其实优化可以做很多,早些年,Yahoo出了个36条,里面很大一部分是前端优化的一些方案,优化可以分为后端优化,前端优化,组件优化,服务器优化等等,再整体聊聊整体的性能优化方案

  • 前端

1.减少http请求,比如常见的用sprite图,现在构建工具很多都能自动生成,比起早些年,已经方便太多
2.css, js压缩合并,减小文件体积,加快网络传输
3.前端缓存
4.css置于前面,防止页面重绘
5.不使用css表达式
6.让ajax可以缓存
7.减少dom数量
8.懒加载
9.减少cookie大小
10.cdn

  • 后端

除了上面那些之外,还有:

1.页面静态化(全局或局部静态化)
2.服务器gzip压缩等等

  • 服务器优化

1.提升配置
2.优化参数,比如tcp连接端口数,timewait释放时间等

性能优化方案

  • 减少http请求
  • 压缩js/css
  • 减少cookie大小
  • css放在顶部,js放于底部
  • 避免使用css表达式
  • 使用CDN
  • 使用gzip压缩
  • 配置Etag
  • 加Expires或Cache-control
  • 避免空的src(浏览器在渲染过程中会把src中空内容加载)
  • 使用kv缓存
  • 数据库加索引,选引擎,读写分离,分库分表等
  • php 开启opcache