1. 程式人生 > >對 Web 應用程式進行效能調優

對 Web 應用程式進行效能調優

動態的 Web 應用程式能夠儲存大量資訊,讓使用者能夠通過熟悉的介面立即訪問這些資訊。但是,隨著應用程式越來越受歡迎,可能會發現對請求的響應速度沒有以前那麼快了。開發人員應該瞭解 Web 應用程式處理 Web 請求的方式,知道在 Web 應用程式開發中可以做什麼,不能做什麼,這有助於減少日後的麻煩。

靜態的 Web 請求(比如圖 1 所示的請求)很容易理解。客戶機連線伺服器(通常通過 TCP 埠 80),使用 HTTP 協議發出一個簡單的請求。

圖 1. 客戶機通過 HTTP 請求靜態的檔案
客戶機通過 HTTP 請求靜態的檔案

伺服器解析這個請求,把它對映到檔案系統上的一個檔案。然後,伺服器向客戶機發送一些描述有效負載(比如網頁或影象)的響應頭,最後向客戶機發送檔案。

在上面的場景中可能出現幾個瓶頸。如果請求的變化很大,導致無法有效地使用作業系統的磁碟快取,那麼伺服器的磁碟會很忙,到了某種程度之後,就會減慢整個過程。如果為客戶機提供資料的網路通道飽和了,就會影響所有客戶機。但是,除了這些狀況之外,“接收請求,傳送檔案” 過程還是相當高效的。

通過做一些假設,可以大致體會靜態伺服器的效能。假設一個請求的服務時間是 10ms(主要受到磁頭尋道時間的限制),那麼大約每秒 100 個請求就會使磁碟接近飽和(10msec/request / 1 second = 100 requests/second)。如果要傳送 10K 的文件,就會產生大約 8mbit/sec 的 Web 通訊流(100 requests/second * 10 KBytes/request * 8bits/byte)。如果可以從記憶體快取中獲取檔案,就可以降低平均服務時間,因此增加伺服器每秒能夠處理的連線數。如果您有磁碟服務時間或平均請求延時的真實資料,可以把它們放進上面的算式,從而計算出更準確的效能估計值。

既然伺服器的處理容量是平均請求服務時間的倒數,那麼如果服務時間加倍,伺服器的處理容量(每秒處理的連線數)就會減半。請記住這一點,下面看看動態應用程式的情況。

動態應用程式的流程依賴於應用程式的具體情況,但是一般情況下與圖 2 相似。

圖 2. 客戶機通過 HTTP 請求動態頁面
客戶機通過 HTTP 請求動態頁面

與前一個示例中的客戶機一樣,圖 2 中的客戶機首先發出一個請求。靜態請求和動態請求之間實際上沒什麼差異(有時候 .php 或 .cgi 等副檔名可能意味著動態請求,但是它們可能引起誤解)。如何處理請求是由 Web 伺服器決定的。

圖 2 中,請求被髮送到一個應用伺服器,比如執行一個 Java™ 應用程式的 Solaris 系統。應用伺服器執行一些處理,然後向資料庫查詢更多的資訊。得到這些資訊之後,應用伺服器生成一個 HTML 頁面,這個頁面由 Web 伺服器轉發給客戶機。因此,這個請求的服務時間是幾個部分的總和。如果資料庫訪問花費 7ms,應用伺服器花費 13ms,Web 伺服器花費 5ms,那麼網頁的服務時間就是 25ms。根據前面介紹的倒數規則,各個元件的容量分別是每秒 142、77 和 200 個請求。因此,瓶頸是應用伺服器,它使這個系統每秒只能處理 77 個連線;超過這個數量之後,Web 伺服器被迫等待,連線開始排隊。

但是,一定要注意一點:因為系統每秒只能分派 77 個連線,而一個連線需要的處理時間是 25ms,所以並非每個應用程式使用者的請求都能夠在 25ms 內得到處理。每個元件每次只能處理一個連線,所以在高峰負載下,請求不得不等待 CPU 時間。在上面的示例中,考慮到排隊時間和 25ms 的處理時間,平均請求服務時間最終會超過 1.1 秒。關於解決這些排隊問題的更多資訊,請參見 參考資料

通過這些示例可以得出以下結論:

  • 在使用者發出請求和獲得最終頁面之間的步驟越多,整個過程就越慢,系統容量就越低。
  • 隨著頁面請求速率的增加,這種效應會越來越顯著。
  • 在專案開始時做出的體系結構決策也會影響站點處理負載的能力。

本文的其餘部分將深入討論這些問題。

用於動態站點的 N 層體系結構

應用程式(包括 Web 應用程式)的體系結構常常按照層來描述。靜態站點可以被看作只有一層 —— Web 伺服器。如果用 Web 伺服器執行某種指令碼語言(比如 PHP),從而連線資料庫,那麼這可以看作兩層。前一節中的示例有三層,即前端 Web 伺服器、應用伺服器和資料庫。

