數(shù)據(jù)庫優(yōu)化與應(yīng)用程序性能的五個平衡點
我們經(jīng)常提到數(shù)據(jù)庫優(yōu)化,經(jīng)常為提高應(yīng)用程序性能對數(shù)據(jù)庫一陣折騰,但這真的有效嗎?我們是否真的看清哪些問題出在數(shù)據(jù)庫方面,哪些問題出在應(yīng)用程序方面?
幾乎所有現(xiàn)代應(yīng)用程序都要通過數(shù)據(jù)庫實現(xiàn)數(shù)據(jù)持久化。數(shù)據(jù)庫訪問層經(jīng)常要對嚴重的性能問題負責(zé)。一旦遇到數(shù)據(jù)庫的問題,大多數(shù)人開始研究數(shù)據(jù)庫本身。正確的索引和數(shù)據(jù)庫結(jié)構(gòu)對提高性能非常關(guān)鍵。然而,很多時候糟糕的性能或可伸縮性問題的罪魁禍?zhǔn)讌s是應(yīng)用程序?qū)?,而不是?shù)據(jù)庫。
應(yīng)用程序?qū)涌刂撇Ⅱ?qū)動數(shù)據(jù)庫的訪問。這一層的問題不能從數(shù)據(jù)庫上得到補償。所以要想得到高性能和擴展性,數(shù)據(jù)訪問邏輯的設(shè)計非常關(guān)鍵。雖然數(shù)據(jù)庫驅(qū)動的應(yīng)用程序中使用情況各不相同,但所有問題能夠歸結(jié)到幾個反模式上。分析你的應(yīng)用程序中是否使用了下列的反模式,并且解決他們,能夠以最小的代價簡單讓你的軟件更快、 更穩(wěn)定。
對象/關(guān)系映射的誤用
對象/關(guān)系映射已經(jīng)成為現(xiàn)代數(shù)據(jù)庫應(yīng)用程序的中心部分。對象/關(guān)系映射讓人從面向?qū)ο筌浖蟹g和訪問關(guān)系型數(shù)據(jù)的重擔(dān)中解脫出來。它們向應(yīng)用程序人員隱藏了數(shù)據(jù)訪問大部分的復(fù)雜邏輯。由于開發(fā)人員更專注于實際的業(yè)務(wù)邏輯,而不是基礎(chǔ)架構(gòu)細節(jié),會使得生產(chǎn)效率更高。對象關(guān)系層不需要看到細節(jié)就可以輕松操作復(fù)雜的對象圖。這經(jīng)常讓人產(chǎn)生錯誤的印象,認為這些框架讓人從設(shè)計數(shù)據(jù)訪問邏輯的重擔(dān)中解脫了出來。
開發(fā)人員經(jīng)常認為數(shù)據(jù)訪問框架很容易就把一切搞定了;然而,不理解內(nèi)部工作機制就使用對象/關(guān)系映射框架,很多時候會導(dǎo)致程序性能低下。主要有兩個誤解引起了這些問題──加載的行為和加載的時間。
對象/關(guān)系映射基于每個對象加載數(shù)據(jù)。這意味著只有當(dāng)一個對象被請求或者訪問時,需要的SQL語句才會被創(chuàng)建并執(zhí)行。這個原則非常普遍,乍一看多數(shù)情況下沒問題。但同時它也常常是性能和擴展性問題的原因所在。
讓我們看一個簡單的例子。在一個存儲地址信息的數(shù)據(jù)庫中,我們有一張表存儲人和一張表存儲地址。如果我們想得到每個人的名字及其居住的城市,我們不得不遍歷人那張表,然后訪問地址信息。下圖顯示了使用直接(out-of-the box)查詢機制的結(jié)果??梢钥闯觯@個簡單的例子就導(dǎo)致了大量的數(shù)據(jù)庫查詢。
這直接引起了對象/關(guān)系映射中第二個重要的細節(jié)──加載時間。對象/關(guān)系映射-如果沒有事先告知-會盡量晚地加載數(shù)據(jù)。這一行為就是延遲加載。延遲加載保證了數(shù)據(jù)盡可能晚地加載,目的是執(zhí)行盡量少的數(shù)據(jù)庫查詢,同時避免創(chuàng)建不必要的對象。雖然這個方法通常情況下是可行的,但當(dāng)它訪問那些沒有加載的數(shù)據(jù),而數(shù)據(jù)連接已經(jīng)不存在時,就可能導(dǎo)致嚴重的性能問題,以及所謂的LazyLoadingExceptions。
在如上所述的情況下,使用專門的數(shù)據(jù)查詢能夠顯著提高性能。
因此,雖然對象/關(guān)系映射在數(shù)據(jù)訪問的開發(fā)方面作用很大,設(shè)計合適的數(shù)據(jù)訪問邏輯的重擔(dān)仍然需要我們挑起。像dynaTrace這樣帶有工具的動態(tài)架構(gòu)驗證,能夠幫助有效地識別程序中性能的弱點,并能主動解決。
加載了太多數(shù)據(jù),實際不需要這么多
數(shù)據(jù)庫訪問中經(jīng)常出現(xiàn)的另外一個反模式是加載了太多的數(shù)據(jù),而實際上不需要這么多。導(dǎo)致這樣的原因很多??焖賾?yīng)用程序開發(fā)工具提供了簡單的方式,能把數(shù)據(jù)結(jié)構(gòu)和用戶接口控制連接起來。由于數(shù)據(jù)層由領(lǐng)域?qū)ο髽?gòu)成,通常它們包含的數(shù)據(jù)要比實際顯示的多得多。再次使用地址薄作為例子。這一次需要顯示人的名字及其居住城市。兩個對象──地址和人──都被加載了,而不是只加載這3個字段。這導(dǎo)致了數(shù)據(jù)庫、網(wǎng)絡(luò)和應(yīng)用程序?qū)拥拇罅块_銷。使用專門的查詢能夠大大減少查詢的數(shù)據(jù)量。然而這種性能的提升需要額外的工作去維護。表中新增一列可能需要對數(shù)據(jù)訪問層修改多處。
設(shè)計的服務(wù)接口不合理也經(jīng)常引起這種反模式。服務(wù)接口通常要設(shè)計的很通用,以支持大量的用例。其好處是各種各樣的用例中都可以使用服務(wù)。另外,用例要比后臺服務(wù)實現(xiàn)變化的快得多。這會導(dǎo)致服務(wù)接口在某些場景下不適合。開發(fā)人員然后不得不使用一些補救方法,這可能導(dǎo)致數(shù)據(jù)訪問邏輯效率低下。這個問題在數(shù)據(jù)驅(qū)動的Web Services上經(jīng)常出現(xiàn)。
為了克服這些問題,開發(fā)過程中需要不斷地分析數(shù)據(jù)訪問模式。如果是敏捷開發(fā)方法,每個用戶故事完成后都應(yīng)該檢查數(shù)據(jù)訪問邏輯。除此之外,應(yīng)該跨應(yīng)用程序用例分析數(shù)據(jù)訪問模式,以理解數(shù)據(jù)訪問邏輯,這樣能夠在開發(fā)中相應(yīng)地優(yōu)化數(shù)據(jù)訪問邏輯。
未充分利用資源
數(shù)據(jù)庫是應(yīng)用程序中資源的瓶頸,所以使用越少越好。通常情況下大家對數(shù)據(jù)庫連接的使用關(guān)注甚少。像任何共享的資源一樣,數(shù)據(jù)庫連接會嚴重影響整個系統(tǒng)的性能。尤其是web應(yīng)用和使用對象/關(guān)系映射框架并用了延遲初始化的程序,會讓數(shù)據(jù)庫保持連接的時間比需要的更長。處理開始時獲得連接,直到頁面生成完成或者再也沒有數(shù)據(jù)訪問了才斷開。在使用對象/關(guān)系映射的應(yīng)用程序中,連接經(jīng)常保持著以避免可惡的延遲初始化的問題。通過重新設(shè)計數(shù)據(jù)訪問邏輯,把它從后處理(比如頁面生成)中分離出來,應(yīng)用程序的性能和擴展性能得到極大的提高。
下圖展示了 。第一個使用了1個數(shù)據(jù)庫連接,第二個使用了2個連接,第三個使用了2個連接,但是有2/3的處理是在釋放連接之后執(zhí)行的。第三個場景數(shù)據(jù)訪問經(jīng)過更好的設(shè)計,僅用了1/10的資源就獲得了幾乎同樣高的性能。
一刀切
一刀切是一種反模式,開發(fā)過程中經(jīng)常見到,敏捷團隊中則更多。這種反模式的特征是開發(fā)了主要功能之后,所有的數(shù)據(jù)訪問就同樣對待,好像它們沒有任何區(qū)別。然而,區(qū)別對待不同類型的數(shù)據(jù)和查詢可以顯著提高應(yīng)用程序的性能和擴展性。
應(yīng)該對數(shù)據(jù)進行分析,考慮其生命周期的特性。它是否經(jīng)常變化,它是可修改的還是只讀的呢?數(shù)據(jù)的訪問頻率和訪問模式,就隱含了一些潛在的代碼,比如可以做緩存。訪問頻率也暗含了一些線索,比如在哪里做優(yōu)化更有意義。這可以避免過早進行優(yōu)化以及不必要的優(yōu)化,保證了性能調(diào)優(yōu)效果最好。
對數(shù)據(jù)的使用模式進行分析也有助于調(diào)整數(shù)據(jù)訪問層。理解真正使用了哪些數(shù)據(jù)有助于優(yōu)化加載策略。比如,理解用戶怎樣瀏覽搜索結(jié)果對優(yōu)化fetch size很有用。知道了用戶是否查看訂單詳細信息可以給訂單選擇延遲還是立即加載。
除數(shù)據(jù)之外,查詢也應(yīng)該被分析并分類。重要的因素包括查詢時間、執(zhí)行頻率、是否用于交互用戶的上下文或者批量處理的場景中。事務(wù)特性有助于更好地調(diào)整查詢的隔離級別。
比如,在同一個連接中運行用戶短暫的交互查詢和時間很長的報表查詢,很容易導(dǎo)致終端用戶的體驗很糟糕。報表查詢花費的時間很長,會占用大量的數(shù)據(jù)庫連接,讓終端用戶的查詢無法拿到連接。通過給不同的查詢類型使用不同的數(shù)據(jù)庫連接池,會使終端用戶的性能更可預(yù)測。降低數(shù)據(jù)庫查詢中不需要的隔離級別,也能引起性能和擴展性的顯著提高。
糟糕的測試
最后,缺少測試或者測試不正確是數(shù)據(jù)庫訪問應(yīng)用程序性能和穩(wěn)定性問題的一個主要原因。最近我曾就這一主題作了一個演講,并詢問聽眾是否把數(shù)據(jù)庫訪問看作應(yīng)用程序中一個性能問題。雖然他們都贊成,但沒人有這樣的測試流程,來測試數(shù)據(jù)訪問的性能。所以雖然這個話題看上去是很重要,大家似乎都沒有花時間去做。然而,即使有測試流程,這也不一定說明測試就是正確的。雖然代碼完成后能夠立刻發(fā)現(xiàn)數(shù)據(jù)訪問邏輯中的很多問題,但通常很晚之后才執(zhí)行測試,比如負載測試的時候。由于在生命周期的晚期才改動,可能需要修改架構(gòu),從而引起額外的開發(fā)和測試工作,這帶來了很高的不必要的代價。
而且,必須設(shè)計一些測試用例,來測試真實世界的數(shù)據(jù)訪問場景。測試數(shù)據(jù)訪問必須在并發(fā)模式下進行,并且使用不同的訪問類型。只有結(jié)合使用讀/寫訪問才可能識別死鎖和并發(fā)的問題。除此之外,輸入的數(shù)據(jù)應(yīng)該多種多樣,以避免數(shù)據(jù)庫訪問時經(jīng)常命中緩存,這是不切合實際的。
很多時候人們對預(yù)期的負載知之甚少,也不知道去測試哪些負載。很不幸的是,根據(jù)我的經(jīng)驗這種情況比比皆是。然而,不能把這當(dāng)作借口,不定義負載和性能標(biāo)準(zhǔn)。要知道,定義一些標(biāo)準(zhǔn)比一點也不定義要好得多。
如果你對性能數(shù)據(jù)真的毫無頭緒,最好是使用負載漸增測試法,逐步增加負載,直到達到了應(yīng)用程序的最大值。這樣你就知道了應(yīng)用程序的負載峰值。如果負載峰值既合理又現(xiàn)實,那就說明你做的不錯。否則你得知道在哪方面提高性能。大多數(shù)情況下初始的測試表明,應(yīng)用程序能夠處理的負載要比期望的少得多。
結(jié)論
數(shù)據(jù)庫訪問是影響現(xiàn)代應(yīng)用程序性能和可伸縮性的一個關(guān)鍵點。雖然框架支持構(gòu)建數(shù)據(jù)訪問邏輯,仍然需要對數(shù)據(jù)訪問邏輯投入相當(dāng)?shù)木Γ员苊夥N種陷阱和問題。問題之關(guān)鍵是要理解應(yīng)用程序數(shù)據(jù)訪問層的動態(tài)和特性的一切細節(jié)。
【編輯推薦】