PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)

注意:本文是我們的 PHP 性能分析系列的第三篇,點(diǎn)此閱讀 PHP 性能分析第一篇: XHProf & XHGui 介紹 ,或 PHP 性能分析第二篇: 深入研究 XHGui

在本系列的 第一篇 中,我們介紹了 XHProf 。而在 第二篇 中,我們深入研究了 XHGui UI, 現(xiàn)在最后一篇,讓我們把 XHProf /XHGui 的知識(shí)用到工作中!

性能調(diào)優(yōu)

不用運(yùn)行的代碼才是絕好的代碼。其他只是好的代碼。所以,性能調(diào)優(yōu)時(shí),最好的選擇是首先確保運(yùn)行盡可能少的代碼。

OpCode 緩存

首先,最快且最簡單的選擇是啟用 OpCode 緩存。OpCode 緩存的更多信息可以在 這里 找到。

PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)
PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)

在上圖,我們看到啟用 Zend OpCache 后發(fā)生的情況。最后一行是我們的基準(zhǔn),也即沒有啟用緩存的情況。

在中間行,我們看到較小的性能提升,以及內(nèi)存使用量的大幅減少。小的性能提升(很可能)來自 Zend OpCache 優(yōu)化,而非 OpCode 緩存。

第一行是優(yōu)化和 OpCode 緩存后結(jié)果,我們看到很大的性能提升。

PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)
PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)

現(xiàn)在,我們看看 APC 之前和之后的變化。如上圖所示,跟 Zend OpCache 相比,隨著緩存的建立,我們看到初始(中間行)請(qǐng)求的性能下降,在消耗時(shí)長與內(nèi)存使用量方面的表現(xiàn)都明顯下降。

接著,隨之 opcode 緩存的建立,我們看到類似的性能提升。

內(nèi)容緩存

第二件我們能做的事是緩存內(nèi)容——這對(duì) WordPress 而言小菜一碟。它提供了許多安裝簡便的插件來實(shí)現(xiàn)內(nèi)容緩存,包括 WP Super Cache。WP Super Cache 會(huì)創(chuàng)建網(wǎng)站的靜態(tài)版本。該版本會(huì)在出現(xiàn)諸如評(píng)論事件時(shí)依照網(wǎng)站設(shè)置自動(dòng)過期。(例如,在非常高負(fù)載情況下,您可能會(huì)想禁止任何原因造成的緩存過期)。

內(nèi)容緩存只能在幾乎沒有寫操作時(shí)有效運(yùn)行,寫操作會(huì)使緩存失效,而讀操作不會(huì)。

你也應(yīng)該緩存應(yīng)用從第三方 API 處收到的內(nèi)容,從而減少由于 API 可用性導(dǎo)致的延遲與依賴。

WordPress 有兩個(gè)緩存插件,可以大大提高網(wǎng)站的性能: W3 Total CacheWP Super Cache

這兩個(gè)插件都會(huì)創(chuàng)建網(wǎng)站的靜態(tài) HTML 副本,而不是每次收到請(qǐng)求時(shí)再生成頁面,從而壓縮響應(yīng)時(shí)間。

如果你正在開發(fā)自己的應(yīng)用程序,大多數(shù)框架都有緩存模塊:

查詢緩存

另一個(gè)緩存選項(xiàng)是查詢緩存。針對(duì) MySQL,有一個(gè)通用的查詢緩存幫助極大。對(duì)于其他數(shù)據(jù)庫,將查詢結(jié)果集緩存在 Memcached 或者 cassandra 這樣的內(nèi)存緩存,也非常有效。

跟內(nèi)容緩存一樣,查詢緩存在包含大量讀取操作的場景是最有效的。由于少量的數(shù)據(jù)改動(dòng)就會(huì)使大塊的緩存區(qū)無效,尤其不能在這種情況下依賴 MySQL 查詢緩存來提高性能。

