前面的話
我們在工作過程中,肯定會遇到性能調(diào)優(yōu)及內(nèi)存溢出的問題,本篇文章會通過幾個(gè)小例子來粗略的介紹性能定位的思路及工具的使用。
性能問題分類
我們經(jīng)常遇到的服務(wù)端的性能問題一般有如下幾種:
1、接口時(shí)延過高,TPS不達(dá)標(biāo)
2、內(nèi)存溢出
栗子說明
本文栗子為使用 springboot 快速開發(fā)了兩個(gè) http 接口,一個(gè)是列表排序栗子,模擬耗時(shí)操作,一個(gè)是往一個(gè)全局列表中不停的插入數(shù)據(jù)達(dá)到內(nèi)存溢出的效果。
關(guān)于列表排序,這里使用兩種排序方式,一種是簡單的冒泡排序,一種是 jdk 里列表的排序方式:加強(qiáng)型多路歸并排序,用兩個(gè)排序算法主要為了說明 JHM 的使用方式。
TPS 不達(dá)標(biāo)問題分析
對于此類問題,則一般是在性能測試階段就能發(fā)現(xiàn)。此時(shí)調(diào)優(yōu)一般在性能測試環(huán)境上進(jìn)行。
如何找出耗時(shí)操作呢,JDK 已經(jīng)給我們提供了一系列的工具來定位該問題了,這里我們使用Java VisualVM
來診斷接口性能。
首先在啟動腳本里打開 JVM 的 JMX 端口,打開方式為-Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Djava.rmi.server.hostname=10.234.196.199
啟動之后,我們就可以通過Java VisualVM
來監(jiān)控我們的 JVM 了。
如圖:
打開抽樣器,進(jìn)行 CPU 抽樣,統(tǒng)計(jì)各個(gè)接口消耗 CPU 時(shí)間。
使用壓測工具,持續(xù)的壓測有性能問題的接口。這里使用 jmeter 進(jìn)行壓測。壓測一段時(shí)間后,打印 CPU 快照,如圖:
這里發(fā)現(xiàn)我們在調(diào)用 getPerf 接口時(shí),進(jìn)一步調(diào)用了 process1 接口,這個(gè)接口里有 bubbleSort 方法和 jdk 自帶的 sort 兩個(gè)調(diào)用。這兩個(gè)都是對列表排序,發(fā)現(xiàn)大部分時(shí)間都耗在冒泡排序上。這里 bubbleSort 就是需要優(yōu)化的地方。排序算法有很多,不同的數(shù)據(jù)量,不同的排序方法耗時(shí)也不一樣。這里需要用 JMH 來評估算法的耗時(shí)。
JMH 相關(guān)介紹可以參考JMH,這里有相關(guān)的例子可以參考
一般對于耗時(shí)操作的優(yōu)化,可以有如下方式:
1、優(yōu)化自身算法,降低算法的時(shí)間復(fù)雜度
2、同步操作異步化。
對于異步化操作,又有如下方式:
1、異步線程
2、線程池,線程復(fù)用(線程池的大小如何確定,CPU 密集型和 IO 密集型)
3、發(fā)布訂閱(消息隊(duì)列或者 spring 的 event 機(jī)制)
3、使用緩存機(jī)制【多級緩存,問題:緩存一致性,緩存防并發(fā),防雪崩----一個(gè)大專題】
4、業(yè)務(wù)流程上進(jìn)行優(yōu)化,提供專門的接口,只做當(dāng)前業(yè)務(wù),不考慮復(fù)用性。
5、如果是數(shù)據(jù)庫查詢慢,則需要優(yōu)化數(shù)據(jù)庫【這又是一個(gè)大專題】。sql 優(yōu)化??? 表優(yōu)化,如果有聯(lián)表查詢,則可以考慮不滿足 3 范式,拉平表結(jié)構(gòu)。
如果無法在測試環(huán)境上復(fù)現(xiàn),則可以試用 arthas 工具,attach 到相關(guān)進(jìn)程,通過 arthas 命令大致查看每個(gè)請求的耗時(shí)。關(guān)于 arthas 的用法,可以參考arthas
內(nèi)存溢出問題分析
為什么內(nèi)存溢出會出現(xiàn)接口時(shí)延過高呢?
我們服務(wù)端一般是 JAVA 語言開發(fā),如果 JVM 虛擬機(jī)內(nèi)存不足時(shí),會觸發(fā) FullGC,F(xiàn)ullGC 會吃大量的 CPU 時(shí)間。如果我們的內(nèi)存一直不足,頻繁的 GC,則會 STW,CPU 居高不下,留給業(yè)務(wù)的 CPU 時(shí)間就降低,導(dǎo)致業(yè)務(wù)接口時(shí)延上升。
內(nèi)存溢出的例子代碼如下
public void process2() {
String name = "The Spring Framework provides a comprehensive programming and configuration model for" +
"modern Java-based enterprise applications - on any kind of deployment platform" +
"A key element of Spring is infrastructural support at the application level: Spring focuses on the" +
"Complete set of java.time based setters on HttpHeaders, CacheControl, CorsConfiguration.\n" +
"@RequestMapping has enhanced produces condition support such that if a media type is declared with a specific parameter, and the requested media types (e.g. from \"Accept\" header) also has that parameter, the parameter values must match. This can be used for example to differentiate methods producing ATOM feeds \"application/atom+xml;type=feed\" vs ATOM entries \"application/atom+xml;type=entry\".\n" +
"CORS revision that adds Vary header for non CORS requests on CORS enabled endpoints and avoid considering same-origin requests with an Origin header as a CORS request.\n" +
"Upgrade to Jackson 2.10\n" +
"Spring Web MVC\n" +
"New \"WebMvc.fn\" programming model, analogous to the existing \"WebFlux.fn\":\n" +
"A functional alternative to annotated controllers built on the Servlet API.\n" +
"WebMvc.fn Kotlin DSL.\n" +
"Request mapping performance optimizations through caching of the lookup path per HandlerMapping, and pre-computing frequently used data in RequestCondition implementations.\n" +
"Improved, compact logging of request mappings on startup.\n" +
"Spring WebFlux\n" +
"Refinements to WebClient API to make the retrieve() method useful for most common cases, specifically adding the ability to retrieve status and headers and addition to the body. The exchange() method is only for genuinely advanced cases, and when using it, applications can now rely on ClientResponse#createException to simplify selective handling of exceptions.\n" +
"Support for Kotlin Coroutines.\n" +
"Server and client now use Reactor checkpoints to insert information about the request URL being processed,sce or the handler used, that is then inserted into exceptions and logged below the exception stacktrace.\n" +
"Request mapping performance optimizations through pre-computing frequently used data in RequestCondition implementations.\n" +
"Header management performance optimizations by wrapping rather than copying server headers, and caching parsed representations of media types. Available from 5.1.1, see issue #21783 and commits under \"Issue Links\".\n" +
"Improved, compact logging of request mappings on startup.\n" +
"Add ServerWebExchangeContextFilter to expose the Reactor Context as an exchange attribute.\n" +
"Add FreeMarker macros support.\n" +
"MultipartBodyBuilder improvements to allow Publisher and Part as input along with option to specify the filename to use for a part.";
list.add(name + System.currentTimeMillis());
}
這里往一個(gè)全局的 list 中添加一個(gè)字符串,每次請求時(shí),添加一個(gè)字符串。
-Xms200m -Xmx200m
這里把 jvm 堆內(nèi)存大小設(shè)置為 200m。
對于內(nèi)存溢出,則需要 gc log 和內(nèi)存快照。gc log 可以在https://gceasy.io上面分析,可以看到相關(guān)的fullgc和yong gc 的情況。gc 分析如圖:
該圖表明發(fā)生 GC 之后,對大小并沒有明顯的減少,可能是堆內(nèi)存不太夠用。圖左邊的每個(gè)按鈕對應(yīng)一個(gè)分析。
定位出內(nèi)存不足后,就要看內(nèi)存中哪些對象回收不掉,這時(shí)需要使用到 jmap 命令,dump 出內(nèi)存快照。命令如下:
jmap -dump:format=b,file=heapdump.hprof pid
?,
獲取到內(nèi)存快照可以使用 mat 進(jìn)行分析。
使用 mat 打開快照文件,如下:
這里看到最大一塊內(nèi)存是 81.7M,點(diǎn)擊餅圖進(jìn)入如下頁面:
上圖可以看到在類 businessServiceImpl 中有個(gè) list,該 list 共有 16081 個(gè)元素,每個(gè)元素大小 5296 個(gè)字節(jié)。共有 82M。
點(diǎn)擊 value,可以查看 list 中具體的值。如圖:
發(fā)現(xiàn)正是我們代碼里插入的字符串。
CPU 高
使用 top 命令,查看哪個(gè)進(jìn)程 CPU 高,通過 top -p <PID> -H 查看哪個(gè)線程消耗 CPU。使用 jstack 命令打印出 java 進(jìn)程的線程堆棧,通過線程號找到相應(yīng)的 java 線程,結(jié)合 java 代碼,一般可以找出系統(tǒng)的耗 CPU 代碼。
相關(guān)操作可以參考如下文章:
誰偷走了你的服務(wù)器性能
寫在最后
這里只是通過一些栗子說明了性能工具的使用方法,只是一個(gè)引子,隨后會進(jìn)一步介紹如何進(jìn)行性能的優(yōu)化。