隨着成千上萬的Java服務器運行在企業線上環境,Java已經成爲構建線上系統的語言之一。如果希望我們的機器表現出可接受的性能,那麼就需要對它們進行定期調優。這篇文章詳細闡述了Java服務器調優的各項技術。
衡量性能
爲了讓我們的調優有意義,我們需要某種方法來衡量性能是否提高。讓我們記住兩個重要的性能指標:延遲和吞吐量。
-
延遲(Latency) 衡量的是端到端的某個操作的處理時間。在分佈式環境中我們通常用發送請求和接收到響應整個來回的時間來衡量延遲。在那些場景,延遲是從客戶端機器開始衡量的,並且也包括網絡傳輸開銷
-
吞吐量(Throughput)衡量的是服務器在某段時間間隔(比如一秒)內處理的消息數。吞吐量可以用下面的公式來計算:
吞吐量 = 請求數量 / 完成這些請求花的時間
理想情況下我們想要獲得最大的吞吐量同時獲得最小的延遲。然而魚和熊掌不可兼得,一個設計良好的服務器系統中必然存在這二者的折衷(tradeoff)。例如下圖所示,如果你想要獲得更大的吞吐量,那麼你必須增大併發度,但是相應地會導致平均延遲增加。通常,你必須實現最大吞吐量的同時保持延遲在一個可接受範圍內。例如,你可能會選擇某個範圍的吞吐量,並且保證延遲低於10ms。
下圖捕捉了服務器的行爲。正如圖中所示,服務器性能是通過衡量延遲和吞吐量與併發度的關係來實現的。
圖中,“ideal path”是理想中的情況。實際上,大多數服務器在併發度過高情況下將崩潰或者出現性能下降。
服務器調優
在調優環節,我們試着深入服務器內部理解它的行爲,並且要麼證明服務器按預期在運行,要麼尋找途徑提高服務器性能。性能調優的目標有三個:
-
增大吞吐量 - 最大化系統單位時間內能夠處理的消息數
-
減小延遲 - 確保我們滿足響應時間SLAs(Service Level Agreement)
-
發現並修復泄露(比如內存、文件、線程、連接泄露)
正如之前所說,我們的目標是獲得最大吞吐量同時保持延遲在可接受範圍內。如果你還沒有進行過任何調優,最好從調優吞吐量開始。那麼我們就先從吞吐量調優開始吧。
吞吐量調優
在我們開始前,我們先弄懂什麼會限制性能。把服務器設想爲水管系統是很有幫助的;增大併發度就好比增大往水管系統中灌入的水量。增加更多的水並不能保證管道中將有更多的水流動;水流由管道系統中最慢的部分決定。
類似地,應用性能由系統最稀缺的資源決定。計算機系統有很多種資源:CPU、內存、磁盤和網絡IO。任何其中一種資源都可能限制我們系統的性能。
當嘗試從外部衡量系統性能時,我們可以逐漸增大負載直到一種資源消耗殆盡。那將有助於發現限制性資源,然後我們要麼分配更多的那種類型資源,要麼修改系統以更節省地使用那種資源。
下一步,我們搭建起系統並且對系統施加相當量的工作負載(更多細節,可以參考我的博客如何衡量服務器性能)。當負載在運行時,下一步我們將找出各種類型資源的利用程度。
基於最稀缺資源維度,我們將服務器性能下降歸結爲以下三類:
-
CPU緊缺型 - 服務器阻塞等待CPU
-
IO緊缺型 - 服務器因爲磁盤或者網絡帶寬而阻塞
-
延遲密集型 - 服務器等待某些事件發生(比如等待數據從磁盤傳輸到網絡)
下面讓我們來研究下某示例系統中那種資源最稀缺。我們先在Unix/Linux系統中運行top命令:
工程師常犯的一個錯誤是在還沒有真正確定是否真正屬於CPU緊缺型的場景時,就開始調優CPU。雖然上圖顯示的CPU利用率很低,當前機器可能正忙於做IO操作(比如讀磁盤、寫數據到網絡)。Load Average是衡量機器是否滿載的更好指標。
Load Average表示在OS調度隊列中等待的進程數。不像CPU,Load Average將因爲任何一種資源的緊缺而增大(比如CPU、網絡、磁盤、內存...)。更多細節請參考理解Linux的Load Average
我們可以利用下面的Load Average值來確定機器是否處於高負載狀態:
-
如果Load Average < CPU核數,那麼機器就非滿載
-
如果Load Average == CPU核數,那麼機器資源就被充分利用了
-
如果Load Average >= 4*CPU核數,那麼機器就處於過載狀態
-
如果Load Average >= ~40*CPU核數,那麼機器就處於不可用狀態
如果機器非滿載,一般表示它處於空閒狀態。有好幾個因素可能引起這種情況,可以通過下面的方法來校正:
-
試着增大負載(通常表示增大併發度)。例如,用一兩個客戶端測試服務器通常不會讓服務器滿載;通常需要成百上千個客戶端同時測試服務器纔可能讓其滿載
-
剖析鎖狀況(如果大部分線程都在等待鎖或者發生死鎖,那麼吞吐量也會下降)。儘可能發現並修復那些你可以找到的情況,並儘可能使用非阻塞數據結構。我們將在“延遲調優”小節更詳細討論鎖剖析
-
調整線程池大小(有時候系統配置過少的線程數,可能會引起系統運行緩慢)。例如我發現增加Tomcat線程池的大小通常會增大吞吐量
-
確保網絡未飽和(如果網絡處於飽和狀態,那麼能夠到達機器的有效負載就可能非常小了)。在大多數機器中你可以用Linux提供的iftop命令來檢查這種情況
另一方面如果機器滿載,那麼很明顯它在做什麼事情,但你仍然需要確保它在做一些有意義的事情!
-
確保你的應用是運行在同一臺機器上的唯一一個重量級進程。你應該不想看到其他應用使你的測試結果扭曲
-
如果排除了上面這種情況,然後用top命令檢查CPU利用率。如果CPU利用率很高,使用一個profiler工具來看CPU Profile信息
例如上圖是從JProfiler截取過來的CPU Profile信息。它顯示了服務器中Java方法執行樹,標明瞭每個方法的執行時間。檢查最耗CPU時間的方法並確保它們在做有意義的事情。(注意:這篇文章中我們使用JProfiler,也可以使用其他工具如Youkit Profiler和JDK 1.7+提供的Java Mission Control)
-
計算應用花在GC上的時間。如果GC時間佔比超過10%那麼你就需要JVM GC調優了(你可以使用類似VisualVM GC插件這樣的工具來完成,參考我的博客我是否該進行GC調優?)。如果GC存在問題,Profiler的allocation view可以幫助你發現內存分配熱點並且修復它們。這裏有個演講是GC調優方面不錯的參考資源:talk by Kirk pepperdine
-
再下一步是檢查網絡和磁盤的IO狀況(假設你的程序寫磁盤)下面的截圖顯示了從JProfiler截取的IO Profile信息。驗證高IO節點確實出現在預期位置。然後對數據庫訪問做同樣操作
-
最後檢查下你的機器是否在進行內存分頁(比如 Check Swap Usage in Linux)。一般來說,你需要避免內存切換因爲那將大幅度拖慢服務器性能。如果存在內存切換,你要麼修改服務器以需要更少的內存,要麼爲機器增加物理內存
如果你把上面所有步驟都嘗試遍了仍然沒有達到預期性能,很可能是服務器達到了它自身的性能瓶頸。你要麼通過加服務器來Scale Up,要麼重新設計服務器架構。
延遲調優
高延遲是由費時請求處理操作引起的。磁盤訪問、網絡訪問和鎖是引起處理操作時間長的幾大罪魁禍首。
當進行延遲調優時,我們首先要檢查網絡和磁盤狀況,正如我們上一節討論的,並且發現並減少IO操作數量。下面是一些可能有用的校正方法:
-
避免不必要的IO操作。儘可能消滅它們或者用Cache替代
-
嘗試批量IO,批量IO將比多個單次IO節省開銷
-
如果你能提前猜到所需要的數據,那麼可以嘗試預加載(或者預取)數據
現在讓我們來看一個JVM線程視圖。下圖顯示了線程的數目和狀態隨時間變化情況。紅色區域表示很多線程阻塞在鎖上。
如果從線程視圖發現許多線程處於等待狀態,那麼可以在展開“Monitor and Locks”視圖找出引起阻塞的線程:
上面的截圖顯示了哪一段代碼阻塞了最長時間和那一段代碼在這段時間內持有鎖。以下是兩條總原則:
-
儘量避免synchronzied語句塊和鎖。你通常可以使用性能表現更良好的java.util.concurrent包中的併發數據結構
-
當你不得不使用鎖或者寫synchronized語句塊時,儘可能早的釋放鎖。當持有鎖時,儘量減少耗時操作,比如IO。此外在Lock或者synchronized語句塊中儘量避免再獲取其他鎖
再下一步檢查客戶端與服務器之間的網絡行爲。你可以通過類似ping和iftop之類的命令來操作,但你最好諮詢網絡管理員獲取詳細的網絡行爲信息。
最後一個可選項是引入更多的服務器從而減少每個服務器的併發度進而減小延遲。一個極端的例子是運行兩份服務器實例拷貝然後使用Jeff Dean在他的演講Taming Latency Variability中提到的第一份結果。此外,如果你想要獲得非常低的延遲,考慮使用類似 LMX disruptor這樣的工具。
性能調優清單
在文中我們討論了吞吐量調優和延遲調優。或許你現在會感嘆:Java服務器調優是一項技巧活,文中所提到的也只是冰山一角。下面是性能調優步驟總清單。它或許不會告訴你有關調優的一切,但它可能幫助你避開很多陷阱:
-
檢查機器的Load Average。如果它大於4*CPU核數,那麼機器處於過載狀態,你可以直接跳到CPU調優部分。
-
你是否給系統施加了足夠的負載?通過增加線程數來模擬更多的客戶端操作。如果那樣做提高了吞吐量那麼繼續增大負載知道達到最大吞吐量。
-
線程是否處於空閒狀態?如果你有太多的鎖或者太少線程,系統可能提供不了足夠的吞吐量。使用一個Profiler工具查看鎖狀態並且嘗試移除鎖。儘管一些鎖難以避免,但大部分情況是沒必要的。
-
嘗試增加線程池中的線程數,檢查那樣做是否能增加吞吐量。
-
現在檢查CPU熱點代碼
-
查看CPU熱點代碼的樹視圖並確保熱點代碼出現在預期位置。例如一個XML解析器預期會消耗很多CPU。但如果你發現非預期行爲消耗了過多CPU,那麼就需要修復它了。
-
檢查內存和GC,如果GC吞吐量小於90%,那麼就需要進行GC調優。如果內存不斷切換分頁,那麼增大內存並觀察是否有幫助。
-
觀察DB Profiles,確保最高負載部分出現在預期位置。
-
觀察網絡IO Profiles。找到熱點,確保大多數寫都在你的預期之中。
-
確保底層資源如網絡和Disk Mounting就緒
Java服務器調優非常具有技巧性,但相應的回報也很豐厚。有時候它更像是藝術而非工程,但根據我提供的步驟應該能讓你走得更遠。
轉載請註明出處:http://my.oschina.net/feichexia/blog/348773 謝謝。