PHP 性能分析第三篇: 性能調優實戰

注意:本文是咱們的 PHP 性能分析系列的第三篇,點此閱讀 PHP 性能分析第一篇: XHProf & XHGui 介紹 ,或  PHP 性能分析第二篇: 深刻研究 XHGui php

在本系列的 第一篇 中,咱們介紹了 XHProf 。而在 第二篇 中,咱們深刻研究了 XHGui UI, 如今最後一篇,讓咱們把 XHProf /XHGui 的知識用到工做中!html

性能調優

不用運行的代碼纔是絕好的代碼。其餘只是好的代碼。因此,性能調優時,最好的選擇是首先確保運行儘量少的代碼。前端

OpCode 緩存mysql

首先,最快且最簡單的選擇是啓用 OpCode 緩存。OpCode 緩存的更多信息能夠在 這裏 找到。linux

PHP 性能分析第三篇: 性能調優實戰

在上圖,咱們看到啓用 Zend OpCache 後發生的狀況。最後一行是咱們的基準,也即沒有啓用緩存的狀況。laravel

在中間行,咱們看到較小的性能提高,以及內存使用量的大幅減小。小的性能提高(極可能)來自 Zend OpCache 優化,而非 OpCode 緩存。git

第一行是優化和 OpCode 緩存後結果,咱們看到很大的性能提高。github

PHP 性能分析第三篇: 性能調優實戰

如今,咱們看看 APC 以前和以後的變化。如上圖所示,跟 Zend OpCache 相比,隨着緩存的創建,咱們看到初始(中間行)請求的性能降低,在消耗時長與內存使用量方面的表現都明顯降低。web

接着,隨之 opcode 緩存的創建,咱們看到相似的性能提高。redis

內容緩存

第二件咱們能作的事是緩存內容——這對 WordPress 而言小菜一碟。它提供了許多安裝簡便的插件來實現內容緩存,包括 WP Super Cache。WP Super Cache 會建立網站的靜態版本。該版本會在出現諸如評論事件時依照網站設置自動過時。(例如,在很是高負載狀況下,您可能會想禁止任何緣由形成的緩存過時)。

內容緩存只能在幾乎沒有寫操做時有效運行,寫操做會使緩存失效,而讀操做不會。

你也應該緩存應用從第三方 API 處收到的內容,從而減小因爲 API 可用性致使的延遲與依賴。
 
WordPress 有兩個緩存插件,能夠大大提升網站的性能: W3 Total Cache 和 WP Super Cache

這兩個插件都會建立網站的靜態 HTML 副本,而不是每次收到請求時再生成頁面,從而壓縮響應時間。

若是你正在開發本身的應用程序,大多數框架都有緩存模塊: 

查詢緩存

另外一個緩存選項是查詢緩存。針對 MySQL,有一個通用的查詢緩存幫助極大。對於其餘數據庫,將查詢結果集緩存在 Memcached 或者 cassandra 這樣的內存緩存,也很是有效。

跟內容緩存同樣,查詢緩存在包含大量讀取操做的場景是最有效的。因爲少許的數據改動就會使大塊的緩存區無效,尤爲不能在這種狀況下依賴 MySQL 查詢緩存來提升性能。

查詢緩存或許在生成內容緩存時對性能有提高。

以下圖所示,當咱們開啓查詢緩存後,實際運行時間減小了 40% ,儘管內存使用量沒有明顯改變。
PHP 性能分析第三篇: 性能調優實戰

現有三種類型的緩存選項,由 query_cache_type 控制設置。

  • 設置值爲 0OFF 將禁用緩存

  • 設置值爲 1ON 將緩存除了以 SELECT SQL_NO_CACHE 開頭以外的全部選擇

  • 設置值爲 2DEMAND 只會緩存以 SELECT SQL_CACHE 開頭的選擇

此外,你應該將 query_cache_size 設置爲非零值。將它設置爲零將禁用緩存,無論 query_cache_type 是否設置。

想獲得設置緩存的幫助,與許多其餘性能相關的設置,請查看 mysql-tuning-primer 腳本。

MySQL 查詢緩存的主要問題是,它是全局的。對緩存結果集構成的表格的任何更改都將致使緩存失效。在寫入操做頻繁的應用程序中,這將使緩存幾乎無效。

然而,你還有許多其餘選擇,能夠根據你的需求和數據集創建更多的智能緩存,例如 Memcached , riak , cassandra 或 redis

查詢優化

如前所述,數據庫查詢經常是程序執行緩慢的緣由,查詢優化每每能比代碼優化帶來更多切身的好處。

查詢優化有助於生成內容緩存時提升性能,並且,在沒法緩存這種最壞的狀況下也有益處。

除了分析, MySQL 還有一個幫助識別慢查詢的選擇——慢查詢日誌。慢查詢日誌會記錄全部耗時超過指定時間的查詢,以及不使用索引的查詢(後者爲可選項)。

您能夠在 my.cnf 中使用如下配置啓用日誌。