一個軟體也可能由多層組成,這取決於您談話的物件。例如,PHP 指令碼可能使用一個模板引擎把業務邏輯與表示分隔開,它可以被看作單獨的兩層。Java 應用程式可能通過 Java servlet 執行表示任務,servlet 通過與 Enterprise Java Bean (EJB) 通訊執行業務邏輯,EJB 通過連線資料庫獲取更多資訊。因此,換一個角度來看,三層體系結構可能是另一副樣子,尤其是在涉及不同的工具集時。

常見的體系結構

儘管應用程式的體系結構各不相同,但是有一些常見的體系結構趨勢。在一般情況下,應用程式需要四個功能層:

  • 客戶機層
  • 表示層
  • 業務邏輯層
  • 資料層

在 Web 應用程式中,客戶機層由 Web 瀏覽器處理。瀏覽器顯示 HTML 並執行 Javascript(以及 Java applet、ActiveX 或 Flash applet),從而向用戶顯示資訊和收集使用者資訊。表示層是從伺服器到客戶機的介面,它負責控制輸出的格式,讓輸出可以在客戶機上顯示。業務邏輯層實施業務規則(比如計算和工作流),從而驅動應用程式。最後,資料訪問層是持久化的資料儲存,比如資料庫或檔案儲存。

大多數應用程式需要所有這四層的功能,儘管它們可能不需要明顯完整地實現這些層。

另一種流行的體系結構是 Model-View-Controller,這是一種用於分隔應用程式元件的模式。在 MVC 模式中,模型封裝業務邏輯層,並與框架一起封裝資料層。檢視負責處理髮送給客戶機的資料表示。控制器的作用是控制應用程式流程。

層的容量擴充套件

擴充套件 Web 應用程式的容量意味著讓它能夠處理更多的通訊流。容量擴充套件的一個方面是如何根據需求部署硬體。另一個方面是應用程式如何響應新的硬體環境。從概念上說,在出現效能問題時,往往首先想到使用功能更強的伺服器;但是應用程式本身很可能造成其他瓶頸。把應用程式劃分為一系列層有助於收縮問題的範圍,可以簡化容量擴充套件。

現在先不考慮應用程式瓶頸。擴充套件應用程式的硬體通常有兩種方式:水平擴充套件垂直擴充套件。水平擴充套件意味著在一層中新增更多的伺服器。在前面的示例中,應用伺服器的瓶頸把請求速率限制在每秒 77 個請求,通過新增第二個應用伺服器並在兩個伺服器之間共享負載,可能可以解決此問題。這會把理論容量提高到每秒 154 個請求,瓶頸位置就會轉到資料庫。

另一方面,垂直擴充套件意味著使用功能更強的計算機。可以使用功能更強的計算機執行應用伺服器的兩個例項,或者更快地處理請求。

初看上去,您可能會完全排除垂直擴充套件方式,因為購買多臺小型計算機通常比不斷購買更高階的伺服器便宜。但是,在許多情況下,垂直擴充套件是更好的方法。如果您有通過邏輯分割槽 (LPAR) 支援硬體分割槽的 IBM® Power® 伺服器,就可以把空閒的容量新增到應用伺服器層。

應用程式的需求也可能促使您選擇垂直擴充套件。在一臺伺服器上很容易通過共享記憶體段共享使用者的會話狀態。如果使用兩臺伺服器,就需要通過其他方式共享狀態,比如資料庫。資料庫訪問比記憶體訪問慢,所以兩臺伺服器的處理速度達不到一臺伺服器的兩倍。

資料庫是另一個常常適合使用垂直擴充套件的場合。讓資料集跨越不同的伺服器需要在應用程式層做大量工作,比如跨兩個資料庫聯結列並確保資料是一致的。使用更強大的資料庫伺服器要容易得多,而且不需要通過重新構建應用程式來支援分散的資料。

把 Web 應用程式建模為佇列

根據前面對應用程式體系結構的討論可以看出,Web 請求會通過多個階段,每個階段花費一定的執行時間。請求排隊通過每個步驟,完成一個步驟之後,再排隊進入下一個步驟。每個步驟很像人們在商店裡排隊結帳的情況。

可以把 Web 應用程式建模為一系列步驟(稱為 “佇列”)。應用程式的每個元件都是一個佇列。建模為一系列佇列的典型 WebSphere 應用程式如圖 3 所示。

圖 3. 建模為排隊網路的 WebSphere® 應用程式
建模為排隊網路的 WebSphere 應用程式

圖 3 顯示請求等待 Web 伺服器處理它們,然後等待 Web 容器,依此類推。如果進入某個佇列的請求速率超過了此佇列處理請求的速率,請求就會聚集起來。當出現請求聚集時,服務時間是不可預測的,使用者會察覺到瀏覽器會話延遲。圖 3 中的佇列代表最糟糕的情況,因為 Web 伺服器可以自己處理一些請求,即不需要訪問資料庫。