查詢緩存或許在生成內(nèi)容緩存時(shí)對(duì)性能有提升。

如下圖所示,當(dāng)我們開啟查詢緩存后,實(shí)際運(yùn)行時(shí)間減少了 40% ,盡管內(nèi)存使用量沒有明顯改變。


PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)
PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)

現(xiàn)有三種類型的緩存選項(xiàng),由 query_cache_type 控制設(shè)置。

  • 設(shè)置值為 0OFF 將禁用緩存
  • 設(shè)置值為 1ON 將緩存除了以 SELECT SQL_NO_CACHE 開頭之外的所有選擇
  • 設(shè)置值為 2DEMAND 只會(huì)緩存以 SELECT SQL_CACHE 開頭的選擇

此外,你應(yīng)該將 query_cache_size 設(shè)置為非零值。將它設(shè)置為零將禁用緩存,不管 query_cache_type 是否設(shè)置。

想得到設(shè)置緩存的幫助,與許多其他性能相關(guān)的設(shè)置,請(qǐng)查看 mysql-tuning-primer 腳本。

MySQL 查詢緩存的主要問題是,它是全局的。對(duì)緩存結(jié)果集構(gòu)成的表格的任何更改都將導(dǎo)致緩存失效。在寫入操作頻繁的應(yīng)用程序中,這將使緩存幾乎無效。

然而,你還有許多其他選擇,可以根據(jù)你的需求和數(shù)據(jù)集建立更多的智能緩存,例如 Memcachedriakcassandraredis

查詢優(yōu)化

如前所述,數(shù)據(jù)庫查詢常常是程序執(zhí)行緩慢的原因,查詢優(yōu)化往往能比代碼優(yōu)化帶來更多切身的好處。

查詢優(yōu)化有助于生成內(nèi)容緩存時(shí)提高性能,而且,在無法緩存這種最壞的情況下也有益處。

除了分析, MySQL 還有一個(gè)幫助識(shí)別慢查詢的選擇——慢查詢?nèi)罩尽B樵內(nèi)罩緯?huì)記錄所有耗時(shí)超過指定時(shí)間的查詢,以及不使用索引的查詢(后者為可選項(xiàng))。

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

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

任何查詢?nèi)绻?long_query_time (以秒為單位),該查詢就會(huì)記錄到日志文件 log_slow_queries 中。默認(rèn)值是10秒,最低1秒。

此外, log-queries-not-using-indexes 選項(xiàng)可以將任何不使用索引的查詢捕獲到日志中。

之后我們可以用與 MySQL 捆綁在一起的 mysqldumpslow 命令檢查日志。

在 WordPress 安裝時(shí)使用這些選項(xiàng) ,主頁加載完成并運(yùn)行后得到如下數(shù)據(jù):

$ 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 表示,數(shù)字則以 N 表示。你可以添加 -a 標(biāo)志來顯示這些值。

接下來,請(qǐng)注意,這兩個(gè)查詢均耗時(shí) 0.00 s,這意味著他們的耗時(shí)在 1 秒的閾值以下,且沒有使用索引。

在 MySQL 控制臺(tái) 使用 EXPLAIN,可以檢查性能下降的原因:

    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_keys 是 NULL,從而確認(rèn)未使用索引。

EXPLAIN 是對(duì)優(yōu)化 MySQL 查詢非常強(qiáng)大的工具,更多信息可以在 這里 找到。

PostgreSQL 同樣也包括一個(gè) EXPLAIN (該 EXPLAIN 與 MySQL 的差別很大),而 MongoDB 有$explain 元 操作符

代碼優(yōu)化

通常只有當(dāng)你不再受到 PHP 本身限制(通過使用 OpCode 緩存),緩存了盡可能多的內(nèi)容,優(yōu)化了查詢之后,才可以開始調(diào)整代碼。