[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 安裝時使用這些選項 ,主頁加載完成並運行後獲得以下數據: 

$ 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.00 s,這意味着他們的耗時在 1 秒的閾值如下,且沒有使用索引。

在 MySQL 控制檯 使用 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,從而確認未使用索引。

EXPLAIN 是對優化 MySQL 查詢很是強大的工具,更多信息能夠在 這裏 找到。

PostgreSQL 一樣也包括一個 EXPLAIN (該 EXPLAIN 與 MySQL 的差異很大),而 MongoDB 有$explain 元 操做符

代碼優化

一般只有當你再也不受到 PHP 自己限制(經過使用 OpCode 緩存),緩存了儘量多的內容,優化了查詢以後,才能夠開始調整代碼。

代碼和查詢優化帶來足夠的性能提高才能建立其餘緩存;代碼在最糟糕的環境(沒有緩存)下性能越高,應用就越穩定,重建緩存的速度也就越快。

讓咱們看看如何(潛在地)優化咱們的 WordPress 安裝。

首先,讓咱們看看最慢的函數: 
PHP 性能分析第三篇: 性能調優實戰

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

WordPress 代碼庫的特色是,經過基於事件的過濾系統執行多種數據轉換,執行次序按照數據經內核、插件添加或回調的順序。

apply_filter() 函數是這些回調應用的地方。

首先,你可能會注意到,函數被調用 4194 次。若是咱們點擊查看更多細節,就能夠按照「調用次數」降序排列「父函數」,從而發現 translate() 調用了__apply_filter()__ 函數 778 次。
PHP 性能分析第三篇: 性能調優實戰

這頗有趣,由於實際上我不使用任何翻譯。我(並懷疑大多數用戶)在使用 WordPress 軟件時都設置爲本土語言:英語。

所以,讓咱們點擊查看細節,進一步查看該 translate() 函數在作什麼。

在這裏,咱們看到兩間有趣的事。首先,在父函數中,有一個被調用了773次:__()。

查看該函數的源代碼後,咱們發現它是 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 );
    }
    ?>

根據經驗法則,函數調用代價昂貴,應該儘可能避免。如今咱們老是調用 __() 而不是 translate() ,咱們應該把別名改成 translate() 來保持向後兼容性,而 __() 則再也不調用非必要的函數。

然而,實際上,這種改變不會帶來多大的差別,只是微觀的優化罷了——但它的確提升了代碼可讀性,簡化了調用圖。

繼續前進,讓咱們看看子函數:
PHP 性能分析第三篇: 性能調優實戰

如今,深刻該函數,咱們看到有 3 個 函數或方法被調用,每一個 778 次:

  • get_translations_for_domain()

  • NOOP_Translations::translate()

  • apply_filters()

按照包容性實際運行時間降序排列,咱們看到 apply_filter() 是目前爲止耗時最長的調用。

查看代碼:

    <?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() 。咱們發現 $translations 是 NOOP_Translations 類的一個實例。

僅根據名稱(__NOOP__),再經代碼中的註釋證明,咱們發現翻譯器實際上沒有任何動做!

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

所以,也許咱們徹底能夠避免這種代碼!

經過在代碼上進行小規模調試,咱們看到當前使用的是默認的域,咱們能夠修改代碼以忽略翻譯器:

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

接下來,咱們再次分析,確保要運行至少兩次——確保全部緩存都創建,纔是公平的對比!
PHP 性能分析第三篇: 性能調優實戰

此次運行的確更快!可是,快多少?爲何?

使用 XHGui 的比較運行這一特性就能找到答案。回到咱們最初的運行,點擊右上角的 「比較此處運行」 按鈕,並從列表中選擇新的運行。
PHP 性能分析第三篇: 性能調優實戰

咱們發現,函數調用的次數減小了3% ,包容性實際運行時間減小 9% ,包容性CPU時間減小12%! 

以後,能夠按調用次數降序排列細節頁,這證明(如同咱們的預期) get_translations_for_domain()NOOP_Translations::translate() 函數的調用次數減小。一樣,能夠確認沒有預料以外的變化發生。

PHP 性能分析第三篇: 性能調優實戰

30 分鐘的工做帶來9 - 12% 的性能提高,這很是可喜。這就意味着真實世界的性能收益,即使是在應用了 opcache 以後。

如今咱們能夠對其函數重複這個過程,直到找不到更多優化點。

注意:此更改已提交到 WordPress.org 並已獲更新。你能夠在 WordPress Bug Tracker 跟蹤討論,查看實踐過程。此更新計劃包含在 WordPress 4.1 版本中。

其餘工具

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

New Relic & OneAPM

New Relic 與 OneAPM  均提供先後端性能分析;洞察後臺堆棧訊息,包括 SQL 查詢與代碼分析,前端 DOM 與 CSS 呈現,以及 Javascript 語句。__OneAPM__ 更多功能請移步 (OneAPM 在線DEMO

uprofiler

uprofiler 是目前還未發佈的 Facebook XHProf 分支,該分支計劃刪除 Facebook 所需的 CLA。目前,二者具有相同的特性,只有一些部分重命名了。

XHProf.io

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

Xdebug

在 XHProf 出現以前,Xdebug 早已存在——Xdebug 是一種主動的性能分析器,這意味着它不該該用於生產環境,但能夠深刻了解代碼。

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

Webgrind 沒法提供 KCachegrind 的那些特性,但它是一個 PHP Web 應用程序,在任何環境都易於安裝。

若搭配 KCachegrind ,你能夠輕易探索並發現性能問題。(事實上,這是我最喜歡的剖析工具!)

結語

分析和性能調優是很是複雜的工程。有了對的工具,並理解如何善用這些工具,咱們能夠很大程度地提升代碼質量——即便是對咱們不熟悉的代碼庫。

花時間去探索和學習這些工具是絕對值得的。

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

相關文章
相關標籤/搜索