|
關(guān)鍵時(shí)刻,第一時(shí)間送達(dá)! 在《2018 最具就業(yè)前景的 7 大編程語言》一文中,通過分析了來自 Indeed 的 25 門編程語言、棧和框架的數(shù)據(jù),我們盤點(diǎn)了18年最具就業(yè)前景的七大編程語言,其中,Java毫無懸念拔得頭籌。 那么對(duì)于開發(fā)者來說,如何讓 Java 應(yīng)用程序達(dá)到性能最佳?本文將一步步教你將 Java 應(yīng)用程序性能優(yōu)化到一流。 本文我們將探討一系列方法,用來提升 Java 應(yīng)用程序的性能。首先定義出可度量的性能指標(biāo),然后通過不同的工具來衡量和監(jiān)控應(yīng)用程序性能,并找到影響性能的瓶頸所在。 此外,我們還將展示一些常用的 Java 代碼級(jí)別優(yōu)化方法以及最佳的編碼實(shí)踐。最后,我們將深入 JVM 特定的調(diào)優(yōu)技巧和架構(gòu)改進(jìn)方法,以提升 Java 應(yīng)用程序的性能。 性能指標(biāo) 在開始動(dòng)手改進(jìn)應(yīng)用程序的性能之前,我們需要定義和理解非功能性需求的一些關(guān)鍵領(lǐng)域,比如可擴(kuò)展性、性能、可用性,等等。 以下是一些用來衡量 Web 應(yīng)用程序性能的常用指標(biāo):
借助不同的負(fù)載測(cè)試手段以及應(yīng)用程序監(jiān)測(cè)工具對(duì)這些指標(biāo)進(jìn)行量化,有助于找出性能瓶頸的關(guān)鍵點(diǎn)并對(duì)其進(jìn)行相應(yīng)的優(yōu)化,從而提升 Java 程序性能。 示例程序 首先創(chuàng)建一個(gè)示例程序,下文將基于該示例程序探討性能優(yōu)化方法。我使用一個(gè)簡(jiǎn)單的 Spring Boot Web 應(yīng)用程序用作本文示例程序(可參考https:///spring-boot-level-up/)。該程序負(fù)責(zé)管理員工列表,通過暴露出 REST API 用來進(jìn)行員工新增和檢索。 在下文中我們將把它作為負(fù)載測(cè)試的參考,來監(jiān)控多種性能指標(biāo)。 找到性能瓶頸 負(fù)載測(cè)試工具和應(yīng)用程序性能管理(APM)解決方案通常用于跟蹤和優(yōu)化 Java 應(yīng)用程序的性能。圍繞不同的應(yīng)用場(chǎng)景運(yùn)行負(fù)載測(cè)試,同時(shí)使用 APM 工具監(jiān)控 CPU、IO、內(nèi)存占用等情況是識(shí)別瓶頸的關(guān)鍵。 Gatling 是負(fù)載測(cè)試的最佳工具之一,它提供了對(duì) HTTP 協(xié)議的極佳支持,這使得它成為對(duì)任何 HTTP 服務(wù)器進(jìn)行負(fù)載測(cè)試的絕佳選擇。 Stackify 的 Retrace 是一個(gè)非常成熟的 APM 解決方案,它具有非常豐富的功能,可以幫助你確定應(yīng)用程序的基準(zhǔn)。Retrace 的關(guān)鍵組件之一是其代碼性能分析模塊,它能夠在不減緩應(yīng)用程序的情況下收集運(yùn)行時(shí)信息。 Retrace 還提供了其他組件,用于監(jiān)控基于 JVM 運(yùn)行的應(yīng)用程序的內(nèi)存、線程和類。除了應(yīng)用程序指標(biāo)之外,它還能夠監(jiān)控托管應(yīng)用程序的服務(wù)器的 CPU 和 IO 使用情況。 因此,像 Retrace 這樣全面的監(jiān)控工具將解鎖應(yīng)用程序性能優(yōu)化的第一部分。第二部分則需要對(duì)應(yīng)用程序在真實(shí)世界的使用情況和負(fù)載進(jìn)行重現(xiàn)來優(yōu)化。 想要重現(xiàn)并不容易,并且了解應(yīng)用程序的當(dāng)前性能配置文件也非常重要。接下來我們將重點(diǎn)關(guān)注這個(gè)問題。 Gatling 負(fù)載測(cè)試 Gatling 模擬腳本采用 Scala 編寫,它附帶了一個(gè)功能強(qiáng)大的 GUI,提供場(chǎng)景記錄功能。GUI 自動(dòng)創(chuàng)建 Scala 腳本呈現(xiàn)模擬測(cè)試結(jié)果。模擬測(cè)試完成之后,Gatling 能夠生成有用的、即時(shí)分析的 HTML 報(bào)告。 定義一個(gè)場(chǎng)景 在啟動(dòng)記錄器之前,我們需要定義一個(gè)場(chǎng)景,用于呈現(xiàn)用戶在瀏覽 Web 應(yīng)用程序時(shí)發(fā)生的情況。 在我們的例子中,測(cè)試方法是:“模擬200個(gè)用戶,每個(gè)用戶發(fā)起10000個(gè)請(qǐng)求。” 配置記錄器 根據(jù) Gatling 的第一步,使用以下代碼創(chuàng)建一個(gè)名為 EmployeeSimulation 的 scala 新文件: class EmployeeSimulation extends Simulation {運(yùn)行負(fù)載測(cè)試 執(zhí)行以下命令啟動(dòng)負(fù)載測(cè)試: $GATLING_HOME/bin/gatling.sh -s basic.EmployeeSimulation對(duì)應(yīng)用程序的 API 進(jìn)行負(fù)載測(cè)試有助于發(fā)現(xiàn)微小且隱蔽的 bug,例如數(shù)據(jù)庫連接耗盡,請(qǐng)求在高負(fù)載期間超時(shí),由于內(nèi)存泄漏而導(dǎo)致的超高堆棧占用,等等。 監(jiān)控應(yīng)用程序 想要使用 Retrace 進(jìn)行 Java 應(yīng)用程序的開發(fā),首先需要在 Stackify 上注冊(cè)一個(gè)免費(fèi)的試用版。 接下來,我們需要將我們的 Spring Boot 應(yīng)用程序配置為 Linux 服務(wù)。我們還需要在托管應(yīng)用程序的服務(wù)器上安裝 Retrace 代理。 一旦啟動(dòng)了 Retrace 代理與要監(jiān)控的 Java 應(yīng)用程序,我們即可在 Retrace 儀表板上點(diǎn)擊 AddApp 鏈接。之后 Retrace 將開始監(jiān)控我們的應(yīng)用程序。 尋找堆中最慢的部分 Retrace 自動(dòng)監(jiān)測(cè)我們的應(yīng)用程序,并跟蹤多種常見框架和依賴項(xiàng)的使用情況,包括SQL、MongoDB、Redis、Elasticsearch 等。如果應(yīng)用程序包含下列性能問題,Retrace 能夠幫助我們快速找到原因:
例如,下圖展示了在給定時(shí)間內(nèi)系統(tǒng)中運(yùn)行最緩慢的部件。 代碼級(jí)別優(yōu)化 負(fù)載測(cè)試和應(yīng)用程序監(jiān)控對(duì)于確定應(yīng)用程序中的一些關(guān)鍵瓶頸非常有用。但同時(shí),我們需要遵循良好的編碼實(shí)踐,盡量避免性能問題在開始應(yīng)用程序監(jiān)控之前出現(xiàn)。 使用 StringBuilder 進(jìn)行字符串連接 字符串連接是編程中非常普遍的操作,同時(shí)也是低效率的操作。簡(jiǎn)而言之,使用 = 來追加字符串的問題在于,每次操作都會(huì)分配新的字符串。 以一個(gè)簡(jiǎn)化的典型循環(huán)為例,我們分別用原始的字符串連接和 builder 方式來實(shí)現(xiàn)。 public String stringAppendLoop() {在上面的代碼中使用 StringBuilder 效率更高,尤其是對(duì)于頻繁進(jìn)行字符串操作的程序來說效率更加顯著。 需要說明的是,當(dāng)前版本的 JVM 自動(dòng)對(duì)字符串操作執(zhí)行了編譯和運(yùn)行時(shí)優(yōu)化。 避免遞歸 在 Java 應(yīng)用程序中,由于遞歸導(dǎo)致 StackOverFlowError 錯(cuò)誤是很常見的。 如果我們無法避免使用遞歸邏輯,那么盡量使用尾遞歸。 來看一個(gè)采用頭遞歸的例子: public int factorial(int n) {將其改為尾遞歸: private int factorial(int n, int accum) {其他一些 JVM 語言(如 Scala)已經(jīng)具有編譯器級(jí)別的支持來優(yōu)化尾遞歸代碼,并且正在設(shè)法將這種優(yōu)化類型引入到 Java 中。 謹(jǐn)慎使用正則表達(dá)式 正則表達(dá)式在很多應(yīng)用場(chǎng)景下確實(shí)作用明顯,但是它們往往需要非常高的性能成本。了解各種使用正則表達(dá)式的 JDK 字符串方法(如 String.replaceAll()或String.split())尤為重要。 如果你必須在計(jì)算密集型的代碼段中使用正則表達(dá)式,那么盡量使用 *Pattern *緩存模式,避免重復(fù)編譯: static final Pattern HEAVY_REGEX = Pattern.compile('(((X)*Y)*Z)*');對(duì)于操作字符串來說,使用像 Apache Commons Lang 這樣的流行庫也是一個(gè)很好的選擇。 避免創(chuàng)建和銷毀太多的線程 創(chuàng)建和處理線程是影響 JVM 性能的常見因素,因?yàn)榫€程對(duì)象的創(chuàng)建和銷毀開銷昂貴。 如果你的應(yīng)用程序需要使用大量的線程,那么線程池的使用將意義非凡,它可以讓這些昂貴的對(duì)象得到重復(fù)利用。 為此,Java 的 ExecutorService 提供了高級(jí) API 用來定義線程池并與之交互。 Java 7 中的 Fork/Join 框架也值得一提,它提供了一些工具來利用處理器多核優(yōu)勢(shì)從而加速并行處理。為了提供有效的并行執(zhí)行,框架使用了名為 ForkJoinPool 的線程池來管理工作線程。 JVM 調(diào)優(yōu) 優(yōu)化堆大小 為生產(chǎn)系統(tǒng)指定合適的 JVM 堆大小并非易事。首先需要回答下列問題來預(yù)測(cè)內(nèi)存需求:
在缺乏真實(shí)測(cè)試的情況下,這些數(shù)字很難估算。 獲得關(guān)于應(yīng)用程序需求最可靠的方法是對(duì)應(yīng)用程序進(jìn)行負(fù)載測(cè)試,并在運(yùn)行時(shí)跟蹤性能指標(biāo)。我們之前討論的基于 Gatling 的測(cè)試就是一個(gè)很好的方法。 選擇合適的垃圾收集器 對(duì)于大多數(shù)面向客戶端的 Java 應(yīng)用程序來說,Stop-the-world 垃圾收集器影響了程序的響應(yīng)能力和整體性能。 但是,新一代的垃圾收集器大多已經(jīng)解決了這個(gè)問題,并且通過適當(dāng)?shù)膬?yōu)化和調(diào)整,收集周期得到了弱化。但是想要做到這樣,你需要深入了解整個(gè) JVM 的垃圾收集機(jī)制以及應(yīng)用程序本身。 像分析器、heap dumps 以及 GC 日志記錄這樣的工具用處很大。同樣,它們都需要在真實(shí)的負(fù)載模式下才能派上用場(chǎng),正如前文討論的 Gatling 性能測(cè)試那樣。 有關(guān)不同垃圾收集器的更多信息,請(qǐng)參閱https:///what-is-java-garbage-collection/。 JDBC 性能 關(guān)系數(shù)據(jù)庫是影響 Java 應(yīng)用程序性能的另一個(gè)常見因素。為了獲得更快的請(qǐng)求響應(yīng)速度,我們必須關(guān)注應(yīng)用程序的每一層,并考慮代碼如何與底層 SQL DB 進(jìn)行交互。 連接池 眾所周知,數(shù)據(jù)庫連接代價(jià)是昂貴的。連接池是優(yōu)化該問題的重要機(jī)制。 強(qiáng)烈推薦 HikariCP JDBC ,它是一個(gè)輕量級(jí)(大約130Kb)且速度非??斓?JDBC 連接池框架。 JDBC 批量處理 在數(shù)據(jù)持久化過程中盡可能地批量操作。JDBC 批處理允許我們?cè)趩蝹€(gè)數(shù)據(jù)庫交互中發(fā)送多個(gè) SQL 語句。 批處理使得驅(qū)動(dòng)和數(shù)據(jù)庫本身的性能都得到提升。PreparedStatement 是批處理的絕佳選擇,一些數(shù)據(jù)庫系統(tǒng)(例如 Oracle)僅支持預(yù)處理語句的批處理。 Hibernate 則更加靈活,允許我們切換到單一配置的批處理。 語句緩存 語句緩存是另一種能夠提高持久層性能的方法 - 一種你可以輕松利用但鮮為人知的性能優(yōu)化手段。 基于 JDBC 驅(qū)動(dòng)程序,你可以在客戶端(Driver)或數(shù)據(jù)庫端(語法樹甚至執(zhí)行計(jì)劃)上緩存 PreparedStatement。 縱向擴(kuò)展與橫向擴(kuò)展 數(shù)據(jù)庫復(fù)制和分片是提高吞吐量的重要手段,我們應(yīng)該充分利用這些久經(jīng)沙場(chǎng)的體系結(jié)構(gòu)來擴(kuò)展企業(yè)應(yīng)用程序的持久層。 架構(gòu)改進(jìn) 高速緩存 如今內(nèi)存價(jià)格不再昂貴而且會(huì)變得越來越低,但是從磁盤或者網(wǎng)絡(luò)檢索數(shù)據(jù)的代價(jià)依然很高。顯然,緩存是我們?cè)谔嵘绦蛐阅軙r(shí)不容忽視的環(huán)節(jié)。 當(dāng)然,將獨(dú)立緩存系統(tǒng)引入到應(yīng)用程序的拓?fù)浣Y(jié)構(gòu)中會(huì)增加架構(gòu)的復(fù)雜性,因此想要利用緩存,最直接的方式是充分利用已經(jīng)使用的庫和框架中的現(xiàn)有緩存功能。 例如,大多數(shù)持久性框架都有很好的緩存支持。Spring MVC 等 Web 框架還可以利用Spring 中內(nèi)置的緩存支持以及基于 ETags 的強(qiáng)大的 HTTP 級(jí)緩存。 簡(jiǎn)單使用了緩存之后,便能頻繁訪問應(yīng)用程序。如果想要更進(jìn)一步,那么諸如 Redis、Ehcache 或 Memcache 這樣的獨(dú)立緩存服務(wù)器是很好的選擇,它們能夠減少數(shù)據(jù)庫負(fù)載并提升應(yīng)用程序性能。 橫向擴(kuò)展 無論我們?yōu)槌绦蚨哑隽硕嗌儆布傆心硞€(gè)時(shí)刻會(huì)顯得依然不夠。雖然橫向擴(kuò)展天生存在局限性,但是當(dāng)系統(tǒng)遇到問題時(shí),橫向擴(kuò)展依然是支撐更多負(fù)載的唯一途徑。 橫向擴(kuò)展實(shí)施起來實(shí)屬不易,但它是在系統(tǒng)遭遇某些瓶頸時(shí)的唯一解決方法。 而且,大多數(shù)的現(xiàn)代框架和庫都支持橫向擴(kuò)展。Spring 生態(tài)系統(tǒng)有項(xiàng)目組專門用于解決該領(lǐng)域的應(yīng)用程序體系結(jié)構(gòu)問題,其他大多數(shù)項(xiàng)目都有類似的支持。 最后,除了純粹的 Java 性能之外,在集群的幫助下進(jìn)行擴(kuò)展的另外一個(gè)好處是,添加新節(jié)點(diǎn)還會(huì)導(dǎo)致冗余和更好的處理故障的技術(shù),從而提高整個(gè)系統(tǒng)的可用性。 總結(jié) 我們探討了許多不同的方法來提高 Java 應(yīng)用程序的性能。我們首先介紹了負(fù)載測(cè)試、基于 APM 工具的應(yīng)用程序和服務(wù)器監(jiān)控,隨后介紹了編寫高性能 Java 代碼的一些最佳實(shí)踐。 最后,我們研究了 JVM 特定的調(diào)優(yōu)技巧、數(shù)據(jù)庫優(yōu)化和架構(gòu)改進(jìn)方案,以擴(kuò)展我們的應(yīng)用程序。
作者:Eugen Paraschiv |
|
|