摘要:本文首先介紹了負(fù)載測試、基于APM工具的應(yīng)用程序和服務(wù)器監(jiān)控,隨后介紹了編寫高性能Java代碼的一些最佳實踐。最后研究了JVM特定的調(diào)優(yōu)技巧、數(shù)據(jù)庫端的優(yōu)化和架構(gòu)方面的調(diào)整。以下是譯文。
在這篇文章中,我們將討論幾個有助于提升Java應(yīng)用程序性能的方法。我們首先將介紹如何定義可度量的性能指標(biāo),然后看看有哪些工具可以用來度量和監(jiān)控應(yīng)用程序性能,以及確定性能瓶頸。
我們還將看到一些常見的Java代碼優(yōu)化方法以及最佳編碼實踐。最后,我們將看看用于提升Java應(yīng)用程序性能的JVM調(diào)優(yōu)技巧和架構(gòu)調(diào)整。
請注意,性能優(yōu)化是一個很寬泛的話題,而本文只是對JVM探索的一個起點。
在開始優(yōu)化應(yīng)用程序的性能之前,我們需要理解諸如可擴(kuò)展性、性能、可用性等方面的非功能需求。
以下是典型Web應(yīng)用程序常用的一些性能指標(biāo):
應(yīng)用程序平均響應(yīng)時間
系統(tǒng)必須支持的平均并發(fā)用戶數(shù)
在負(fù)載高峰期間,預(yù)期的每秒請求數(shù)
這些指標(biāo)可以通過使用多種監(jiān)視工具監(jiān)測到,它們對分析性能瓶頸和性能調(diào)優(yōu)有著非常大的作用。
我們將使用一個簡單的Spring Boot Web應(yīng)用程序作為示例,在這篇文章中有相關(guān)的介紹。這個應(yīng)用程序可用于管理員工列表,并對外公開了添加和檢索員工的REST API。
我們將使用這個程序作為參考來運行負(fù)載測試,并在接下來的章節(jié)中監(jiān)控各種應(yīng)用指標(biāo)。
負(fù)載測試工具和應(yīng)用程序性能管理(APM)解決方案常用于跟蹤和優(yōu)化Java應(yīng)用程序的性能。要找出性能瓶頸,主要就是對各種應(yīng)用場景進(jìn)行負(fù)載測試,并同時使用APM工具對CPU、IO、堆的使用情況進(jìn)行監(jiān)控等等。
Gatling是進(jìn)行負(fù)載測試最好的工具之一,它提供了對HTTP協(xié)議的支持,是HTTP服務(wù)器負(fù)載測試的絕佳選擇。
Stackify的Retrace是一個成熟的APM解決方案。它的功能很豐富,對確定應(yīng)用程序的性能基線很有幫助。 Retrace的關(guān)鍵組件之一是它的代碼分析功能,它能夠在不減慢應(yīng)用程序的情況下收集運行時信息。
Retrace還提供了監(jiān)視基于JVM應(yīng)用程序的內(nèi)存、線程和類的小部件。除了應(yīng)用程序本身的指標(biāo)之外,它還支持監(jiān)視托管應(yīng)用程序的服務(wù)器的CPU和IO使用情況。
因此,像Retrace這樣功能全面的監(jiān)控工具是解鎖應(yīng)用程序性能潛力的第一步。而第二步則是在你的系統(tǒng)上重現(xiàn)真實使用場景和負(fù)載。
說起來容易,做起來難,而且了解應(yīng)用程序當(dāng)前的性能也非常重要。這就是我們接下來要關(guān)注的問題。
Gatling的模擬測試腳本是用Scala編寫的,但該工具還附帶了一個非常有用的圖形界面,可用于記錄具體的場景,并生成Scala腳本。
在運行模擬腳本之后,Gatling會生成一份非常有用的、可用于分析的HTML報告。
在啟動記錄器之前,我們需要定義一個場景,表示用戶在瀏覽Web應(yīng)用時發(fā)生的事情。
在我們的這個例子中,具體的場景將是“啟動200個用戶,每個用戶發(fā)出一萬個請求?!?/p>
根據(jù)“Gatling的第一步”所述,用下面的代碼創(chuàng)建一個名為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)) }
要執(zhí)行負(fù)載測試,請運行以下命令:
$GATLING_HOME/bin/gatling.sh-sbasic.EmployeeSimulation
對應(yīng)用程序的API進(jìn)行負(fù)載測試有助于發(fā)現(xiàn)及其細(xì)微的并且難以發(fā)現(xiàn)的錯誤,如數(shù)據(jù)庫連接耗盡、高負(fù)載情況下的請求超時、因為內(nèi)存泄漏而導(dǎo)致堆的高使用率等等。
要使用Retrace進(jìn)行Java應(yīng)用程序的開發(fā),首先需要在Stackify上申請免費試用賬號。然后,將我們自己的Spring Boot應(yīng)用程序配置為Linux服務(wù)。我們還需要在托管應(yīng)用程序的服務(wù)器上安裝Retrace代理,按照這篇文章所述的操作即可。
Retrace代理和要監(jiān)控的Java應(yīng)用程序啟動后,我們就可以到Retrace儀表板上單擊AddApp按鈕添加應(yīng)用了。添加應(yīng)用完成之后,Retrace將開始監(jiān)控應(yīng)用程序了。
Retrace會自動監(jiān)控應(yīng)用程序,并跟蹤數(shù)十種常見框架及其依賴關(guān)系的使用情況,包括SQL、MongoDB、Redis、Elasticsearch等等。Retrace能幫助我們快速確定應(yīng)用程序為什么會出現(xiàn)如下性能問題:
某個SQL語句是否會拖慢系統(tǒng)的速度?
Redis突然變慢了嗎?
特定的HTTP Web服務(wù)宕了,還是變慢了?
例如,下面的圖形展示了在一段給定的時間內(nèi)速度最慢的組件。
負(fù)載測試和應(yīng)用程序監(jiān)控對于確定應(yīng)用程序的一些關(guān)鍵性能瓶頸非常有用。但同時,我們需要遵循良好的編碼習(xí)慣,以避免在對應(yīng)用程序進(jìn)行監(jiān)控的時候出現(xiàn)過多的性能問題。
在下一章節(jié)中,我們將來看一些最佳實踐。
字符串連接是一個非常常見的操作,也是一個低效率的操作。簡單地說,使用+=來追加字符串的問題在于每次操作都會分配新的String。
下面這個例子是一個簡化了的但卻很典型的循環(huán)。前面使用了原始的連接方式,后面使用了構(gòu)建器:
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對性能的提升非常有效。請注意,現(xiàn)代的JVM會在編譯或者運行時對字符串操作進(jìn)行優(yōu)化。
導(dǎo)致出現(xiàn)StackOverFlowError錯誤的遞歸代碼邏輯是Java應(yīng)用程序中另一種常見的問題。如果無法去掉遞歸邏輯,那么尾遞歸作為替代方案將會更好。
我們來看一個頭遞歸的例子:
public int factorial(int n) { if (n == 0) { return 1; } else { return n * factorial(n - 1); } }
現(xiàn)在我們把它重寫為尾遞歸:
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)已經(jīng)在編譯器級支持尾遞歸代碼的優(yōu)化,當(dāng)然,對于這種優(yōu)化目前也存在著一些爭議。
正則表達(dá)式在很多場景中都非常有用,但它們往往具有非常高的性能成本。了解各種使用正則表達(dá)式的JDK字符串方法很重要,例如String.replaceAll()、String.split()。
如果你不得不在計算密集的代碼段中使用正則表達(dá)式,那么需要緩存Pattern的引用而避免重復(fù)編譯:
static final Pattern HEAVY_REGEX = Pattern.compile("(((X)*Y)*Z)*");
使用一些流行的庫,比如Apache Commons Lang也是一個很好的選擇,特別是在字符串的操作方面。
線程的創(chuàng)建和處置是JVM出現(xiàn)性能問題的常見原因,因為線程對象的創(chuàng)建和銷毀相對較重。
如果應(yīng)用程序使用了大量的線程,那么使用線程池會更加有用,因為線程池允許這些昂貴的對象被重用。
為此,Java的ExecutorService是線程池的基礎(chǔ),它提供了一個高級API來定義線程池的語義并與之進(jìn)行交互。
Java 7中的Fork/Join框架也值得提一下,因為它提供了一些工具來嘗試使用所有可用的處理器核心以幫助加速并行處理。為了提高并行執(zhí)行效率,框架使用了一個名為ForkJoinPool的線程池來管理工作線程。
為生產(chǎn)系統(tǒng)確定合適的JVM堆大小并不是一件簡單的事情。要做的第一步是回答以下問題以預(yù)測內(nèi)存需求:
計劃要把多少個不同的應(yīng)用程序部署到單個JVM進(jìn)程中,例如EAR文件、WAR文件、jar文件的數(shù)量是多少?
在運行時可能會加載多少個Java類,包括第三方API的類?
估計內(nèi)存緩存所需的空間,例如,由應(yīng)用程序(和第三方API)加載的內(nèi)部緩存數(shù)據(jù)結(jié)構(gòu),比如從數(shù)據(jù)庫緩存的數(shù)據(jù)、從文件中讀取的數(shù)據(jù)等等。
估計應(yīng)用程序?qū)?chuàng)建的線程數(shù)。
如果沒有經(jīng)過真實場景的測試,這些數(shù)字很難估計。
要獲得有關(guān)應(yīng)用程序需求的最好最可靠的方法是對應(yīng)用程序執(zhí)行實際的負(fù)載測試,并在運行時跟蹤性能指標(biāo)。我們之前討論的基于Gatling的測試就是一個很好的方法。
Stop-the-world(STW)垃圾收集的周期是影響大多數(shù)面向客戶端應(yīng)用程序響應(yīng)和整體Java性能的大問題。但是,目前的垃圾收集器大多解決了這個問題,并且通過適當(dāng)?shù)膬?yōu)化和大小的調(diào)整,能夠消除對收集周期的感知。
分析器、堆轉(zhuǎn)儲和詳細(xì)的GC日志記錄工具對此有一定的幫助作用。再一次注意,這些都需要在真實場景的負(fù)載模式下進(jìn)行監(jiān)控。
有關(guān)不同垃圾收集器的更多信息,請查看這個指南。
關(guān)系型數(shù)據(jù)庫是Java應(yīng)用程序中另一個常見的性能問題。為了獲得完整請求的響應(yīng)時間,我們很自然地必須查看應(yīng)用程序的每一層,并思考如何讓代碼與底層SQL DB進(jìn)行交互。
讓我們從眾所周知的事實開始,即數(shù)據(jù)庫連接是昂貴的。 連接池機(jī)制是解決這個問題非常重要的第一步。
這里建議使用HikariCP JDBC,這是一個非常輕量級(大約130Kb)并且速度極快的JDBC連接池框架。
持久化處理應(yīng)盡可能地執(zhí)行批量操作。 JDBC批處理允許我們在單次數(shù)據(jù)庫交互中發(fā)送多個SQL語句。
這樣,無論是在驅(qū)動端還是在數(shù)據(jù)庫端,性能都可能得到顯著地提升。 * PreparedStatement*是一個非常棒的的批處理命令,一些數(shù)據(jù)庫系統(tǒng)(例如Oracle)只支持預(yù)處理語句的批處理。
另一方面,Hibernate則更加靈活,它允許我們只需修改一個配置即可快速切換為批處理操作。
語句緩存是另一種提高持久層性能的方法,這是一種鮮為人知但又容易掌握的性能優(yōu)化方法。
只要底層的JDBC驅(qū)動程序支持,你就可以在客戶端(驅(qū)動程序)或數(shù)據(jù)庫端(語法樹甚至執(zhí)行計劃)中緩存PreparedStatement。
數(shù)據(jù)庫復(fù)制和分片是提高吞吐量非常好的方法,我們應(yīng)該充分利用這些經(jīng)過實踐檢驗的架構(gòu)模式,以擴(kuò)展企業(yè)應(yīng)用的持久層。
現(xiàn)在內(nèi)存的價格很低,而且越來越低,從磁盤或通過網(wǎng)絡(luò)來檢索數(shù)據(jù)的性能代價仍然很高。緩存自然而然的變成了在應(yīng)用程序性能方面不能忽視的關(guān)鍵。
當(dāng)然,在應(yīng)用的拓?fù)浣Y(jié)構(gòu)中引入一個獨立的緩存系統(tǒng)確實會增加架構(gòu)的復(fù)雜度,所以,應(yīng)當(dāng)充分利用當(dāng)前使用的庫和框架現(xiàn)有的緩存功能。
例如,大多數(shù)的持久化框架都支持緩存。 Spring MVC等Web框架還可以使用Spring中內(nèi)置的緩存支持,以及基于ETags的強(qiáng)大的HTTP級緩存。
無論我們在單個實例中準(zhǔn)備了多少硬件,都會有不夠用的時候。簡而言之,擴(kuò)展有著天生的局限性,當(dāng)系統(tǒng)遇到這些問題時,橫向擴(kuò)展是處理更多負(fù)載的唯一途徑。這一步肯定會相當(dāng)?shù)膹?fù)雜,但卻是擴(kuò)展應(yīng)用的唯一辦法。
對大多數(shù)的現(xiàn)代框架和庫來說,這方面還是支持得很好的,而且會變得越來越好。 Spring生態(tài)系統(tǒng)有一個完整的項目集,專門用于解決這個特定的應(yīng)用程序架構(gòu)領(lǐng)域,其他大多數(shù)的框架也都有類似的支持。
除了能夠提升Java的性能,通過集群進(jìn)行橫向擴(kuò)展也有其他的好處,添加新的節(jié)點能產(chǎn)生冗余,并更好的處理故障,從而提高整個系統(tǒng)的可用性。
在這篇文章中,我們圍繞著提升Java應(yīng)用的性能探討了許多概念。我們首先介紹了負(fù)載測試、基于APM工具的應(yīng)用程序和服務(wù)器監(jiān)控,隨后介紹了編寫高性能Java代碼的一些最佳實踐。最后,我們研究了JVM特定的調(diào)優(yōu)技巧、數(shù)據(jù)庫端的優(yōu)化和架構(gòu)方面的調(diào)整。