轉帖|行業資訊|編輯:龔雪|2016-05-06 17:46:34.000|閱讀 267 次
概述:你的Java應用程序的性能是怎樣診斷和優化的?不妨看看這兩位西醫的方子。如果你有更好療效的藥方,也歡迎在評論區告訴我們。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
當我在幫助一些開發者或架構師分析及優化Java應用程序的性能時,關鍵往往不在于對個別方法進行微調,以節省一或兩微秒的執行時間。雖然對某些軟件來說,微秒級的優化確實非常重要,但我認為這并非著眼點所在。我在2015年間對數百個應用進行了分析,發現多數性能與可伸縮性問題都來源于糟糕的架構決策、框架的錯誤配置、錯誤的數據庫訪問模式、過量的日志記錄,以及由于內存過度消耗而導致的垃圾回收所帶來的影響。
在我看來,性能工程的根本在于通過大量的觀察,將關鍵的架構指標、可伸縮性指標以及性能指標關聯在一起。通過對每次構建的結果以及不同負載情況下的表現進行分析,以發現系統中的回歸缺陷或瓶頸所在。以下圖中的儀表板作為示例:
通過將系統負載、響應時間與SQL語句的執行次數等指標相關聯,可得出某些性能工程方面問題的根本原因。
最上面一張圖叫做“層分解”圖表,它顯示了你的應用中各個邏輯組件(例如Web Service、數據庫訪問、業務邏輯、Web服務器等等)的總體執行時間。紅色部分所表示的是某個后端Web Service所花費的時間,很明顯這里產生了一個組件熱點。
我們同時可以發現,該Web Service并沒有承受異常的負載,因為從第二張圖來看,當時應用程序所處理的請求數量這條線比較平穩。一般情況下,整體響應時間多數都耗費在數據層,但這并不代表數據庫本身的速度緩慢!我了解,低效的數據庫訪問往往是造成性能不佳的主要原因,因此通常會結合分析SQL語句的執行次數。在這個示例中,已經能夠很清楚地看到它與大多數響應時間的峰值是相關的。
我所觀察到最常見的問題模式就是糟糕的數據庫訪問模式,此外還有過于細粒度的服務調用、糟糕的共享數據訪問共享、過度的日志記錄,以及由內存泄露以及大量的對象創建所導致的垃圾回收影響或是應用程序的崩潰。
在本文中,我將專注于探討數據庫方面的問題,因為我十分確信你的所有應用都是因這些訪問模式中的某一種而產生問題的!你可以在市場上已有的各種性能診斷、追蹤,或是APM工具之間隨意選擇。Perfino就是一款不錯的產品,還有免費的Dynatrace Personal License。Java本身也提供了各種出色的工具,例如Java Mission Control等等。許多提供數據訪問功能的框架也經常通過其日志輸出提供各種診斷選項,例如Hibernate或Spring等等。
在使用這些跟蹤工具時,通常不需要對代碼進行任何修改,因為他們都利用了JVMTI(JVM Tooling Interface)以捕獲代碼層面的信息,甚至能夠跨遠程的各層次進行調用追蹤,這一點對于分布式、面向(微)服務的應用來說非常實用。你所要做的就是修改你的JVM啟動命令行選項,以加載這些工具。
某些工具的開發商還提供了與IDE的集成功能,你只需簡單地表示“在運行時開啟XYZ性能診斷功能”。我在YouTube上做了一個簡單的視頻指南,演示了如何對在Eclipse中啟動的應用進行追蹤。(//www.youtube.com/watch?v=unrey8wfq-M&list=PLqt2rd0eew1bmDn54E2_M2uvbhm_WxY_6&index=14)
即使你已經發現造成應用整體響應時間過長的主要原因在于數據庫,但也不要因此就輕率地指責數據庫與DBA!造成數據庫繁忙的原因可能有以下幾種:
在本文中,我將著重講解如何在應用程序端將訪問數據庫所消耗的時間減至最低。
在對應用程序進行問題診斷時,我通常總要檢查幾個數據庫訪問模式。我會逐個分析應用的請求,并將這些問題分別放入以下這個“DB問題模式”的分類表中:
我的第一個示例是一個web應用程序,它能夠提供某幢大樓中的會議室信息。會議室的信息都保存在某個數據庫中,每次當用戶生成會議室信息的報表時,就會調用某個自定義的數據訪問層以訪問該數據庫。
在對個別請求進行分析時,我總是從所謂的事務流(Transaction Flow)著手檢查。事務流是一種可視化選項,可展現出應用程序處理請求的過程。對于會議室信息報表這個請求來說,可以看到,該請求首先進入web服務器層(圖左)、隨后進入應用服務層(圖中),然后對數據層發起調用(圖右)。這些層之間的“鏈接”表現了這些層之間的交互次數,例如這個單一的請求執行了多少次SQL查詢。
從這個屏幕上我們可以立即發現造成問題的頭兩種模式,即過多的SQL執行模式以及數據庫繁忙模式。讓我們來分析一下:
很容易就可以看出這個請求產生了大量的SQL語句執行,并且造成數據庫繁忙效應:它總共執行了24889次SQL!花費了40.27秒(占整個請求時間的66.51%)才完成整個執行過程!
如果我們對個別的SQL語句進行分析,將發現這個請求還有另外的問題,即N+1 次查詢問題以及低效的連接池訪問(下文將進行詳細討論):
這種糟糕的訪問模式是無法通過對數據庫的索引進行優化而解決的。
我已經無數次看到這種問題發生了。應用的邏輯需要對某個對象列表進行迭代,但它并沒有選擇使用“貪婪加載”(Eager Loading)方式,則是使用了“延遲加載”(Lazy Loading)方式。這種選擇可能來自于O/R映射框架,例如Hibernate或Spring,也可能來自于自主開發的框架,正如上文所述的示例一樣。該示例使用了某種自主開發的實現方式,它會加載每個會議室對象,并通過獨立的SQL查詢語句獲取每個會議室的全部屬性。
每個SQL查詢都是在一個向連接池獲取的JDBC連接中執行的,然后在每個查詢完成之后都會返回。這也解釋了為什么該請求會產生12444次set clientname操作,因為Sybase JDBC驅動每次向連接池請求連接時都會提交這一請求。這就是問題所在!其他的JDBC驅動未必會產生set clientname這個調用,你可以查看一下調用getConnection的次數,這同樣可反映出這個問題。
對于N+1次查詢問題本身來說,使用連接查詢就可以輕易地避免這一問題。在這個會議室與屬性的示例中,可以使用以下連接查詢:
select r.*, p.* from meeting_rooms as r inner join room_properties as p on p.room_id = r.room_id
結果就是整個執行過程只產生了1次查詢執行,不再是12000多次了!同時也免除了12000次連接的獲取操作以及對“set clientname”的調用。
據我所知,Hibernate或其他O/R映射器有許多使用者。我想要提醒你們一點,O/R映射器所提供的延遲加載與貪婪加載選項,以及其他各種緩存層各有其存在的理由。對于特定的用例,需要確保你正確地使用了這些特性與選項。
在下面這個示例中,延遲加載并不是一種好的選擇,因為加載2千個對象以及他們的屬性會導致產生4千多次SQL查詢。考慮到我們總是需要獲取所有對象,那么更好的方式是貪婪加載這些對象,然后考慮對他們進行緩存,前提是這些對象不會變更得十分頻繁:
在使用Hibernate或Spring等O/R映射器時,需要選擇正確的加載與緩存選項。你需要理解他們的幕后工作原理。
大多數O/R映射器都會通過日志記錄提供優秀的診斷選項,同時也可以查看在線社區中的內容,以了解各種最佳實踐。推薦你閱讀由Alois Reitbauer撰寫的一系列博客文章,他曾經在Hibernate推出的早些年頭對其進行過非常深入的研究。在這系列文章中,他特別強調了如何有效地使用緩存與加載選項。
當數據庫引擎完成對某條SQL語句的解析,并創建了數據訪問的執行計劃后,該結果會被保存在數據庫中的一個緩存區域中以便重用,而無需重新解析這一語句(語句解析是數據庫中最耗費CPU時間的操作)。用于在緩存中找到某個查詢的鍵是語句的全文本。這也意味著,如果你調用了1000次相同的語句,卻為其傳了100個不同的參數值(例如where語句中的值),那么在緩存中就會產生1000個不同的條目,而使用了新參數的第1001次查詢也必須被再次解析。這種工作方式非常低效。
因此,我們提出了“預處理的語句”這一概念:某條語句經過預處理、解析后被保存在緩存中,以占位符的方式表示變量。在這條語句的實際執行過程中,這些占位符會被實際的值所替換,無需再次解析這條語句,可以直接從緩存中找出執行計劃。
數據庫訪問框架通常在這一點上做得很出色,會對查詢語句進行預處理。但在自定義代碼中,我發現開發者經常會忽略這一點。在以下示例中,只有一小部分SQL執行經過了預處理過程:
通過對SQL執行次數與已預處理的SQL執行次數進行對比,發現了未經預處理的數據庫訪問的問題
如果你打算自行開發數據庫訪問代碼,請再次確認你正確地調用了prepareStatement。舉例來說,如果你調用某個查詢不止1次,那么通常來說最好能夠使用PreparedStatement。如果你選擇使用框架以訪問數據,也請再次確認這些框架的行為,以及在優化和執行所生成的SQL時有哪些配置選項可以選擇。實現一點最簡單的方式是對executeStatement與prepareStatement執行的次數進行監控。如果你重復對每個SQL查詢進行相同的監控,那么將很容易地找到優化熱點。
我經常發現有些應用會使用默認的連接池大小,例如每個池10或20個連接。開發者總是會忽略對連接池大小的優化,因為他們沒有進行必要的大規模負載測試,也不知道有多少個用戶會使用這些新特性,更不了解并行的DB訪問會導致什么結果。也有可能是從預發布環境轉向生產環境的部署時“丟失”了連接池的配置信息,導致生產環境中的配置使用了應用服務器中的默認配置。
通過JMX指標信息,能夠方便地對連接池的使用情況進行監控。每種應用服務器(Tomcat、JBoss、Websphere等等)都會提供這些指標,不過有些服務器需要你明確地開啟這種特性。下圖展示了某個群集中的WebLogic服務器的連接池使用情況。你可以看到,在其中三臺應用服務器中,“活動的DB連接數量”都已經達到最大值。
確保你適當地調整了連接池的大小,不要使用與你期待的負載情況不符的默認設置
出現這一問題的根本原因不在于訪問量的峰值。在本文開頭部分所介紹的“系統負載 / 響應時間 / 數據庫執行次數”這個儀表板中顯示,應用并沒有產生特別的訪問量峰值情況。最終發現,在每天下午2點多這個時間段設定了一個運行報表的計劃,它需要執行多個運行時間相當長的UPDATE語句,每個語句都使用了不同的連接。這會在幾分鐘內阻塞其他連接,導致了應用程序在“正常的”訪問量下出現性能問題,因為用戶的請求無法獲得數據庫的連接:
個別的SQL執行阻塞了其他連接達幾分鐘,造成了連接池資源消耗殆盡的問題
如果你已經了解到某些請求會使連接掛起一段較長的時間,你可以選擇以下幾種方案:
不過,首先你要確保對這些查詢進行優化。通過分析SQL查詢執行計劃,以找出哪些操作是最耗時的。如今,大多數APM工具都能夠讓你以某種方式獲取某個SQL語句的執行計劃。如果沒有可用的工具,最簡單的方式就是使用數據庫的命令行工具,或者咨詢某個DBA,讓他幫助你生成執行計劃。
通過學習SQL查詢執行計劃,對你的SQL語句進行優化
執行計劃能夠顯示出DB引擎處理SQL語句的方式。造成SQL語句執行緩慢的原因多種多樣,不僅僅限于缺少索引或是使用索引的方式不對,很多情況下是因為設計、結構或連接查詢所造成的。如果你并非SQL方面的專家,可以向DBA或SQL大牛求助。
負載測試以及在生產環境中進行監控的提示與技巧
除了對各個請求進行分析,以指出這些問題模式之外,我同樣也會關注當某個應用程序在負載情況下的長期趨勢。除了我在本文開頭為你展示的儀表板之外,我也會指出數據驅動行為的變化,并對數據緩存是否正確運行進行驗證。
下面這張圖表展示了SQL語句執行的平均次數(綠色)以及SQL語句執行的總次數(藍色)。我們為應用進行了一次兩小時的性能測試,保持負載始終處于較高水平。我所期望的結果是平均次數逐漸減少,而總次數則趨向平穩。因為按照我的假設,從DB所獲取的數據大多數是靜態的,或是會被緩存在某個不同的層。
如果你的應用表現不符合這一預期,那么可能是遇到了數據驅動的性能問題,或是產生了緩存問題
假設如我之前所展示的一樣,你的應用中產生了常見的N+1次查詢問題。那么隨著終端用戶在DB中產生越來越多的數據,應用程序所產生的SQL平均次數也將不斷提高,因為這些查詢所返回的數據也會越來越多!因此,請務必注意這些數字!
示例4表現了某個后臺報表應用在每天下午2點執行所造成的問題,與之類似,我同樣也會關注SQL訪問隨著時間變化的模式。我所關注的不僅包括總執行時間,同時也包括SELECT、INSERT、UPDATE與DELETE的執行次數。這樣一來,我就能夠指出是否在某個時間段內會進行一些特別的活動,例如通過后臺作業對大批數據進行更新。
通過觀察總執行時間,以及SELECT、INSERT、UPDATE與DELETE的執行次數,了解應用的數據庫訪問行為
進行大量更新操作的批處理作業的執行需要一段時間才能完成,尤其對于包含大量行的表來說更為明顯。如果整張表因此被鎖住,那么其他需要對這張表、哪怕只是對其中某些行進行更新的請求都必須等待鎖被釋放。你應考慮在沒有其他用戶在線的時間段運行這些作業,或實現某種不同的加鎖邏輯,實現對單個行的加鎖、更新以及釋放操作。
在本文中,我著重分析的數據庫性能問題多數與數據庫服務器本身是否緩慢是無關的,而主要是由使用了糟糕的數據庫訪問模式(N+1次查詢問題、未經預處理的語句等等)的應用程序代碼、或是配置錯誤(低效的連接池訪問、數據驅動問題)所導致的問題。
但是,如果我們完全忽略了數據庫本身,那也是不明智的。因此,我總是會對關鍵的數據庫性能指標進行檢查。大多數數據庫都會通過特殊的系統表提供豐富的性能信息,比如Oracle就會提供某些v$表以及視圖,以訪問關鍵的數據庫性能指標(會話、等待時間、解析時間、執行時間等等),或是表鎖以及運行時間較慢的SQL等信息,這些信息來自于使用這個共享的數據庫實例的各個應用程序。
我在進行數據庫健康檢查時通常會觀察兩個儀表板,你可以在此看到來自于這些性能表中的指標數據:
觀察數據庫是處于健康狀態,還是由于共享該數據庫實例的應用產生過多的負載而產生了影響。
通過表鎖等信息,判斷是否有某個正在執行中的SQL語句對服務器乃至你的應用造成了負面影響
在持續集成流程中對數據庫指標進行自動檢測
在我為你介紹分析關鍵數據庫指標以及用例的一些新點子之前,我希望首先能夠彌補一個缺失的主題,而這一點是我們都應當考慮到的,那就是自動化!
我建議你不要手動地執行這些檢查步驟,而是通過持續集成工具檢查這些指標,將這一步驟與單元測試、集成測試、REST API或其他類型的功能性測試等步驟結合在一起。如果你已經設計出一套測試用例集,用于檢查各種REST API或新特性的功能,那么為什么不在每次構建的測試執行期間去捕獲這些指標呢?這種方式可以帶來以下益處:
下面這幅屏幕截圖展示了每次構建與每次測試時對這些指標的追蹤,并在其表現異常時發出警告。你可以將這些指標集成在你的構建管道中,并且當某個代碼變更造成影響時通過通知信息了解情況,隨后立即修復這一問題,避免當代碼發布到生產環境時產生系統崩潰的情況。
在你的持續集成流程中加入這些指標,并對指標的變化進行觀察,以自動地找出各種糟糕的數據庫訪問模式!
在本文中,我們專注的是數據庫方面的熱點問題。但在我的工作過程中,我也在其他領域發現許多類型的性能問題。在2015年,我參與了一個將一體性應用遷移為(微)服務的項目,在其中發現了一個巨大的峰值問題。該問題類似于我們已分析過的某些模式,例如N+1次查詢問題,原因在于某個用例會數百次調用某個后端服務。
大多數情況下,這種問題都是由糟糕的接口設計而造成的,并且沒有考慮到某個原本在本地調用的方法在Docker容器或云計算環境中被執行時會發生什么。網絡問題會突然間出現,包括通過網絡傳輸的信息以及新的連接池(意味著你需要考慮線程與套接字),這些問題是你必須處理的。
原文翻譯自:
文章轉載自:
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn