PHP 應用提速 - 第 2 部分: 分析 PHP 應用程序以查找、診斷和加速運行緩慢的代碼

爲 PHP 應用提速” 系列文章的 第 1 部分 演示瞭如何使用 XCache(PHP 操作碼緩存) 加速整個站點。XCache(僅是少數幾種緩存包中的一種)保留了編譯過程的輸出,去掉了其他冗餘的工作。只要頁面沒有發生變化,緩存後的頁面就能夠勝任代理的作用。當頁面發生變化時,緩存後的頁面就會變爲無效並被替換掉。

操作碼緩存 —— 以及一個操作碼優化器,通常由相同的包提供 —— 是一種加快站點響應的低成本技術。很多緩存包是免費的,並且是開源的,無需改變任何代碼即可從中受益。

當然,在某些應用程序中,相比較實際的執行時間,將 PHP 源代碼文件翻譯爲其相應的操作碼所需的時間微不足道。連接到遠程數據庫服務器,使用低效的 SQL 語句進行查詢,以及其他大量解析和操作數據的工作都非常的繁瑣,也因此增加了開銷,甚至產生浪費。良好的網絡設計和靈巧的數據庫結構可以使時間冗長和查詢緩慢的情況有所改善,如果需要的話還可以向友好的專家請求幫助。但是,如果代碼運行緩慢,您可能更希望自己處理。

但是從何開始呢?正如人們普遍認爲的,在代碼完成前調試代碼的做法很不明智 —— 因爲代碼的首次實現可能會非常的迅速。當代碼正確且能實現相應的功能時,不管其表面上看起來運行緩慢還是實際如此,首先要做的就是對其性能進行測試或基準測試。不執行這樣的診斷而嘗試去優化代碼無疑是在黑暗中摸索。

一個簡單的性能指標是掛鐘時間(wall clock time),或測量頁面請求與完成呈現之間的實際延遲。對於某些情況 —— 比如在您自己的工作站本地運行的 Web 服務器、數據庫和瀏覽器 —— 掛鐘時間能夠提供信息。然而,掛鐘時間對於其他大多數情況而言並無實際意義,比如網絡延遲時間、活動的 Web 服務器或者活動的數據庫。

一種更精確的測量 —— 甚至可以測量運行單個源代碼語句的時間 —— 可以採用代碼分析器。分析器通常被實現爲 PHP 運行時引擎的擴展,記錄語句開始和結束的 delta、記錄程序開始和結束之間的 delta 並捕獲對來到的請求形成響應的總時間。有了這種垂直度,就可以將語句、循環、函數、類或者是運行緩慢的庫作爲分析目標。如果不是時間而是內存使用出現了問題,那麼一個優秀的分析器還可以顯示組件的內存佔用情況。

PHP 的一個較流行的分析器是 Xdebug,它還爲交互地調試 PHP 應用程序提供了服務器掛鉤(hook)。(參見“調試的更好方法”以瞭解更多信息。該系列的另一部分將探討高級交互式調試。) Xdebug 很容易從源代碼構建,將其作爲 Zend 擴展進行安裝也非常簡單。(現在已有針對某些平臺的二進制文件。)當就緒後,對基於 PHP 頁面的每個請求都將生成可在 KCacheGrind 中查看的數據集。

構建並安裝 Xdebug

如果具備了 PHP 實用工具 phpizephp-config,而且具有對系統的 php.ini 配置文件的訪問權,那麼安裝和設置 Xdebug 只需幾分鐘的時間。下面給出的指導說明針對 Linux®,不過在 Mac OS X 上的安裝步驟實際上與此類似。(您可以從 Xdebug Web 站點找到針對 Microsoft® Windows® 的 Xdebug 預編譯版本。)

Xdebug 的最新版本爲 V2.0.0RC3(最終版本 V2.0.0 在您閱讀此文時也許已經可用)。下載並解包 tarball,然後切換到源代碼的子目錄。確保 phpizephp-config 位於 shell 的 PATH,準備使用 phpize 進行構建。


清單 1. 設置 Xdebug

				
$ wget http://www.xdebug.org/files/xdebug-2.0.0RC3.tgz
$ tar xzf xdebug-2.0.0RC3.tgz
$ cd xdebug-2.0.0RC3/xdebug-2.0.0RC3
$ phpize
Configuring for:
PHP Api Version:         20020918
Zend Module Api No:      20020429
Zend Extension Api No:   20050606

 

phpize 的產品是一個腳本 —— 名爲配置 —— 它對餘下的構建過程進行配置。要構建 Xdebug,在 make 後緊接着輸入 ./configure 即可。


清單 2. 構建 Xdebug

				
$ ./configure
checking build system type... i686-apple-darwin8.8.1
checking host system type... i686-apple-darwin8.8.1
checking for egrep... grep -E
...
$ make
...
Build complete.
(It is safe to ignore warnings about tempnam and tmpnam).

 

