21CTO 導讀:讓 Java 應用程式以最佳性能運行需要開發者的一些努力。本文為確保 Java 以最佳性能運行的方法。
簡介
在本文中,我們將討論一些有助於提高 Java 應用程式性能的方法。
我們將從怎樣定義可量化的性能目標開始,然後我們再用不同的工具來衡量和監控應用程式的性能,以找出瓶頸出現的地方,以便能夠對症下藥。
另外,我們還會介紹一些常見的 Java 代碼級優化以及編程的最佳實踐。最後,我將介紹 JVM和體系結構的特定調優方法,以提高 Java 應用程式的性能。
當然,性能優化是一個很寬很廣的主題,這只是在 JVM 上探索一個起點。
優化目標
在開始提高應用程式性能之前,我們需要定義和理解關於非功能需求方法的關鍵點,這裡有可伸縮性,性能,可用性等。
我總結了典型 Web 應用程式的一些常用性能指標,如下:
1、應用平均響應時間
2、系統必須支持的平均並發用戶數
3、峰值最高負載期間,預期的每秒請求數(QPS)
通過使用不同的負載測試和應用監測工具(APM)解決方案通常用於跟蹤與優化 Java應用程式的性能。我們圍繞不同的應用場景運行負載測試,同時使用 APM 工具監控 CPU、IO、內存堆棧使用情況,此為識別瓶頸之關鍵。
Gatling (https://gatling.io/)
Gatling 就是一款負載測試的最佳工具之一,它對 HTTP 協議有著非常出色的支持,從而使之成為大多數 HTTP 伺服器做負載測試的良好選擇。
Retrace (https://stackify.com/retrace/)
Stackify 的Retrace 提供了一個成熟的 APM 解決方案,能夠幫助確定應用程式基線的好方法,也有很豐富的功能。其關鍵組件之一是代碼分析,可以在不降低應用程式性能的情況下收集運行時信息。
Retrace 還有用於監視正在運行的應用程式,基於 JVM 的內存、線程和類的小組件。除了應用程式指標外,它還支持監控託管伺服器的 CPU 與 IO 使用的信息。其涵蓋了應用程式性能潛力,另外亦能看到真實情況的使用和負載率。
實際上比寫起文章來要複雜得多,要理解應用程式的當前性能配置也很關鍵。接下來我們就來關注以下內容。
Gatling 的負載試驗
Gatling 是仿真環境腳本是使用 Scala 編寫的,它還提供了一個 GUI 環境,可以記錄場景之數據,由 GUI 創建模擬的 Scala 腳本。運行完仿真模擬環境後,Gatling 會生成有用的,可供分析的 HTML 報告。
一、定義方案
在啟動之前,我們需要定義一個場景,即用戶在使用 Web 應用程式時會發生哪些動作。
在本例中,場景為:「啟動200個用戶,每個用戶生成10,000個請求」。
配置記錄器
我們使用 Gatling 官網上的手冊:Gatling first steps(https://github.com/excilys/gatling/wiki/First-Steps-with-Gatling),使用以下代碼創建一個文件:
EmployeeSimulation的scala 文件,代碼如下:
class EmployeeSimulation extends Simulation {
val scn = scenario("FetchEmployees").repeat(10000) {
exec(
http("GetEmployees-API")
.get("http://localhost:8080/employees")
.check(status.is(200))
)
}
setUp(scn.users(200).ramp(100))
}
運行負載測試
要執行負載測試,我們運行如下的命令:
$GATLING_HOME/bin/gatling.sh -s basic.EmployeeSimulation
對應用程式的 API 進行負載測試有助開發者找到細微的、不容易發現的錯誤,例如資料庫連接池耗盡,在請求發生高負載的同時,因為代碼等問題導至內存洩漏等不必要的內存高使用率等問題。
監控應用程式
開始使用 Rectrace,第一步先在 Stackify 上註冊免費試用版。網址:https://stackify.com/retrace/
接下來,需要將 Spring Boot 應用程式配置為 Linux 服務,接下來再將 Rectrace Proxy 安裝到應用程式所在的伺服器上。詳細連結:https://support.stackify.com/hc/en-us/articles/205419575
啟動 Retrace 代理與Java 應用,接下在 Retrace 的網站儀錶盤中添加 AddApp 連結,填寫好信息好,Retrace 就會很好的監控我們的應用了。
找到應用中最慢的部分
Retrace 能夠自動檢測到應用程式中問題,包括數十種常見框架和依賴項的使用,包括 SQL、MongoDB、Redis、Elasticsearch 等,比如:
1)某個 SQL 語句拖慢了應用嗎?
2)Redis 掛掉了嗎?
3)某個 HTTP Web 伺服器的性能變慢了嗎?
下圖完整的顯示了在指定連續時間中技術棧最慢部分和周邊的數據。
代碼級優化
雖然 APM 提供了負載和應用程式測試能夠幫助我們識別應用程式中的瓶頸。但是最重要還是開發者的技術能力,遵循良好的編碼實踐,才能在進行應用程式監控之前避免很多性能問題。
使用 StringBuilder 進行字符串連接
字符串連接是一種常見的操作,但也是一種低效的操作。簡單的講,在 Java 中使用+=的問題,它會為每個新操作分配一個新的 String。
例如,這是一個簡化且典型的循環結構,首先使用原始連接,然後再使用屬性構建器。如下代碼:
public String stringAppendLoop() {
String s = "";
for (int i = 0; i < 10000; i++) {
if (s.length() > 0)
s += ", ";
s += "bar";
}
return s;
}
public String stringAppendBuilderLoop() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
if (sb.length() > 0)
sb.append(", ");
sb.append("bar");
}
return sb.toString();
}
在以上代碼可以看到,改用StringBuilder的效率要高得多,特別是要考慮到這些基於String的操作頻率。
在我們繼續之前,請注意當前一代JVM確實對字符串操作執行編譯和/或運行時優化(參考:https://docs.oracle.com/javase/specs/jls/se9/html/jls-15.html#jls-15.18.1)。
避免使用遞歸
使用遞歸操作是導至 StackOverFlow Error 的常見現象。如果你非用遞歸不可,那麼使用尾遞歸作為替代方案可能會更好。
先來看一個頭部遞歸(Head-recursive)的例子:
public int factorial(int n) {
if (n == 0) {
return 1;
} else {
return n * factorial(n - 1);
}
}
下面重寫為 尾部遞歸 (tail recursive)的例子:
private int factorial(int n, int accum) {
if (n == 0) {
return accum;
} else {
return factorial(n - 1, accum * n);
}
}
public int factorial(int n) {
return factorial(n, 1);
}
其它類 JVM 語言,如 Scala 已經有編譯器級別的支持來特別優化尾遞歸代碼,已經有議程討論將此類優化加入 Java 的問題。
謹慎使用正則表達式
正則在編程很多場景都很有用,但是用它們太多會付出比較高的性能成本。
因此,開發者要了解各種 JDK 字符串處理方法也很重要,這些方法比使用正則表達式效率更高更快,如 String.replaceAll()或 String.split()。
如果在計算密集型的代碼中使用正則表達式,則可以使用緩存 Pattern 引用,而不用重複編譯。如下代碼:
static final Pattern HEAVY_REGEX = Pattern.compile("(((X)*Y)*Z)*");
另外,使用像Apache Commons Lang(https://commons.apache.org/proper/commons-lang/)這樣的庫也是很好的選擇,特別是對字符串的操作。
避免創建和銷毀太多線程
創建和處理線程是 JVM 出現性能問題的常見原因,因為線程對象的創建和銷毀開銷相對較重。如果應用程式中使用大量線程,那麼可以使用線程池就非常有意義,它允許我們重用這些昂貴的對象。
因此,Java Executor Service 是基於此點的基礎,它提供了一個高級的 API 來定義線程池的語義並與其交互。
Java 7的Fork/Join框架中提供了一些工具,可以嘗試使用所有可用處理器內核來加速並行處理。 為提供有效的並行執行,框架用一個名為ForkJoinPool的線程池來管理工作線程。
JVM 調優
堆大小調整
確定生產系統的 JVM 的正確大小並非一件簡單的工作。我們通過以下問題確定可預測的內存需求:
1、我們計劃將多少個不同的應用程式部署到單個JVM進程,例如,EAR文件,WAR文件,jar文件等的數量?
2、在運行時可能加載多少個Java類,包含多少個第三方API?
3、估算內存緩存所需的佔用空間,例如,由我們的應用程式(加第三方API)加載的內部緩存數據結構,例如來自資料庫的緩存數據,從文件讀取的數據等。
4、估計應用程式要創建的線程數。
如果沒有一些真實的測試,這些數字是很難估算出來的。
了解應用需求的最可靠方法是對應用程式進行實際負載測試並在運行時跟蹤度量。 前面討論的基於Gatling的測試是一個很好的方法。
選擇垃圾收集器
停止全局的垃圾收集周期。過去此種原因是大多數面向用戶的應用程式的響應能力和整體Java性能的巨大問題所在。
現在的 Java 垃圾收集器已經解決了這個問題,並且通過適當的調整和調整,沒有明顯的收集周期。 話雖如此,也需要深入了解整個JVM上的GC,以及應用程式的特定配置文件,能夠更好地實現目標,比如分析器,堆轉儲和詳細的GC日誌等會幫到我們。這些例外都需要在實際負載中捕獲,也就是前面Gatling性能測試的結果。
有關不同垃圾收集器的更多主題,可以查看指南:https://stackify.com/what-is-java-garbage-collection/。
JDBC性能
關係資料庫是Java應用程式中的另一個典型且常見的性能問題。為了獲取完整請求的響應時間,我們需要查看應用程式的每一層,看代碼如何與底層資料庫進行SQL交互。
連接池
從眾所周知的事實開始,Java 中的資料庫連接很昂貴。 連接池機制是解決這個問題的第一步。
向大家推薦是HikariCP JDBC - https://vladmihalcea.com/2014/04/17/the-anatomy-of-connection-pooling/一個非常輕量級(大約130Kb)和快速JDBC連接池框架。
JDBC批處理
我們處理持久性的方式的另一個方面是嘗試儘可能批量操作。批處理讓我們在單個資料庫通訊中發送多個SQL語句。
在驅動程序和資料庫端中性能都很重要。PreparedStatement是批處理的理想選擇,一些資料庫系統(如Oracle)僅支持對預準備語句進行批處理。
Hibernate更靈活,允許我們使用單一配置切換到JDBC批處理。
聲明緩存
接下來,SQL語句緩存是另一種可能提高持久層性能的方法:這是一種鮮為人知的性能優化。
根據底層JDBC驅動程序,你可以在客戶端(驅動程序)或資料庫端(語法樹甚至執行計劃)緩存PreparedStatement 。
橫向擴展和擴容
資料庫複製和分片也是提高吞吐量的最佳實踐,我們應利用這些經過實戰考驗的體系架構來擴展應用程式的持久層。
架構改進
高速緩存
內存的價格會越來越低,但從磁碟或通過網絡檢索數據仍然很昂貴。 緩存是我們不應忽視的應用程式性能的一個重要方面。
在應用程式中引入獨立的緩存系統會增加架構的一部分複雜性,開始時就要充分利用好已有的庫和框架中的緩存功能。
大多數持久性框架都有很好的緩存支持。諸如Spring MVC之類的Web框架,可以利用Spring中的內置緩存支持,以及基於ETag的HTTP級緩存。
於是,選擇完所有懸而未決的成果之後,架設一個獨立的緩存伺服器(如Redis,Ehcache或Memcache),緩存經常訪問應用的內容是開發者的下一步。
為減少資料庫負載,為提升應用程式性能提供重要的支撐。
擴容
無論我們在單個實例上投入多少硬體,在某些時候這都是不夠的。單機擴展具有很大的局限性 ,當系統遇到瓶頸時,不要被成本束縛手腳,該擴容擴容,它有時是處理更大負載的唯一方法。
也許,這一步具有很大的複雜性,它是在某一點達到瓶頸後,擴展應用程式的唯一方法。
在大多數現代框架和庫中,大多數支持是好的並且會越來越好。Spring生態系統有一整套專門用於解決這一特定應用程式架構的,其它大多數框架也都有類似支持。
除了純Java性能之外,在集群或雲架構下進行擴展的另一個優勢是,添加新節點還可以實現冗餘和更好的處理單點故障,從而實現系統整體的更高可用性。
小結
在本文裡,我們一起討論了很多有關提高Java應用性能的各種概念。 我們從負載測試談起,討論基於APM工具的應用程式和伺服器監控,接下來圍繞編寫高性能Java代碼的最佳實踐。
最後,我們還討論了JVM特定的調優技巧,資料庫端性能增強和架構的升級優化,通過這些,有效擴展Java應用程式,讓你不再為 Java 性能而擔心發愁。
作者:喬喬
來源:21CTO社區