提高數據密集型應用程序性能的技巧
在大規(guī)模應用程序中,數據流的重要性很容易被忽視,但是這可能會導致很嚴重的性能泄漏。在 Shantanu Bhattacharya 撰寫的這一篇文章中,我們將探索可能影響具有多個服務器的 n 層應用程序性能的數據流的各個方面。您還會看到在大規(guī)模應用程序的設計與架構方面提高性能的一些技巧。
開發(fā)者論壇中對數據處理的討論通常都圍繞 applet 和 servlet 展開,而討論焦點往往是一些顯而易見的性能問題,例如觀感、安全性和加載時間。但從一臺機器傳輸到另一臺機器的實際數據量(通常稱為數據流)的問題卻很少被討論到。實際上,數據流是一個非常重要的課題,但卻很少被提起,其重要性也沒有得到充分的認識;對于大規(guī)模的數據密集型應用程序來說更是如此。
本文將介紹數據流在具有多個服務器的 n 層應用程序中是如何對性能產生影響的。我們將使用一個數據流模型來展示一些數據可能延緩或阻塞應用程序處理的接合點,并解釋如何解決常見的驗證、安全性和數據訪問問題。您還會看到一些更高級的設計和架構決策,它們可以在很大程度上提高應用程序的性能。另外,我們將對數據的集中存儲和分散存儲進行評測,這在當前的 n 層系統(tǒng)環(huán)境中是一個關系重大、但尚未得到充分考慮的因素。
數據流在任何階段都可能會延緩甚至破壞應用程序的運行,因此技巧就是預見問題,在問題出現前就將其解決掉。我將使用一個數據流模型來描述最常見的數據流瓶頸,以及避免此類瓶頸的一些技巧。圖 1 展示了通過一個具有多個服務器的典型大規(guī)模 n 層應用程序的數據流。
圖 1. 大規(guī)模應用程序中的數據流