make 命令生成 Xdebug 擴展,xdebug.so。剩下的工作就是使用 sudo make install 進行安裝。

$ sudo make install
Installing shared extensions: /usr/lib/php/extensions/no-debug-non-zts-20020429/

 

注: 如果在終端窗口中運行最後一個命令,請選擇並複製最後一步中發出的目錄。在下一個步驟中將會用到它。

最後,要使配置數據可視化,必須使用 KCacheGrindGraphViz。包含 K Desktop Environment (KDE)的 Linux 發行版很可能已經含有了 KCacheGrindGraphViz。如果沒有包含,適合您所使用的 Linux 的那些版本也不難找到。Debian 用戶可以使用 Advanced Packaging Tool (APT) 快速安裝 KCacheGrindGraphViz 以及所有包的依賴關係。


清單 3. 安裝 KCacheGrind

				
$ apt-cache search kcachegrind
valgrind-callgrind - call-graph skin for valgrind
kcachegrind - visualisation tool for valgrind profiling output
kcachegrind-converters - format converters for KCachegrind profiling visualisation tool
$ apt-cache search graphviz
graphviz - rich set of graph drawing tools
graphviz-dev - graphviz Libs and Headers against which to build applications
graphviz-doc - additional documentation for graphviz
libdeps-renderer-dot-perl - DEPS renderer plugin using GraphViz/dot
...
$ sudo apt-get install kcachegrind graphviz 
...

 

如果沒有將 KDE 安裝到系統中,KCacheGrindGraphViz 以及所有必要的內容將佔用大約 90 MB 的磁盤空間。

 


 

配置 Xdebug

安裝了 Xdebug 擴展後,就可以準備啓用和配置該擴展了。在文本編輯器中打開 php.ini,並添加以下代碼行。


清單 4. 啓用和配置該擴展

				
zend_extension = /usr/lib/php/extensions/no-debug-non-zts-20020429/xdebug.so
xdebug.profiler_output_dir = "/tmp/xdebug/"
xdebug.profiler_enable = Off
xdebug.profiler_enable_trigger = 1

 

第一行 zend_extension 加載 Xdebug 擴展。第二行命名放置分析器輸出的目錄。如果需要的話,創建命名的目標並更改其模式以允許用戶對 Web 服務器進行寫訪問。

第三行禁用了分析器。然而,第四行將在設置 HTTP GETPOST 參數 XDEBUG_PROFILE 時啓用分析器。(如果您希望一直使用分析器,在第三行代碼中將 Off 更改爲 On。)

添加這幾行代碼並驗證了輸出目錄是可寫的,然後重新啓動 Web 服務器。對於其他 PHP 擴展,要驗證 Xdebug 是否安裝並可用,可以創建一個簡單的骨架 PHP 程序來調用 phpinfo() 並查看結果。應該能夠看到類似於圖 1 所示的內容。(爲簡便起見,省略了完整輸出的部分內容。)

圖 1. Phpinfo 指明 Xdebug 是否已安裝
Phpinfo 指明 Xdebug 是否已安裝

您還可以向下滾動到 Zend 徽標。如果正確安裝並配置了 Xdebug,它將顯示在徽標的旁邊。

 


 

使用分析器

要分析代碼,只需將瀏覽器指向 PHP 應用程序即可。如果您將分析器設置爲對每個觸發逐個解決的方式,將 XDEBUG_PROFILE=1 追加到 URL 中,或者,像下面一樣,將參數嵌入到表單中。

作爲一個示例,我們來分析一下這個簡單的 ACME Fibonacci Maker,fibonacci.php,如清單 5 所示。爲方便起見,將 XDEBUG_PROFILE 參數設置在表單的隱藏變量內。(當代碼投入生產時,很可能將禁用 Xdebug,呈現這個變量將不會造成什麼損失。)


清單 5. Fibonacci.php

				
<?php
  function fib($nth = 1) {
    if ( $nth < 2 ) {
      return( $nth ); 
    }
    
    return( fib( $nth - 1) + fib( $nth - 2 ) );
  }
?> 
  
<html>
  <head>
    <title>ACME Fibonacci Maker</title>
  </head>
  <body>
    <h2>Try the ACME Fibonacci Maker!</h2>
    <form action="fibonacci.php" method="POST">
    <input type="hidden" name="XDEBUG_PROFILE" value="1" />
    Enter a number: <input type="text" name="n"></input>
    </form>
    <hr />

<?php  
  if ( ! empty( $_REQUEST['n'] ) ) {
    $n = $_REQUEST['n'] % 10;
    $suffix = array( 1 => "st", 2 => "nd", 3 => "rd" );
    if ( $_REQUEST['n'] < 4 || $_REQUEST['n'] > 20 ) {
      $suffix = $suffix[$n];
    }
    else {
      $suffix = 'th';
    }
    
    echo '<p>The ' . $_REQUEST['n'] . $suffix .' Fibonacci number is ';
    echo fib( $_REQUEST['n'] ) . '</p>';
  }
?>
  </body>
</html>

 

將瀏覽器指向 http://localhost/fibonacci.php(或者合適的 URL)並輸入數字 —— 比如,16。其結果 —— Fibonacci 系列的第 16 個元素 —— 如圖 2 所示。


圖 2. 示例 Fibonacci 應用程序
示例 Fibonacci 應用程序

如果將分析器輸出目錄中的內容(名爲 php.ini)列出來的話,應該能看到類似 cachegrind.out.951917687 這樣名稱的文件。cachegrind.out. 前綴是固定的。默認情況下,數值後綴是目錄路徑到 fibonacci.php 文件的 CRC32 散列。因此,如果每一個應用程序都位於自己的目錄,那麼每個程序的輸出將根據文件名而被分隔。(如果您更喜歡將輸出與時間相關聯,將下面這行代碼:

xdebug.profiler_output_name = timestamp

 

添加到 php.ini。)

從終端窗口啓動 KCacheGrind 並打開 cachegrind.out.951917687。將立即打開一個類似於圖 3 的新窗口。


圖 3. KCacheGrind 應用程序
KCacheGrind 應用程序

單擊 Callees 選項卡,雙擊源代碼中突出顯示的行,並從 Grouping 列表選擇 Source File 。所看到的視圖應變爲類似圖 4 所示的內容。


圖 4. 查看結果
查看結果

正如您預期的一樣,實際上全部的處理時間(70,989 毫秒的 99.87%)都花費在 3193 次對 fib() 函數的調用上了。要加快該應用程序(隨着進一步執行 Fibonacci 序列,程序會隨之變慢),應該避免重新計算 Fibonacci 數字這樣代價高昂的重複工作。事實上,ACME Fibonacci Maker 能夠很好地進行計算重用。

下面展示了 fib() 函數的優化版本。新的版本用內存換來了時間上的節省,因爲它保留了中間的計算以便以後使用。圖 5 展示了分析結果:與上次的 3192 次函數調用相比,這裏僅需要 30 次調用(並且只有一半的調用需要計算結果),而時間則減少爲只有 20 毫秒。


清單 6. 更新了的 fib() 函數

				
function fib($nth = 1) {
  static $fibs = array();

  if ( ! empty ($fibs[$nth] ) ) { 
    return( $fibs[$nth] );
  }
  
  if ( $nth < 2 ) {
    $fibs[$nth] = $nth;
  }
  else {  
    $fibs[$nth - 1] = fib( $nth - 1 );
    $fibs[$nth - 2] = fib( $nth - 2 );
    $fibs[$nth] = $fibs[$nth - 1] + $fibs[$nth -2];
  }
  
  return( $fibs[$nth] );
}
?>


圖 5. 加快了的 Fibonacci 函數
加快了的 Fibonacci 函數

雖然單次運行應用程序能夠指出一些問題(可以試試上面原始的應用程序中的 Fibonacci 序列的第 50 個元素 ),通常,還是需要通過幾次調用收集統計信息以及查看模式。

如果保留默認的 “crc32” 命名模式,每次運行 fibonacci.php 時,將重寫數據文件。然而,可以通過在 php.ini 中設置 xdebug.profiler_append = 1 改變這種行爲並將後續運行追加到相同的文件。更改之後重新啓動 Web 服務器。

圖 6 顯示了三次運行 Fibonacci Maker 之後數據合計的示例。總時間稍大於兩秒;其中 99.97% 的時間花費在了 fib() 上。圖 6 顯示了 Call Graph 選項卡,它由 GraphVizdot 工具生成。關於 KCacheGrind 的具體用法不在本文討論的範圍之內,但是可以從網上獲得其完整的文檔。KCacheGrind 可以以很多種方法對數據進行交叉分析,根據您希望解決的問題選擇合適的方法。

圖 6. 合計分析數據
合計分析數據

調試的更好方法

除了分析 PHP 應用程序,還可以在發生錯誤並進行交互式調試時,配置 Xdebug 擴展(如其名字暗示的一樣)來提供詳細的棧跟蹤和錯誤消息。棧跟蹤和錯誤消息可以指出錯誤的原因,而交互式調試允許每次逐步調試代碼中的一條指令,查看程序變量的類型和值,並檢查所有的 PHP 超全局變量,包括進來的請求參數。