佇列在 UNIX® 環境中很常見。當應用程式發出磁碟請求的速率快於磁碟返回資料的速率時,作業系統會讓磁碟請求排隊,還可能調整請求的次序以降低尋道時間。另一個佇列是執行佇列,其中包含等待執行的程序的有序列表。應用程式會等待輪到它們使用某些有限的資源(比如 CPU)。

因此,佇列調優是一種平衡的藝術。佇列太小,就會在仍然有富餘容量的情況下拒絕使用者。佇列太大,就會試圖為過多的使用者提供服務,導致效能很差。

導致情況更復雜的另一個因素是,這些排隊位置並不是無成本的。保留排隊位置會導致記憶體開銷,對於應用伺服器,這會與正在處理請求的執行緒爭用記憶體。因此,在一般情況下,在應用伺服器上排隊並不是好方法。推薦的方法是在應用伺服器之前(比如在 Web 伺服器上)排隊。這意味著 Web 伺服器要保持與 Web 客戶機的連線,並在應用伺服器空閒時發出請求。應用伺服器只需處理它能夠及時派發的請求。

IBM 的文件中推薦了 Web 應用程式佈局方法和各種佇列的調優方法。但是注意,IBM 建議應該避免在 WebSphere 中排隊。這意味著應該把傳送給 WebSphere 應用伺服器的請求速率控制在能夠立即處理的範圍內。Web 伺服器(或 Web 伺服器前面的代理伺服器)應該限制過多的連線,讓它們等待處理。這確保負載比較重的應用伺服器佇列能夠把時間花在為有限的請求提供服務上,而不是試圖同時為所有請求提供服務。

針對開發人員的提示

作為開發人員,應該按照一些一般原則提高應用程式的可伸縮性。這些原則可以應用於大多數 Web 應用程式。

度量設施

應用程式應該以某種方式向收集系統提供度量值(即使收集系統僅僅是日誌檔案)。這些度量值包括訪問應用程式中某個函式的頻率或處理一個請求花費的時間等。這並不會使應用程式執行得更快,但是有助於瞭解應用程式為什麼會變慢以及程式碼的哪些部分花費的時間最長。瞭解什麼時候呼叫某些函式,這有助於把在系統上觀察到的現象(比如 CPU 忙或磁碟活動量高)與應用程式中的活動(比如上傳影象)聯絡起來。

能夠了解站點上發生的情況,這是擴充套件站點容量的關鍵。您認為不夠優化的程式碼部分可能不會造成問題。只有通過適當的度量,才能發現真正的瓶頸。

會話

Web 在本質上是無狀態的。使用者發出的每個請求都獨立於以前的請求。但是,應用程式常常是有狀態的。使用者必須登入應用程式以證明自己的身份,在訪問站點期間可能要維護購物車的狀態,還可能要填寫供以後使用的個人資訊。跟蹤會話是一種成本很高的操作,尤其是在涉及多個伺服器的情況下。

在單一伺服器上執行的 Web 應用程式可以把會話資訊放在記憶體中,在伺服器上執行的任何 Web 應用程式例項都可以訪問共享記憶體。常常會給使用者分配一個標誌,這個標誌標識記憶體中的會話。考慮一下在涉及第二個應用伺服器時會發生什麼。如果使用者的第一個請求傳送給一個伺服器,第二個請求傳送給另一個伺服器,那麼會存在兩個單獨的會話,它們並不相同。

此問題的常用解決方案是,把會話儲存在資料庫而不是記憶體中。這種方法導致的問題是,對於每個請求,需要增加資料庫讀操作,還可能涉及資料庫寫操作。每個 Web 應用伺服器都需要這個資料庫。

一個解決方案是,只在需要會話的地方使用會話。應用程式並不為每個請求裝載會話,而是隻在需要會話時裝載會話。這會減少對後端資料庫的請求數量。

另一個方法是加密會話資料並把它傳送回客戶機,這樣就不需要在本地儲存會話。在使用者的 cookie 中能夠儲存的資料量是有限的,但是 RFC 2109 規定客戶機應該能夠為每個域名儲存至少 20 個 cookie,每個 cookie 至少可以儲存 4K 位元組的資料。

如果發現用資料庫儲存的會話是效能瓶頸,而且無法消除它們,那麼應該考慮把它們分散到單獨的資料庫,甚至是多個數據庫。例如,可以在一個數據庫中儲存偶數的會話 ID,在另一個數據庫中儲存奇數的會話 ID。

快取

