舫摘

知人者智 自知者明 胜人者有力 自胜者强

0%

PHP 性能分析(三): 性能调优实战

在本系列的第一篇中,我们介绍了XHProf。而在第二篇中,我们深入研究了XHGui UI,现在最后一篇,让我们把XHProf/XHGui的知识用到工作中!

性能调优

不用运行的代码才是绝好的代码。其他只是好的代码。所以,性能调优时,最好的选择是首先确保运行尽可能少的代码。

OpCode 缓存

首先,最快且最简单的选择是启用OpCode缓存。OpCode缓存的更多信息可以在这里找到。

在上图,我们看到启用Zend OpCache后发生的情况。最后一行是我们的基准,也即没有启用缓存的情况。

在中间行,我们看到较小的性能提升,以及内存使用量的大幅减少。小的性能提升(很可能)来自Zend OpCache优化,而非OpCode缓存。

第一行是优化和OpCode缓存后结果,我们看到很大的性能提升。

现在,我们看看APC之前和之后的变化。如上图所示,跟Zend OpCache相比,随着缓存的建立,我们看到初始(中间行)请求的性能下降,在消耗时长与内存使用量方面的表现都明显下降。

接着,随之opcode缓存的建立,我们看到类似的性能提升。

内容缓存

第二件我们能做的事是缓存内容——这对WordPress而言小菜一碟。它提供了许多安装简便的插件来实现内容缓存,包括WP Super CacheWP Super Cache会创建网站的静态版本。该版本会在出现诸如评论事件时依照网站设置自动过期。(例如,在非常高负载情况下,您可能会想禁止任何原因造成的缓存过期)。

内容缓存只能在几乎没有写操作时有效运行,写操作会使缓存失效,而读操作不会。

你也应该缓存应用从第三方API处收到的内容,从而减少由于API可用性导致的延迟与依赖。WordPress有两个缓存插件,可以大大提高网站的性能:W3 Total Cache 和 WP Super Cache

这两个插件都会创建网站的静态HTML副本,而不是每次收到请求时再生成页面,从而压缩响应时间。

如果你正在开发自己的应用程序,大多数框架都有缓存模块:

  • Zend Framework 2:Zend\Cache
  • Symfony 2:Multiple options
  • Laravel 4:Laravel Cache
  • ThinkPHP 3.2.3:ThinkPHP Cache

查询缓存

另一个缓存选项是查询缓存。针对MySQL,有一个通用的查询缓存帮助极大。对于其他数据库,将查询结果集缓存在Memcached或者cassandra这样的内存缓存,也非常有效。

跟内容缓存一样,查询缓存在包含大量读取操作的场景是最有效的。由于少量的数据改动就会使大块的缓存区无效,尤其不能在这种情况下依赖MySQL查询缓存来提高性能。

查询缓存或许在生成内容缓存时对性能有提升。

如下图所示,当我们开启查询缓存后,实际运行时间减少了40%,尽管内存使用量没有明显改变。

