簡介
Wikipedia、Facebook 和 Yahoo! 等主要 web 屬性使用 LAMP 架構來為每天數百萬的請求提供服務,而 WordPress、Joomla、Drupal 和 SugarCRM 等 web 應用程式軟體使用其架構來讓組織輕鬆部署基於 web 的應用程式。
該架構的優勢在於其簡單性。而 .NET 這樣的堆棧和 Java 技術可能使用大量硬體、昂貴的軟體棧和複雜的效能調優,LAMP 堆棧可以運行於商品硬體之上,使用開源軟體棧。由於軟體棧是一個鬆散的組件集,而非一個整體堆棧,效能調優是一大挑戰,因為需要分析和調優每個組件。
然而,這有幾個個簡單效能任務會對任何規模的網站的效能產生巨大的影響。在本文中,我們將探討旨在最佳化 LAMP 應用程式效能的 5 個這樣的任務。這些項目應當很少需要對您的應用程式進行架構更改,使其成為最大化您的 web 應用程式所需的響應能力和硬體需求的安全、便捷的選擇。
使用作業碼緩衝
提高任何 PHP 應用程式(當然是 LAMP 中的 “P”)的效能的最簡單方式是利用一個作業碼緩衝。對於我使用的任何網站,它是我確儲存在的一項內容,因為效能影響很大(很多時候有了作業碼緩衝,回應時間可減少一半)。但是對 PHP 不熟悉的大部分人的一個很大的疑問是,為何改進會如此之大。答案在於 PHP 如何處理 web 請求。圖 1 概覽了 PHP 請求的流程。
圖 1. PHP 請求
由於 PHP 是一種解釋語言,而非 C 或 Java 等編譯語言,對每個請求執行了 “解析-編譯-執行” 的整個步驟。您可以看到為何這會耗時、耗資源,特別是當指令碼在請求之間很少變化時。解析和編譯指令碼之後,指令碼作為一系列作業碼處於機器可解析狀態。這是作業碼緩衝發揮效用的地方。它作為一系列作業碼緩衝這些編譯指令碼,以避免為解析和編譯每個請求步驟。您將在圖 2 中看到這樣的工作流程是如何運作的。
圖 2. PHP 請求使用作業碼緩衝
因此當 PHP 指令碼的快取作業碼存在時,我們可以跳過 PHP 請求流程的解析和編譯步驟,直接執行快取作業碼並輸出結果。檢查演算法負責處理您可能對指令檔進行了更改的情況,因此在已變更指令碼的第一個請求後,會為隨後的請求自動重新編譯和快取作業碼,替換緩衝的指令碼。
作業碼緩衝對於 PHP 流行已久,其中早期的一些要追溯到 PHP V4 的全盛期。目前有一些流行選項正在積極開發和使用中:
- 替代 PHP 緩衝(APC)可能是 PHP 最流行的作業碼緩衝(參見 參考資料)。它由若干核心 PHP 開發人員所開發,做出了很大貢獻,Facebook 和 Yahoo! 的工程師賦予了其速度和穩定性。它還支援用於處理 PHP 請求的若干其他速度改進,包括一個使用者緩衝組件,這將在本文後面探討。
- Wincache 是主要由 Microsoft 的 Internet Information Services (IIS) 團隊積極開發的一個作業碼緩衝,僅供在使用 IIS 網頁伺服器的 Windows 上使用(參見 參考資料)。開發它的主要動力在於使 PHP 成為 Windows-IIS-PHP 堆棧上的一流開發平台,因為據知 APC 在該堆棧上運作的不是很好。它在功能上非常類似於 APC,且支援一個使用者緩衝組件,以及一個內建會話處理常式,以將 Wincache 作為一個會話處理常式直接加以利用。
- eAccelerator 是原始 PHP 緩衝之一 Turck MMCache 作業碼緩衝(參見 參考資料)的一個派生。不同於 APC 和 Wincache,它僅是一個作業碼緩衝和最佳化器,因此它不包含使用者緩衝組件。它在 UNIX 和 Windows 堆棧上完全相容,且對於不打算利用 APC 或 Wincache 提供的其他功能的網站很流行。如果您要使用 memcache 這樣的解決方案來為多 網頁伺服器環境提供一個單獨的使用者快取服務器,那麼這就是常見情況。
毫無疑問,一個作業碼緩衝是通過在每次請求後消除解析和編譯指令碼的需要來加速 PHP 的第一步。完成第一步之後,您應當看到回應時間和伺服器負載方面的改進。但是最佳化 PHP 可以做的不止這些,我們接下來將加以討論。
最佳化您的 PHP 設定
雖然實現作業碼緩衝是效能改進的一大創舉,不過也有大量其他最佳化選項可供您基於 php.ini 檔案中的設定最佳化您的 PHP 設定。這些設定更適合於生產執行個體;在開發或測試執行個體上,您可能不希望做這些變更,因為它會使得應用程式問題的調試變得更難。
讓我們看一下對於效能提升很重要的一些項目。
應當禁用的選項
有若干 php.ini 設定應當予以禁用,因為它們常用作向後相容性:
register_globals — 在 PHP V4.2 之前該功能常常是預設值,其中傳入的請求變數被自動賦給普通 PHP 變數。這樣做除了引起重大安全問題之外(使未過濾的傳入請求資料與普通 PHP 變數內容相混),對每一個請求這樣做還會產生開銷。因此禁用這一設定使您的應用程式更安全且能提高效能。
magic_quotes_* — 這是 PHP V4 的另一遺留項,其中傳入的資料會自動避開有風險的表單資料。它旨在作為一個安全特性,在將傳入的資料發送到資料庫之前對其進行整理,但不是很有效,因為它不能協助使用者預防常見的 SQL 插入式攻擊。由於大部分資料庫層支援能更好地處理該風險的準備語句,禁用該設定會再次消除這個煩人的效能問題。
always_populate_raw_post_data — 這僅當您出於某些原因需要查看傳入的未過濾 POST 資料的整個負載時才需要。否則,它僅在記憶體中儲存 POST 資料的一個副本,而這沒有必要。
然而,在遺留代碼上禁用這些選項會有風險,因為它們可能取決於其設定來實現正確執行。不應當基於被設定的這些選項來開發任何新代碼,而且可能的話,您應當尋求方法來重構您的現有代碼,避免使用它們。
應當禁用或調整設定的選項
您可以啟用 php.ini 檔案的一些優秀效能選項,來提升您的指令碼速度:
output_buffering — 您應當確保啟用該選項,因為它會以塊為單位將輸出刷回到瀏覽器,而非以每個 echo 或 print 語句為單位,而後者會大大減緩您的請求回應時間。
variables_order — 這個指令控制傳入請求的 EGPCS(Environment、Get、Post、Cookie 和 Server)變數解析順序。如果您沒有使用某種超全域變數(比如環境變數),您可以安全地刪除它們來獲得一點加速,從而避免在每一個請求上解析它們。
date.timezone — 這是在 PHP V5.1 中添加的一個指令,用於設定預設時區,然後用於後面將要介紹的 DateTime 函數。如果您不在 php.ini 檔案中設定該選項,PHP 會執行大量系統請求來弄清它是什麼,且在 PHP V5.3 中,對每一個請求會發出一個警告。
就以應當在您的生產執行個體上配置的設定而言,這些被看作是 “唾手可得”。就 PHP 而言,還有一件事需要考慮。這就是您的應用程式中 require() 和 include()(以及其同級 require_once() 和 include_once())的使用。這些函數最佳化您的 PHP 配置和代碼,以防止對每個請求進行不必要的檔案狀態檢查,從而減少回應時間。
管理您的 require() 和 include()
從效能來看,檔案狀態調用(即為檢查一個檔案是否存在而對底層檔案系統進行的調用)相當昂貴。檔案狀態的最大元兇之一以 require() 和 include() 語句的形式出現,這兩個語句用於將代碼帶到指令碼中。require_once() 和 include_once() 的同級調用更成問題,因為它們不僅需要驗證檔案是否存在,而且它之前沒有包含在內。
那麼解決這個問題的最好方式是什嗎?您可以做一些事來加快解決。
- 為所有
require() 和 include() 調用使用絕對路徑。這將使 PHP 更清楚您希望包含的確切檔案,因此無需為您的檔案檢查整個 include_path。
- 保持
include_path 中的條目數較低。這在很難為每個 require() 和 include() 調用提供絕對路徑的情況(通常在大型遺留應用程式中會出現這種情況)下很有用,方法就是不檢查您包含的檔案不在的位置。
APC 和 Wincache 還有用於緩衝 PHP 進行的檔案狀態檢查結果的機制,因此無需進行反覆的檔案系統檢查。當您將 include 檔案名稱保留為靜態而非變數驅動的時,它們最有效,因此儘可能嘗試這樣做很有用。
最佳化您的資料庫
資料庫最佳化很快會成為一個前沿話題,我幾乎沒有空間在這裡完全公正地做這個話題。但是如果您在尋求最佳化您的資料庫的速度,首先應當採取一些步驟,這應當對常見問題有所協助。
將資料庫放在自己的機器上
資料庫查詢自身可以變得相當激烈,通常在對大小合理的資料集執行簡單的 SELECT 語句時限定在 100% 的 CPU。如果您的 網頁伺服器和資料庫伺服器都在竟用單一機器上的 CPU 時間,這無疑將減慢您的請求速度。因此我想第一步最好是將 網頁伺服器和資料庫伺服器放在單獨的機器上,確保您的資料庫伺服器是兩者中更強健的(資料庫伺服器喜歡大量記憶體和多個 CPU)。
合理設計和編製表索引
資料庫效能的最大問題可能源自於不良資料庫設計和缺失索引。SELECT 語句通常是運行在典型 web 應用程式中的最常見的查詢類型。它們也是在資料庫伺服器上啟動並執行最耗時的查詢。此外,這些類型的 SQL 陳述式對適當的索引和資料庫設計最敏感,因此查看以下指示,擷取實現最優效能的技巧。
- 確保每個表都有一個主鍵。這為表提供一個預設順序和快速方式來聯結其他表。
- 確保一個表中的任何外鍵(即連結記錄到另一個表中的記錄的鍵)的索引得到合理編製。許多資料庫會自動對這些鍵施加約束,以便值真正匹配另一個表中的一條記錄,這有助於擺脫這一困難。
- 試圖限制一個表中的列數。一個表中有太多列比僅有一些列時進行查詢所需的掃描時間要長。此外,如果您有不常用的含多個列的一個表,您也在通過
NULL 值欄位浪費磁碟空間。文本或 blob 等可變大小欄位也是如此,其中表大小的增長可以遠超過需求。在這種情況下,您應當考慮將其他欄分成不同的表,在記錄的主鍵上將其聯合起來。
分析在伺服器上啟動並執行查詢
改進資料庫效能的最佳方法是分析在您的資料庫伺服器上運行什麼查詢,且運行它們需要多長時間。幾乎每個資料庫都有具有這種功能的工具。對於 MySQL,您可以利用慢查詢日誌來尋找有問題的查詢。要使用它,在 MySQL 設定檔中將 slow_query_log 設定為 1,然後將 log_output 設定為 FILE,將它們記錄到檔案 hostname-slow.log 中。您可以設定 long_query_time 閾值,確定查詢必須運行多少秒才被看作是 “慢查詢”。我想建議將該閾值首先設定為 5 秒,隨著時間的推移將其縮減為 1 秒,具體取決於您的資料集。如果您探究該檔案,您會看到類似於清單 1 的詳細查詢。
清單 1. MySQL 慢查詢日誌
/usr/local/mysql/bin/mysqld, Version: 5.1.49-log, started with:Tcp port: 3306 Unix socket: /tmp/mysql.sockTime Id Command Argument# Time: 030207 15:03:33# User@Host: user[user] @ localhost.localdomain [127.0.0.1]# Query_time: 13 Lock_time: 0 Rows_sent: 117 Rows_examined: 234use sugarcrm;select * from accounts inner join leads on accounts.id = leads.account_id; |
我們想要考慮的關鍵對象是 Query_time,顯示查詢需要的時間。另一項要考慮的是 Rows_sent 和 Rows_examined 的數量,因為這些可指這樣的情況:其中如果一個查詢察看太多行或返回太多行,就會被錯誤地書寫。您可以更深入地鑽研如何寫查詢,即在查詢開始處加上 EXPLAIN,它會返回查詢計劃,而非結果集,如清單 2 所示。
清單 2. MySQL EXPLAIN 結果
mysql> explain select * from accounts inner join leads on accounts.id = leads.account_id;+----+-------------+----------+--------+--------------------------+---------+---| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+----------+--------+--------------------------+---------+--------| 1 | SIMPLE | leads | ALL | idx_leads_acct_del | NULL | NULL | NULL | 200 | || 1 | SIMPLE | accounts | eq_ref | PRIMARY,idx_accnt_id_del | PRIMARY | 108 | sugarcrm.leads.account_id | 1 | |+----+-------------+----------+--------+--------------------------+---------+---------2 rows in set (0.00 sec) |
MySQL 手冊更深入探究 EXPLAIN 輸出的主題(參見 參考資料),但是我考慮的一項重要內容是 ‘type' 列為 ‘ALL' 的地方,因為這需要 MySQL 做一個全表掃描,且不需要鍵來執行查詢。這些協助您在添加索引時會大幅提高查詢速度。
有效快取資料
正如我們在上一節看到的,資料庫往往容易成為您 web 應用程式效能的最大痛點。但是如果您要查詢的資料不經常改變怎麼辦?在這種情況下,一個好的選擇就是在本機存放區這些結果,而非針對每個請求調用查詢。
我們之前探究的兩個作業碼緩衝 APC 和 Wincache 具有實現上述操作的工具,其中您可以將 PHP 資料直接儲存到一個共用記憶體段中,便於快速查詢。清單 3 提供了具體樣本。
清單 3. 使用 APC 快取資料庫結果的樣本
<?phpfunction getListOfUsers(){ $list = apc_fetch('getListOfUsers'); if ( empty($list) ) { $conn = new PDO('mysql:dbname=testdb;host=127.0.0.1', 'dbuser', 'dbpass'); $sql = 'SELECT id, name FROM users ORDER BY name'; foreach ($conn->query($sql) as $row) { $list[] = $row; } apc_store('getListOfUsers',$list); } return $list;} |
我們僅需一次執行查詢。之後,我們將結果推送到 getListOfUsers 鍵下的 APC 緩衝中。從這裡開始,直到緩衝到期,您就能夠直接從緩衝中擷取結果數組,跳過 SQL 查詢。
APC 和 Wincache 並非一個使用者緩衝的惟一選擇;memcache 和 Redis 是不需要您在與 Web 服務器相同的伺服器上運行使用者緩衝的其他流行選擇。這就提高了效能和靈活性,特別是當您的 web 應用程式跨多個 Web 服務器向外擴充時。
在本文中,我們探究了調優您的 LAMP 效能的 5 種簡單方法。我們不僅通過利用一個作業碼緩衝和最佳化 PHP 配置探究了 PHP 層級的技術,而且探究了如何最佳化您的資料庫設計來實現合理的索引編製。我們還探討了如何利用一個使用者緩衝(以 APC 為例)來展示如何在資料不經常改變時避免重複的資料庫調用。