本系列的下一篇文章將具體介紹交互式調試。同時,您可以啓用幾個 Xdebug 特性來說明應用程序在發生錯誤時的狀態:

  • 無論何時只要應用程序出現錯誤,設置 xdebug.default_enable=On 顯示棧跟蹤。如果您已經花費時間安裝了 Xdebug,那麼只要進行代碼開發就啓用這個特性。
  • 還可以設置 xdebug.show_local_vars=1 來進一步顯示最頂部範圍內的所有變量。
  • xdebug.var_display_max_childrenxdebug.var_display_max_dataxdebug.var_display_max_depth> 是相關的三個設置,分別用來控制因 xdebug.show_local_vars 的使用而顯示的變量的屬性數、字符串長度和嵌套深度。

可以在 Xdebug Web 站點找到更多信息。

 


 

分析類

如果沒有具體的代碼,那麼很難演示具有意義的分析,下面這個示例是十分典型的代碼,展示了從中所能獲得的信息。清單 7 顯示了一個裝配玩具火箭的應用程序(人爲設計)。這種玩具火箭由幾個部分組成,生產每一個部分都需要一定的時間。在 PHP 中,使用類代表每個組成部分,使用實例方法表示每個部分的構造時間。您可以將這個玩具看作是一個應用程序,並把每個部分看作是該應用程序的功能。

清單 7. 模擬玩具裝配的一組 PHP 類

				
<?php
    define( 'BOOSTER', 5 );
    define( 'CAPSULE', 2 );
    define( 'MINUTE', 60 );
    define( 'STAGE', 3 );
    define( 'PRODUCTION', 1000 );
    
    class Part {
        function Part() {
            $this->build( MINUTE );
        }
        
        function build( $delay = 0 ) {
            if ( $delay <= 0 )
                return;
                
            while ( $delay-- > 0 ) {
            }
        }
    }
    
    class Capsule extends Part {
        function Capsule() {
          parent::Part();
            $this->build( CAPSULE * MINUTE );
        }
    }
    
    class Booster extends Part {
        function Booster() {
          parent::Part();
            $this->build( BOOSTER * MINUTE );
        }
    }
    
    class Stage extends Part {
        function Stage() {
          parent::Part();
          $this->build( STAGE * MINUTE );
        }
    }
    
    class SpaceShip {
        var $booster;
        var $capsule; 
        var $stages;
        
        function SpaceShip( $numberStages = 3 ) {
            $this->booster = new Booster();
            $this->capsule = new Capsule();
            $this->stages = array();
            
            while ( $numberStages-- >= 0 ) {
                $stages[$numberStages] = new Stage();
            }
        }
    }
    
    $toys = array();
    $count = PRODUCTION;
    
    while ( $count-- >= 0  ) {
      $toys[] = new SpaceShip( 2 );
    }
?>

<html>
<head>
<title>
Toy Factory Output
</title>
</head>
<body>
  <h1>Toy Production</h1>
  <p>Built <? echo PRODUCTION . ' toys' ?></p>
</body>
</html>

 

運行這些代碼將生成一個新的數據文件。同樣,將數據加載到 KCacheGrind。如果切換到 SourceCall Graph 選項卡,將看到類似圖 7 所示的視圖。

圖 7. 太空船應用程序的配置文件
太空船應用程序的配置文件

Flat Profile 窗格(左面)顯示了應用程序調用的所有函數(方法)。最左面的列展示了近似的累計總數,第二列展示了每種方法的單獨測試,第三列列出了調用該方法的次數。在調用圖表中使用有顏色的方塊反映圖表內容,這非常方便,能夠很容易地將事件序列與其花費的時間關聯起來。

很明顯,構建階段所使用的時間代價最昂貴。構建每一部分所需的系統開銷(使用 Part 的構造器表示)次之。再看一下 PHP 自身的 define() 函數,它只花費了很少的開銷。

最後,還可以查看內存的使用情況。從靠近頂部的下拉菜單中選擇 MemoryClass,然後切換到頂部以及底部的 TypesCaller Map 選項卡。您看到的屏幕應該類似圖 8。


圖 8. 太空船應用程序的內存使用情況
太空船應用程序的內存使用情況


 

找回週期

和其他衆多 PHP 擴展一樣,Xdebug 容易構建、安裝快捷且易於配置 —— 所有這些工作 10 分鐘內即可完成。如果您已經優化了 Apache 安裝並且對應用程序進行了緩存,但是性能仍然很差,那麼可以考慮一下代碼的運行。算法是否有效?代碼是否過於複雜?是否重複實現了 PHP 已提供的函數?

當然,如果不能判斷出應用程序的瓶頸所在,那麼就必須進行查找並加以修復。不要只憑猜測 —— 要進行分析!您可能會驚訝於寶貴的計算週期是如何被輕意耗費掉的。

並且永遠不要忘記:要在生產服務器中禁用 Xdebug,因爲啓用它總會增加系統開銷。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章