代碼和查詢優(yōu)化帶來足夠的性能提升才能創(chuàng)建其他緩存;代碼在最糟糕的環(huán)境(沒有緩存)下性能越高,應(yīng)用就越穩(wěn)定,重建緩存的速度也就越快。

讓我們看看如何(潛在地)優(yōu)化我們的 WordPress 安裝。

首先,讓我們看看最慢的函數(shù):


PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)
PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)

令我驚訝的是,列表中的第一項(xiàng) 不是 MySQL (事實(shí)上 mysql_query() 是第四),而是 apply_filter() 函數(shù)。

WordPress 代碼庫的特點(diǎn)是,通過基于事件的過濾系統(tǒng)執(zhí)行多種數(shù)據(jù)轉(zhuǎn)換,執(zhí)行次序按照數(shù)據(jù)經(jīng)內(nèi)核、插件添加或回調(diào)的順序。

apply_filter() 函數(shù)是這些回調(diào)應(yīng)用的地方。

首先,你可能會(huì)注意到,函數(shù)被調(diào)用 4194 次。如果我們點(diǎn)擊查看更多細(xì)節(jié),就可以按照“調(diào)用次數(shù)”降序排列“父函數(shù)”,從而發(fā)現(xiàn) translate() 調(diào)用了apply_filter() 函數(shù) 778 次。

PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)
PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)

這很有趣,因?yàn)閷?shí)際上我不使用任何翻譯。我(并懷疑大多數(shù)用戶)在使用 WordPress 軟件時(shí)都設(shè)置為本土語言:英語。

因此,讓我們點(diǎn)擊查看細(xì)節(jié),進(jìn)一步查看該 translate() 函數(shù)在做什么。

在這里,我們看到兩間有趣的事。首先,在父函數(shù)中,有一個(gè)被調(diào)用了773次:__()。

查看該函數(shù)的源代碼后,我們發(fā)現(xiàn)它是 translate() 的包裝器。

    <?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 );
    }
    ?>

根據(jù)經(jīng)驗(yàn)法則,函數(shù)調(diào)用代價(jià)昂貴,應(yīng)該盡量避免。現(xiàn)在我們總是調(diào)用 __() 而不是 translate() ,我們應(yīng)該把別名改為 translate() 來保持向后兼容性,而 __() 則不再調(diào)用非必要的函數(shù)。

然而,實(shí)際上,這種改變不會(huì)帶來多大的差異,只是微觀的優(yōu)化罷了——但它的確提高了代碼可讀性,簡化了調(diào)用圖。

繼續(xù)前進(jìn),讓我們看看子函數(shù):


PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)
PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)

現(xiàn)在,深入該函數(shù),我們看到有 3 個(gè) 函數(shù)或方法被調(diào)用,每個(gè) 778 次:

  • get_translations_for_domain()
  • NOOP_Translations::translate()
  • apply_filters()

按照包容性實(shí)際運(yùn)行時(shí)間降序排列,我們看到 apply_filter() 是目前為止耗時(shí)最長的調(diào)用。

查看代碼:

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

這段代碼的作用是檢索一個(gè)翻譯對(duì)象,然后將 $translations->translate() 的結(jié)果傳給 apply_filter() 。我們發(fā)現(xiàn) $translations 是 NOOP_Translations 類的一個(gè)實(shí)例。

僅根據(jù)名稱(NOOP),再經(jīng)代碼中的注釋證實(shí),我們發(fā)現(xiàn)翻譯器實(shí)際上沒有任何動(dòng)作!

    <?php
    /**
     * Provides the same interface as Translations, but doesn't do anything
     */
    class NOOP_Translations {
    ?>

因此,也許我們完全可以避免這種代碼!

通過在代碼上進(jìn)行小規(guī)模調(diào)試,我們看到當(dāng)前使用的是默認(rèn)的域,我們可以修改代碼以忽略翻譯器:

    <?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 );
    }
    ?>

接下來,我們?cè)俅畏治觯_保要運(yùn)行至少兩次——確保所有緩存都建立,才是公平的對(duì)比!

PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)
PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)

這次運(yùn)行的確更快!但是,快多少?為什么?

使用 XHGui 的比較運(yùn)行這一特性就能找到答案。回到我們最初的運(yùn)行,點(diǎn)擊右上角的 “比較此處運(yùn)行” 按鈕,并從列表中選擇新的運(yùn)行。


PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)
PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)

我們發(fā)現(xiàn),函數(shù)調(diào)用的次數(shù)減少了3% ,包容性實(shí)際運(yùn)行時(shí)間減少 9% ,包容性CPU時(shí)間減少12%!

之后,可以按調(diào)用次數(shù)降序排列細(xì)節(jié)頁,這證實(shí)(如同我們的預(yù)期) get_translations_for_domain()NOOP_Translations::translate() 函數(shù)的調(diào)用次數(shù)減少。同樣,可以確認(rèn)沒有預(yù)料之外的變化發(fā)生。

PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)
PHP 性能分析第三篇: 性能調(diào)優(yōu)實(shí)戰(zhàn)

30 分鐘的工作帶來9 - 12% 的性能提升,這非常可喜。這就意味著真實(shí)世界的性能收益,即便是在應(yīng)用了 opcache 之后。

現(xiàn)在我們可以對(duì)其函數(shù)重復(fù)這個(gè)過程,直到找不到更多優(yōu)化點(diǎn)。

注意:此更改已提交到 WordPress.org 并已獲更新。你可以在 WordPress Bug Tracker 跟蹤討論,查看實(shí)踐過程。此更新計(jì)劃包含在 WordPress 4.1 版本中。

其他工具

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

New Relic & OneAPM

New RelicOneAPM 均提供前后端性能分析;洞察后臺(tái)堆棧訊息,包括 SQL 查詢與代碼分析,前端 DOM 與 CSS 呈現(xiàn),以及 Javascript 語句。OneAPM 更多功能請(qǐng)移步 (OneAPM 在線DEMO)

uprofiler

uprofiler 是目前還未發(fā)布的 Facebook XHProf 分支,該分支計(jì)劃刪除 Facebook 所需的 CLA。目前,兩者具備相同的特性,只有一些部分重命名了。

XHProf.io

XHProf.io 是 XHProf 的另一種用戶界面。XHProf.io 在配置文件存儲(chǔ)使用 MySQL ,用戶友好性方面不及 XHGui。

Xdebug

在 XHProf 出現(xiàn)之前,Xdebug 早已存在——Xdebug 是一種主動(dòng)的性能分析器,這意味著它不應(yīng)該用于生產(chǎn)環(huán)境,但可以深入了解代碼。

然而,它必須與另一個(gè)工具配合使用以讀取分析器的輸出 , 比如 KCachegrind。但是 KCachegrind 很難安裝在非 linux 機(jī)器上。另一個(gè)選擇是 Webgrind

Webgrind 無法提供 KCachegrind 的那些特性,但它是一個(gè) PHP Web 應(yīng)用程序,在任何環(huán)境都易于安裝。

若搭配 KCachegrind ,你可以輕易探索并發(fā)現(xiàn)性能問題。(事實(shí)上,這是我最喜歡的剖析工具!)

結(jié)語

分析和性能調(diào)優(yōu)是非常復(fù)雜的工程。有了對(duì)的工具,并理解如何善用這些工具,我們可以很大程度地提高代碼質(zhì)量——即使是對(duì)我們不熟悉的代碼庫。

花時(shí)間去探索和學(xué)習(xí)這些工具是絕對(duì)值得的。

注意:本文是我們的 PHP 性能分析系列的第三篇,閱讀 PHP 性能分析第一篇: XHProf & XHGui 介紹 ,和 PHP 性能分析第二篇: 深入研究 XHGui 。(本文系應(yīng)用性能管理領(lǐng)軍企業(yè) OneAPM 工程師編譯整理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容