與其他部分相比,應用程式的某些部分會更頻繁地修改資料。新聞網站可能每個月只修改頂級分類列表一次。因此,對於每個請求都通過查詢資料庫獲取最新的分類列表是很浪費的。同樣,包含新聞稿的頁面在其整個生命週期中可能只修改一兩次,所以不需要為每個請求重新生成它。

快取意味著把處理成本很高的請求的結果儲存起來,供以後使用。可以快取分類列表或整個頁面。

在考慮快取時,問自己一個問題:“這些資訊必須是最新的嗎?” 如果不是這樣,就可以考慮使用快取。在新聞最初出現時,能夠及時改變新聞稿可能很重要;但是在以後,每分鐘檢查一次修改並通過快取提供頁面,就足夠了。

一種補充方法是,當底層資料改變時,讓快取的資料項失效。如果修改了新聞稿,在儲存它時可以刪除快取的版本。對於下一個請求,由於沒有快取的版本,所以會生成新的資料項。

在使用快取時,必須注意在快取項過期或被刪除時發生的情況。如果有許多請求在請求快取項,那麼在快取項過期時,會為許多使用者重新生成快取項。為了解決這個問題,可以只為第一個請求重新生成快取,而其他使用者使用過時的版本,直到新的快取項可用為止。

memcached 是一種流行的分散式記憶體快取系統,在 UNIX 環境中部署的許多應用程式都使用它。伺服器執行 memcache 守護程序的例項,這些程序分配一塊可以通過一種簡單的網路協議訪問的 RAM。希望在 memcache 中儲存或獲取資料的應用程式首先對鍵進行雜湊計算,這告訴它們應該使用 memcache 池中的哪個伺服器。然後,通過連線這個伺服器檢查或儲存資料,這比磁碟或資料庫訪問快得多。

在尋找應該快取的資料時,還應該考慮是否確實需要直接提供這些資訊。需要在每個頁面上顯示使用者的購物車嗎?只顯示總金額怎麼樣?或者只顯示一個簡單的連結 “view the contents of your cart”。

Edge-Side Includes (ESI) 是一種標記語言,可以用它把網頁劃分為單獨的可快取的實體。應用程式負責生成包含 ESI 標記的 HTML 文件,還負責生成元件。Web 應用程式前面的代理快取根據各個部分重新組裝最終的文件,負責快取一些元件併為其他元件發出請求。清單 1 給出一個 ESI 文件示例。

清單 1. ESI 示例
<html>
<head>
</head>
<body>
<p>This is static content</p>
<esi:include src="/stories/123" />
<p>The line above just told the proxy to request /stories/123 and insert 
 it in the middle of the page </p>
</body>
</html>

儘管這個示例非常簡單,但是 清單 1 說明了如何把兩個文件拼接在一起,這兩個文件有自己的快取規則。

非同步處理

還有一個問題與 “這些資訊必須是最新的嗎?” 相關:“必須在處理完請求時更新這些資訊嗎?” 在許多情況下,可以獲取使用者提交的資料並把處理延後幾秒,而不需要在處理資訊時讓使用者一直等待裝載頁面。這稱為非同步處理。一種常用方法是,讓應用程式把資料傳送給一個訊息佇列,比如 IBM WebSphere MQ,等待到資源可用時處理資料。這樣就可以立即把一個頁面返回給使用者,儘管資料處理的結果還是未知的。

請考慮一個電子商務應用程式,使用者會在這個程式中提交訂單。立即返回信用卡檢驗結果可能是很重要的,但是不需要讓訂單系統馬上確認訂單的所有內容都是有效的。可以把訂單放進一個佇列中等待處理,這可能會在幾秒內發生。如果發生了錯誤,可以通過電子郵件通知使用者,如果使用者仍然在網站上,甚至可以把錯誤通知插入他的會話。另一個示例是報告。不需要讓使用者一直等待生成報告,而是可以返回 “please check the reports page in a few minutes” 訊息,同時在另一臺伺服器上非同步地生成報告。

結束語

應用程式常常採用分層方式編寫。表示邏輯與業務邏輯分隔開,業務邏輯又與持久化儲存分隔開。這種方式可以提高程式碼的可維護性,但是也會導致一些開銷。在擴充套件應用程式的容量時,應該瞭解資料在分層環境中的流動並尋找出現瓶頸的位置。

快取和非同步處理等技術可以重用以前的結果或把工作轉移到另一臺計算機上,從而降低應用程式的工作負載。在應用程式中提供度量設施,有助於及時瞭解 “熱點”。

應用伺服器環境的工作方式與排隊網路很相似,一定要仔細地管理佇列的大小,確保一層不會對另一層施加過大的壓力。IBM 建議儘可能在應用伺服器之前排隊,比如在外部 Web 伺服器或代理伺服器上。

僅僅靠投入更多的硬體,很少能夠有效地擴充套件應用程式的容量。常常需要綜合應用這些技術,才能讓新的硬體發揮作用。