现有三种类型的缓存选项,由query_cache_type控制设置。

  • 设置值为0OFF将禁用缓存
  • 设置值为1ON将缓存除了以SELECT SQL_NO_CACHE开头之外的所有选择
  • 设置值为2DEMAND只会缓存以SELECT SQL_CACHE开头的选择
  • 此外,你应该将query_cache_size设置为非零值。将它设置为零将禁用缓存,不管query_cache_type是否设置。
    1
    想得到设置缓存的帮助,与许多其他性能相关的设置,请查`` mysql-tuning-primer`脚本。
    MySQL查询缓存的主要问题是,它是全局的。对缓存结果集构成的表格的任何更改都将导致缓存失效。在写入操作频繁的应用程序中,这将使缓存几乎无效。

然而,你还有许多其他选择,可以根据你的需求和数据集建立更多的智能缓存,例如Memcachedriakcassandraredis

查询优化

如前所述,数据库查询常常是程序执行缓慢的原因,查询优化往往能比代码优化带来更多切身的好处。

查询优化有助于生成内容缓存时提高性能,而且,在无法缓存这种最坏的情况下也有益处。

除了分析,MySQL还有一个帮助识别慢查询的选择——慢查询日志。慢查询日志会记录所有耗时超过指定时间的查询,以及不使用索引的查询(后者为可选项)。

您可以在my.cnf中使用以下配置启用日志。

1
2
3
4
[mysqld]
log_slow_queries =/var/log/mysql/mysql-slow.log 
long_query_time =1
log-queries-not-using-indexes

任何查询如果慢于long_query_time(以秒为单位),该查询就会记录到日志文件log_slow_queries中。默认值是10秒,最低1秒。

此外,log-queries-not-using-indexes选项可以将任何不使用索引的查询捕获到日志中。

之后我们可以用与MySQL捆绑在一起的mysqldumpslow命令检查日志。

WordPress安装时使用这些选项,主页加载完成并运行后得到如下数据:

1
2
3
4
5
6
$ mysqldumpslow -g "wp_" /var/log/mysql/mysql-slow.log
Reading mysql slow query log from /var/log/mysql/mysql-slow.log
Count: 1 Time=0.00s (0s) Lock=0.00s (0s) Rows=358.0 (358), user[user]@[host]
SELECT option_name, option_value FROM wp_options WHERE autoload = 'S'
Count: 1 Time=0.00s (0s) Lock=0.00s (0s) Rows=41.0 (41), user[user]@[host]
SELECT user_id, meta_key, meta_value FROM wp_usermeta WHERE user_id IN (N)

首先,注意所有字符串值都以S表示,数字则以N表示。你可以添加-a标志来显示这些值。

接下来,请注意,这两个查询均耗时0.00s,这意味着他们的耗时在1秒的阈值以下,且没有使用索引。

MySQL控制台使用EXPLAIN,可以检查性能下降的原因:

1
2
3
4
5
6
7
8
9
10
11
12
mysql> EXPLAIN SELECT option_name, option_value FROM wp_options WHERE autoload = 'S'\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: wp_options
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 433
Extra: Using where

此处,我们看到possible_keysNULL,从而确认未使用索引。

EXPLAIN是对优化MySQL查询非常强大的工具,更多信息可以在这里找到。

PostgreSQL同样也包括一个EXPLAIN(该EXPLAINMySQL的差别很大),而MongoDB$explain 元 操作符

代码优化

通常只有当你不再受到PHP本身限制(通过使用OpCode缓存),缓存了尽可能多的内容,优化了查询之后,才可以开始调整代码。

代码和查询优化带来足够的性能提升才能创建其他缓存;代码在最糟糕的环境(没有缓存)下性能越高,应用就越稳定,重建缓存的速度也就越快。

让我们看看如何(潜在地)优化我们的WordPress安装。

首先,让我们看看最慢的函数:

令我惊讶的是,列表中的第一项不是MySQL(事实上mysql_query()是第四),而是apply_filter()函数。

WordPress代码库的特点是,通过基于事件的过滤系统执行多种数据转换,执行次序按照数据经内核、插件添加或回调的顺序。

apply_filter()函数是这些回调应用的地方。

首先,你可能会注意到,函数被调用4194次。如果我们点击查看更多细节,就可以按照“调用次数”降序排列“父函数”,从而发现translate()调用了apply_filter()函数778次。

这很有趣,因为实际上我不使用任何翻译。我(并怀疑大多数用户)在使用WordPress软件时都设置为本土语言:英语。

因此,让我们点击查看细节,进一步查看该translate()函数在做什么。

在这里,我们看到两间有趣的事。首先,在父函数中,有一个被调用了773次:__()

查看该函数的源代码后,我们发现它是translate()的包装器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
/**
* Retrieves the translation of $text. If there is no translation, or
* the domain isn't loaded, the original text is returned.
*
* @see translate() An alias of translate()
* @since 2.1.0
*
* @param string $text Text to translate
* @param string $domain Optional. Domain to retrieve the translated text
* @return string Translated text
*/
function __( $text, $domain = 'default' ) {
return translate( $text, $domain );
}
?>

根据经验法则,函数调用代价昂贵,应该尽量避免。现在我们总是调用__()而不是translate(),我们应该把别名改为translate()来保持向后兼容性,而__()则不再调用非必要的函数。

然而,实际上,这种改变不会带来多大的差异,只是微观的优化罢了——但它的确提高了代码可读性,简化了调用图。

继续前进,让我们看看子函数:

现在,深入该函数,我们看到有3个函数或方法被调用,每个778次:

  • get_translations_for_domain()
  • NOOP_Translations::translate()
  • apply_filters()
    按照包容性实际运行时间降序排列,我们看到apply_filter()是目前为止耗时最长的调用。

查看代码:

1
2
3
4
5
6
<?php
function translate( $text, $domain = 'default' ) {
$translations = get_translations_for_domain( $domain );
return apply_filters( 'gettext', $translations->translate( $text ), $text, $domain );
}
?>

这段代码的作用是检索一个翻译对象,然后将$translations->translate()的结果传给apply_filter()。我们发现$translationsNOOP_Translations类的一个实例。

仅根据名称(NOOP),再经代码中的注释证实,我们发现翻译器实际上没有任何动作!

1
2
3
4
5
6
<?php
/**
* Provides the same interface as Translations, but doesn't do anything
*/
class NOOP_Translations {
?>

因此,也许我们完全可以避免这种代码!

通过在代码上进行小规模调试,我们看到当前使用的是默认的域,我们可以修改代码以忽略翻译器:

1
2
3
4
5
6
7
8
9
<?php
function translate( $text, $domain = 'default' ) {
if ($domain == 'default') {
return apply_filters( 'gettext', $text, $text, $domain );
}
$translations = get_translations_for_domain( $domain );
return apply_filters( 'gettext', $translations->translate( $text ), $text, $domain );
}
?>

接下来,我们再次分析,确保要运行至少两次——确保所有缓存都建立,才是公平的对比!

这次运行的确更快!但是,快多少?为什么?

使用XHGui的比较运行这一特性就能找到答案。回到我们最初的运行,点击右上角的“比较此处运行”按钮,并从列表中选择新的运行。

我们发现,函数调用的次数减少了3%,包容性实际运行时间减少9%,包容性CPU时间减少12%!

之后,可以按调用次数降序排列细节页,这证实(如同我们的预期)get_translations_for_domain()NOOP_Translations::translate()函数的调用次数减少。同样,可以确认没有预料之外的变化发生。

30分钟的工作带来9% - 12%的性能提升,这非常可喜。这就意味着真实世界的性能收益,即便是在应用了opcache之后。

现在我们可以对其函数重复这个过程,直到找不到更多优化点。

注意:此更改已提交到WordPress.org并已获更新。你可以在WordPress Bug Tracker跟踪讨论,查看实践过程。此更新计划包含在WordPress 4.1版本中。

其他工具

除了出色的XHProf/XHGui,还有一些很好的工具。

New Relic & OneAPM

New RelicOneAPM均提供前后端性能分析;洞察后台堆栈讯息,包括SQL查询与代码分析,前端DOMCSS呈现,以及Javascript语句。OneAPM更多功能请移步(OneAPM在线DEMO)

uprofiler

uprofiler是目前还未发布的Facebook XHProf分支,该分支计划删除Facebook所需的CLA。目前,两者具备相同的特性,只有一些部分重命名了。

XHProf.io

XHProf.ioXHProf的另一种用户界面。XHProf.io在配置文件存储使用MySQL,用户友好性方面不及XHGui

Xdebug

XHProf出现之前,Xdebug早已存在——Xdebug是一种主动的性能分析器,这意味着它不应该用于生产环境,但可以深入了解代码。

然而,它必须与另一个工具配合使用以读取分析器的输出,比如KCachegrind。但是KCachegrind很难安装在非linux机器上。另一个选择是Webgrind

Webgrind无法提供KCachegrind的那些特性,但它是一个PHP Web应用程序,在任何环境都易于安装。

若搭配KCachegrind,你可以轻易探索并发现性能问题。(事实上,这是我最喜欢的剖析工具!)

结语

分析和性能调优是非常复杂的工程。有了对的工具,并理解如何善用这些工具,我们可以很大程度地提高代码质量——即使是对我们不熟悉的代码库。

花时间去探索和学习这些工具是绝对值得的。
学习这些工具是绝对值得的。