下面讓我們來看一下數據在哪些地方容易造成程序的延緩,以及您可采取的對策。
- 1. 從客戶機到 Web 服務器
- 對于數據流來說,這是一個必不可少的步驟;但是在有些情況下,很多通過這個點的數據流都是不必要的。例如,在服務器端(而不是在客戶端)進行大量簡單驗證的應用就會造成系統(tǒng)速度變慢。理想情況下,我們希望只有在數據由客戶機成功進行驗證之后才移動到服務器上。盡管在某些情況中驗證實際上并不是在客戶端發(fā)生的,但是如果我們對應用程序進行重組,即可解決這個問題。例如,數據驗證通常都被視為業(yè)務邏輯,因此也會作為服務器端的一項功能來考慮。實際上,數據驗證通常是 特定于數據的,因此應在客戶端執(zhí)行。
- 2. Web 服務器到應用服務器
- 如果您希望為客戶機處理數據的呈現規(guī)則,請在服務器上進行。通常,Web 服務器的用途只是將數據傳遞到應用層;為掃清大量性能障礙,我們可以改為在 Web 服務器上處理數據。在呈現數據的情況中,無論如何都要為應用層來處理這些數據。盡管在應用層上處理數據從代碼的角度來看要更加簡單,但這也會造成傳遞的數據遠遠超過您的需要。多傳輸 10 個字節(jié)看起來不是什么大問題,但是一旦經過一百萬次傳輸(在大型應用程序中很常見),所傳輸的不必要的數據就會多達 10 MB。這還沒有考慮數據所使用的報頭和報尾呢!
- 3. 應用服務器到數據庫服務器
- 請在數據到達數據庫服務器之前完成所有數據的處理。這可確保數據庫服務器只需為快速簡單地訪問數據而對數據進行重組即可。它還能確保只有必要的數據會到達數據庫服務器。我們應根據這些考慮事項來確定用于在應用層進行處理的服務器個數。應用層中每增加一臺服務器,不僅會增加硬件成本,而且還會增加數據傳輸的負載。
- 4. 從數據庫服務器檢索的數據
- 按照檢索過程中需要連接數最少的方法來存儲數據,這意味著數據必須在適當的地方進行規(guī)范化(normalization)和反規(guī)范化(denormalization)。稍后我們將更詳細討論 數據的規(guī)范化和反規(guī)范化 問題。
- 5. 從應用層返回 Web 服務器
- 在檢索過程中,只有一個應用服務器應在利用之前接觸數據流,即使整個系統(tǒng)超過 3 層或應用層有多種類型的服務器也是如此。注意存儲和處理數據所使用的路徑都不需要與檢索數據的路徑相同。根據正在編寫的應用程序考慮一下要檢索的數據類型,這也是非常值得的。例如,在一個基于 Web 的交易站點上,我們很可能在客戶下訂單之后就立即檢索這些訂單(比如用戶希望修改或取消的訂單)。另一方面,如果從下訂單到發(fā)貨之間經過了很長時間,我們也可能需要訪問很多數據來檢查訂單的狀態(tài)。這些考慮事項都可以引導您以一些原本不會采取的方式優(yōu)化代碼。 #p#page_title#e#
- 6. 在客戶機上顯示檢索到的數據
- 所有為數據存儲而進行的處理也必須應用于數據檢索。因此,我們有必要同時編寫編碼和解碼例程,從而盡可能有效地使數據檢索與處理相結合。
一旦您理解了數據流有可能在哪些特定點造成性能的大幅度降低,能夠針對此類瓶頸編碼,那么也就有了一個很好的開端。接下來,我們需要運用一種更高級的方法,從而避免出現可能導致系統(tǒng)性能下降的設計和架構錯誤??紤]一下大規(guī)模應用程序中可能對性能造成影響的一些功能,以及如何設計才能獲得更好的性能。
如前所述,僅為了進行數據驗證就將數據從客戶機移動到服務器上是一個巨大的錯誤。將數據從客戶機發(fā)送到服務器上,然后由于驗證失敗就拒絕用戶請求,這只會增加所傳輸的不必要數據的總量。最好的選擇是重組應用程序,使所有的驗證都在客戶端進行。如果不能這樣做 —— 也就是說,如果您必須在服務器上驗證數據 —— 至少可以使用諸如 ActiveX® 或 Java™ applet 之類的代碼傳輸機制來進行這種操作。代碼傳輸只會發(fā)生一次,但每次在另外一端遇到驗證問題時都要傳輸數據。
如果您所處理的數據依賴于已為驗證而存儲的數據,還要保持那些已存儲的數據在客戶端可用。例如,若需要保存一個用戶所提交值的列表,以便與新值比較,那么在客戶機而不是服務器上存儲這些數據會比較經濟。多個用戶所輸入的數據量會很快超過您為驗證而必須傳輸的數據量。
對于這條規(guī)則,確實有例外情況,例如在數據庫列中進行惟一性測試時。由于無法在客戶端完成這一任務,因此在服務器上進行驗證是有必要的。然而在大部分情況中,在客戶端完成數據驗證是最佳實踐。
需要確保安全性的數據通常會比不需保護的數據耗費更多的投影(Projections)操作。盡管看似違背直覺,但不安全的數據所經歷的操作往往與安全數據完全相同,這只是因為這兩類數據都被存儲到了相同的位置。您可通過在數據庫中獨立存儲安全數據和不安全數據來提高應用程序的整體性能。
將兩種類型的數據獨立存儲無效果的惟一情況就是:安全數據量與不安全數據量相比非常少,例如,有些應用程序中惟一的安全機制就是用戶密碼。此時可以將這兩類數據保存在一起,不過仍然需要確保對安全數據所進行的操作不會發(fā)生在不安全數據上。
訪問頻率是我們在確定存儲標準時需要考慮的一個重要因素。例如,考慮一個醫(yī)院信息系統(tǒng)。在這個系統(tǒng)中,對一位患者統(tǒng)計信息的訪問頻率要遠遠低于其姓名和 ID 號。因此,將統(tǒng)計信息與姓名和 ID 數據分開存儲是很有意義的。如果將這兩類數據存儲在一起,那么每次訪問患者的 ID 和姓名時,數據庫都必須執(zhí)行一些操作來過濾掉關于統(tǒng)計信息的數據。
在這個例子中,在一個表中還是在一個數據庫中存儲所有這些信息并不重要。如果是在一個表中,那么您必須執(zhí)行一次投影操作來過濾數據。如果是在數據庫中,若用于脫機處理的數據(例如數據倉庫)與用于聯機處理的數據存儲在一起,就會降低系統(tǒng)速度。如果頻繁訪問的數據也會用于脫機處理,那么這種問題就會更加突出;此時,應將這些數據存儲在另一個數據庫中。
即便在所有的數據都依賴于聯機事務處理時,您也可以考慮為某些數據使用另外一個數據庫。我們可以考慮為某些數據使用一個不同的數據庫,這樣這個數據庫中所有的數據都是用于在線事務處理的了。如果應用程序不會同時訪問兩部分數據,而且這兩部分數據都可能會增長到很大(達到數 GB 或 TB),那么這是一種很好的策略。這時將數據劃分到兩個不同的數據庫中會很有幫助。
很多時候您都能夠根據應用程序的當前狀態(tài)來預測其后續(xù)狀態(tài),并且能保證一定的準確度。例如,在貿易軟件中,如果用戶查詢某個訂單,那么他很可能將要查看這份訂單的具體細節(jié)。對于絕大多數應用程序類型,我們都可以觀察到類似的模式。觀察這些模式,然后即可優(yōu)化應用程序,方法是:根據您的預測算法,提前對某項給定操作后可能會被立即調用的數據進行一些操作。 #p#page_title#e#
乍看起來,這種策略似乎在系統(tǒng)中引入了有狀態(tài)性,這可能會導致外擴問題,但實際上不會。
這種方法的確可以幫助您優(yōu)化系統(tǒng):只有在這些信息丟失時,系統(tǒng)運行速度才會減慢。預先操作失誤的惟一情況就是所預測的結果不可用,例如下一個調用被路由到其他系統(tǒng)上去了。(這種策略對于那些采用服務器場的系統(tǒng)也非常有用。在這種情況中,信息可以簡單地存儲到一個跨場中所有服務器的集中儲存庫內。)
我們可以使用一個貿易應用程序的例子來進一步闡述這個問題,一名用戶會訪問一個訂單。該請求被發(fā)送到應用服務器場內 3 個服務器中的第 1 個(server1)。應用程序預測這名用戶接下來會訪問同一訂單的詳細內容,并提前獲取了這些數據。您可能會將這些數據存儲在 server1 上。但是由于 server1 和 server2 繁忙,與該訂單的詳細信息有關的后續(xù)請求都被發(fā)送到了 server3 上。現在,如果與此訂單的詳細信息有關的數據在 server3 上不可用,那么它就只好自行獲取這些數據。盡管這并沒有帶來預期的優(yōu)化,但是也不會導致出現不一致的狀態(tài),因為訂單的詳細信息在任何一種情況中都可以獲取到。為了利用這種優(yōu)化機制,應將數據保存到場中所有服務器都可訪問的共享存儲中
從理論上來說,總是建議您保證數據的規(guī)范化;但是在實踐中,過度的規(guī)范化(或者沒有恰當反規(guī)范化的規(guī)范化)會導致對數據檢索連接的依賴性。例如,考慮這樣一種情況:患者姓名總是與其統(tǒng)計信息一起訪問的,而患者 ID 是關鍵字。此時,將患者姓名與患者統(tǒng)計數據存儲在一起是比較明智的(即便規(guī)范化要求與此不符),這樣可以確保我們不需要在每次要訪問患者統(tǒng)計信息時都需要對患者姓名執(zhí)行一次連接操作。如果是這樣,就需要在兩個位置更新患者姓名。但由于更新患者姓名的頻率遠遠低于更新患者統(tǒng)計信息的頻率,因此就性能來說,系統(tǒng)依然得到了優(yōu)化。
注意數據的規(guī)范化在任何面向數據庫的應用程序中都是標準實踐。在優(yōu)化系統(tǒng)時,應首先從以充分規(guī)范化的數據為基礎入手,這一點非常重要;否則,在更新數據時,就必須更新其所有實例。
在一個由多個服務器處理數據的 n 層應用程序中,聚合非常重要。例如,考慮這樣一種情況,要打印的數據在抵達打印機之前必須通過多個服務器。如果不仔細考慮這個問題,這種類型的設置可能會造成相關服務器和打印進程的速度降低,在打印大量數據時更是如此。為了解決這個問題,請確保數據的使用點要與數據存儲點盡可能地接近。(將數據移到數據庫中時,這種技巧也同樣適用。)
同時訪問的數據應該總是存儲在一起;分別訪問的數據也應該獨立存儲。違背這一基本準則會導致數據庫在訪問數據時執(zhí)行更多操作。將通常會被一起訪問的數據分別存儲將導致更多不必要的連接操作;將通常分別訪問的數據存儲在一起則意味著需要執(zhí)行更多投影操作來過濾掉不需要的數據。
聯機事務處理(OLTP)與聯機分析處理(OLAP)并不相同,兩種技術所使用的數據訪問模式也有很大的區(qū)別。因此,每種處理可能需要不同的數據庫 —— 即使這意味著需要將相同的數據重復存儲兩次,也是需要這樣做的。獨立存儲 OLTP 數據和 OLAP 數據使您可為每種技術的訪問模式進行優(yōu)化。在 表 1 中,您可以看到 OLTP 和 OLAP 數據庫間差異的細目,這些差異主要與每種技術使用的訪問模式和資源使用情況相關。
表 1. OLTP 與 OLAP 處理的對比
特性 | OLTP | OLAP |
---|---|---|
訪問模式 | 反復 | 偶爾 |
操作 | 讀寫 | 全表掃描 |
工作單位 | 基于哈?;蜿P鍵字/索引的簡單查詢 | 復雜查詢 |
所訪問的記錄數量 | 數十或數百 | 數百萬(全表) |
大小 | 數十 MB 到 GB | 數十 GB 到 TB |
度量標準 | 事務處理吞吐量 | 查詢吞吐量 |
盡管這個問題總是被忽視,但在數據密集型應用程序中,數據流確實會消耗大量帶寬。應用程序越大、越復雜,其中的數據移動就越多,設計時考慮到的問題也必須越復雜。因此最好建模數據流通過應用程序的路徑,并考慮清楚常規(guī)應用程序功能中可能減緩數據流速度的各點。接下來,考慮應用程序的架構,確定可以優(yōu)化哪些地方來提高性能。只要遇到疑問,就回頭來做最基本的事情:檢查數據流的路徑,確保應用程序在每個接合點處都執(zhí)行良好。
現貨:全球最快的Fusion-io SSD硬盤卡-提升IO速度200倍述評