Java 應(yīng)用壓測(cè)性能問(wèn)題定位經(jīng)驗(yàn)分享
什么是壓測(cè)
壓測(cè),即壓力測(cè)試,是確立系統(tǒng)穩(wěn)定性的一種測(cè)試方法,通常在系統(tǒng)正常運(yùn)作范圍之外進(jìn)行,以考察其功能極限和和可能存在的隱患。
壓測(cè)主要用于檢測(cè)服務(wù)器的承受能力,包括用戶(hù)承受能力,即多少用戶(hù)同時(shí)使用系統(tǒng)時(shí)基本不影響質(zhì)量、流量承受等。另外,通過(guò)諸如疲勞測(cè)試還能發(fā)現(xiàn)系統(tǒng)一些穩(wěn)定性的問(wèn)題,比如是否存在連接池中的連接被耗盡,內(nèi)存被耗盡,線程池被耗盡,這些只能通過(guò)疲勞測(cè)試來(lái)進(jìn)行發(fā)現(xiàn)定位。
為什么要壓測(cè)
壓測(cè)的目的就是通過(guò)模擬真實(shí)用戶(hù)的行為,測(cè)算出機(jī)器的性能(單臺(tái)機(jī)器的 QPS、TPS),從而推算出系統(tǒng)在承受指定用戶(hù)數(shù)(100 W)時(shí),需要多少機(jī)器能支撐得住。因此在進(jìn)行壓測(cè)時(shí)一定要事先設(shè)定壓測(cè)目標(biāo)值,這個(gè)值不能太小,也不能太大,按照目前業(yè)務(wù)預(yù)估的增長(zhǎng)量來(lái)做一個(gè)合理的評(píng)估。壓測(cè)是在上線前為了應(yīng)對(duì)未來(lái)可能達(dá)到的用戶(hù)數(shù)量的一次預(yù)估(提前演練),壓測(cè)以后通過(guò)優(yōu)化程序的性能或準(zhǔn)備充足的機(jī)器,來(lái)保證用戶(hù)的體驗(yàn)。壓測(cè)還能探測(cè)應(yīng)用系統(tǒng)在出現(xiàn)交易洪峰時(shí)穩(wěn)定性情況,以及可能出現(xiàn)的一些問(wèn)題,發(fā)現(xiàn)應(yīng)用系統(tǒng)薄弱一環(huán),從而更有針對(duì)性地進(jìn)行加強(qiáng)。
壓測(cè)
這幾種測(cè)試可以穿插進(jìn)行,一般會(huì)在壓力測(cè)試性能指標(biāo)達(dá)標(biāo)后,再安排耐久性測(cè)試。
壓測(cè)名詞解釋
常見(jiàn)的壓測(cè)工具
ab
ApacheBench 是 Apache 服務(wù)器自帶的一個(gè) web 壓力測(cè)試工具,簡(jiǎn)稱(chēng) ab。ab 又是一個(gè)命令行工具,對(duì)發(fā)起負(fù)載的本機(jī)要求很低,根據(jù) ab 命令可以創(chuàng)建很多的并發(fā)訪問(wèn)線程,模擬多個(gè)訪問(wèn)者同時(shí)對(duì)某一 URL 地址進(jìn)行訪問(wèn),因此可以用來(lái)測(cè)試目標(biāo)服務(wù)器的負(fù)載壓力??偟膩?lái)說(shuō) ab 工具小巧簡(jiǎn)單,上手學(xué)習(xí)較快,可以提供需要的基本性能指標(biāo),但是沒(méi)有圖形化結(jié)果,不能監(jiān)控。
Jmeter
Apache JMeter 是 Apache 組織開(kāi)發(fā)的基于 Java 的壓力測(cè)試工具。用于對(duì)軟件做壓力測(cè)試,它最初被設(shè)計(jì)用于 Web 應(yīng)用測(cè)試,但后來(lái)擴(kuò)展到其他測(cè)試領(lǐng)域。
JMeter 能夠?qū)?yīng)用程序做功能/回歸測(cè)試,通過(guò)創(chuàng)建帶有斷言的腳本來(lái)驗(yàn)證你的程序返回了你期望的結(jié)果。
JMeter 的功能過(guò)于強(qiáng)大,這里暫時(shí)不介紹用法,可以查詢(xún)相關(guān)文檔使用(參考文獻(xiàn)中有推薦的教程文檔)
LoadRunner
LoadRunner 是 HP(Mercury)公司出品的一個(gè)性能測(cè)試工具,功能非常強(qiáng)大,很多企業(yè)級(jí)客戶(hù)都在使用,具體請(qǐng)參考官網(wǎng)鏈接。
阿里云PTS
性能測(cè)試 PTS(Performance Testing Service)是一款性能測(cè)試工具。支持按需發(fā)起壓測(cè)任務(wù),可提供百萬(wàn)并發(fā)、千萬(wàn) TPS 流量發(fā)起能力,100% 兼容 JMeter。提供的場(chǎng)景編排、API 調(diào)試、流量定制、流量錄制等功能,可快速創(chuàng)建業(yè)務(wù)壓測(cè)腳本,精準(zhǔn)模擬不同量級(jí)用戶(hù)訪問(wèn)業(yè)務(wù)系統(tǒng),幫助業(yè)務(wù)快速提升系統(tǒng)性能和穩(wěn)定性。
作為阿里內(nèi)部使用多年的性能測(cè)試工具,PTS 具備如下特性:
- 免運(yùn)維、開(kāi)箱即用。SaaS化施壓、最大支持百萬(wàn)級(jí)并發(fā)、千萬(wàn)級(jí)TPS流量自助發(fā)起能力。
- 支持多協(xié)議HTTP1.1/HTTP2/JDBC/MQTT/Kafka/RokectMq/Redis/Websocket/RMTP/HLS/TCP/UDP/SpringCloud/Dubbo/Grpc 等主流協(xié)議。
- 支持流量定制。全球施壓地域定制/運(yùn)營(yíng)商流量定制/IPv6 流量定制。
- 穩(wěn)定、安全。阿里自研引擎、多年雙十一場(chǎng)景打磨、支持 VPC 網(wǎng)絡(luò)壓測(cè)。
- 性能壓測(cè)一站式解決方案。** 0 編碼構(gòu)建復(fù)雜壓測(cè)場(chǎng)景,覆蓋壓測(cè)場(chǎng)景構(gòu)建、壓測(cè)模型設(shè)定、發(fā)起壓力、分析定位問(wèn)題、出壓測(cè)報(bào)告完整的壓測(cè)生命周期。
- 100% 兼容開(kāi)源 JMeter。
- 提供安全、無(wú)侵入的生產(chǎn)環(huán)境寫(xiě)壓測(cè)解決方案。
壓測(cè)工具的比較
如何選擇壓測(cè)工具
這個(gè)世界上沒(méi)有最好的工具,只有最適合的工具,工具千千萬(wàn),選擇一款適合你的才是最重要的,在實(shí)際使用中有各種場(chǎng)景,讀者可以結(jié)合壓測(cè)步驟來(lái)確定適合自己的工具:
- 確定性能壓測(cè)目標(biāo):性能壓測(cè)目標(biāo)可能源于項(xiàng)目計(jì)劃、業(yè)務(wù)方需求等
- 確定性能壓測(cè)環(huán)境:為了盡可能發(fā)揮性能壓測(cè)作用,性能壓測(cè)環(huán)境應(yīng)當(dāng)盡可能同線上環(huán)境一致
- 確定性能壓測(cè)通過(guò)標(biāo)準(zhǔn):針對(duì)性能壓測(cè)目標(biāo)以及選取的性能壓測(cè)環(huán)境,制定性能壓測(cè)通過(guò)標(biāo)準(zhǔn),對(duì)于不同于線上環(huán)境的性能壓測(cè)環(huán)境,通過(guò)標(biāo)準(zhǔn)也應(yīng)當(dāng)適度放寬
- 設(shè)計(jì)性能壓測(cè):編排壓測(cè)鏈路,構(gòu)造性能壓測(cè)數(shù)據(jù),盡可能模擬真實(shí)的請(qǐng)求鏈路以及請(qǐng)求負(fù)載
- 執(zhí)行性能壓測(cè):借助性能壓測(cè)工具,按照設(shè)計(jì)執(zhí)行性能壓測(cè)
- 分析性能壓測(cè)結(jié)果報(bào)告:分析解讀性能壓測(cè)結(jié)果報(bào)告,判定性能壓測(cè)是否達(dá)到預(yù)期目標(biāo),若不滿足,要基于性能壓測(cè)結(jié)果報(bào)告分析原因
由上述步驟可知,一次成功的性能壓測(cè)涉及到多個(gè)環(huán)節(jié),從場(chǎng)景設(shè)計(jì)到施壓再到分析,缺一不可。工欲善其事,必先利其器,而一款合適的性能工具意味著我們能夠在盡可能短的時(shí)間內(nèi)完成一次合理的性能壓測(cè),達(dá)到事半功倍的效果。
JAVA 應(yīng)用性能問(wèn)題排查指南
問(wèn)題分類(lèi)
問(wèn)題形形色色,各種各樣的問(wèn)題都會(huì)有。對(duì)其進(jìn)行抽象和分類(lèi)是非常必要的。這里將從兩個(gè)維度來(lái)對(duì)性能問(wèn)題進(jìn)行分類(lèi)。第一個(gè)維度是資源維度,第二個(gè)維度是頻率維度。
資源維度類(lèi)的問(wèn)題:CPU 沖高,內(nèi)存使用不當(dāng),網(wǎng)絡(luò)過(guò)載。
頻率維度類(lèi)的問(wèn)題:交易持續(xù)性緩慢,交易偶發(fā)性緩慢。
對(duì)于每一類(lèi)問(wèn)題都有相應(yīng)的解決辦法,方法或者工具使用不當(dāng),會(huì)導(dǎo)致不能快速而且精準(zhǔn)地排查定位問(wèn)題。
壓測(cè)性能問(wèn)題定位調(diào)優(yōu)是一門(mén)需要多方面綜合能力結(jié)合的一種技術(shù)工作,需要憑借個(gè)人的技術(shù)能力、經(jīng)驗(yàn)、有時(shí)候還需要一些直覺(jué)和靈感,還需要一定的溝通能力,因?yàn)橛袝r(shí)候問(wèn)題并不是由定位問(wèn)題的人發(fā)現(xiàn)的,所以需要通過(guò)不斷地溝通來(lái)發(fā)現(xiàn)一些蛛絲馬跡。涉及的技術(shù)知識(shí)面遠(yuǎn)不僅限于程序語(yǔ)言本身,還可能需要扎實(shí)的技術(shù)基本功,比如操作系統(tǒng)原理、網(wǎng)絡(luò)、編譯原理、JVM 等知識(shí),決不只是簡(jiǎn)單的了解,而是真正的掌握,比如 TCP/IP,必須得深入掌握。JVM 得深入掌握內(nèi)存組成,內(nèi)存模型,深入掌握 GC 的一些算法等。這也是一些初中級(jí)技術(shù)人員在一遇到性能問(wèn)題就傻眼,完全不知道如何從哪里下手。如果擁有扎實(shí)的技術(shù)基本功,再加上一些實(shí)戰(zhàn)經(jīng)驗(yàn)然后形成一套屬于自己的打法,在遇到問(wèn)題后才能心中不亂,快速撥開(kāi)迷霧,最終找到問(wèn)題的癥結(jié)。
本文筆者還帶來(lái)了實(shí)際工作中定位和排查出來(lái)的一些典型的性能問(wèn)題的案例,每個(gè)案例都會(huì)介紹問(wèn)題發(fā)生的相關(guān)背景,一線人員提供的問(wèn)題現(xiàn)象和初步排查定位結(jié)論,且在筆者介入后看到的問(wèn)題現(xiàn)象,再配合一些常用的問(wèn)題定位工具,介紹發(fā)現(xiàn)和定位問(wèn)題的整個(gè)過(guò)程,問(wèn)題發(fā)生的根本原因等。
分析思路框架
遇到一個(gè)性能問(wèn)題,首先要從各種表象和一些簡(jiǎn)單工具將問(wèn)題進(jìn)行定義和分類(lèi),然后再做進(jìn)一步的定位分析,可以參考一下圖 1 作者總結(jié)出來(lái)的一個(gè)決策圖,這張圖是筆者從近幾個(gè)金融行業(yè) ToB 項(xiàng)目中做性能定位調(diào)優(yōu)過(guò)程的一個(gè)總結(jié)提練,不一定適合所有的問(wèn)題,但至少覆蓋到了近幾個(gè)項(xiàng)目中遇到的性能問(wèn)題的排查過(guò)程。在接下來(lái)的大篇幅中將對(duì)每一類(lèi)問(wèn)題進(jìn)行展開(kāi),并附上一些真實(shí)的經(jīng)典案例,這些案例都是真真實(shí)實(shí)發(fā)生的,有一定的代表性,且很多都是客戶(hù)定位了很長(zhǎng)時(shí)間都沒(méi)發(fā)現(xiàn)問(wèn)題根本原因的問(wèn)題。其中 GC 類(lèi)問(wèn)題在此文不做過(guò)多分析,對(duì)于 GC 這一類(lèi)問(wèn)題后續(xù)有空寫(xiě)一篇專(zhuān)門(mén)的文章來(lái)進(jìn)行展開(kāi)。
內(nèi)存溢出
內(nèi)存溢出問(wèn)題按照問(wèn)題發(fā)生頻率又可進(jìn)一步分為堆內(nèi)存溢出、棧內(nèi)存溢出、Metaspace 內(nèi)存溢出以及 Native 內(nèi)存溢出,下面對(duì)每種溢出情況進(jìn)行詳細(xì)分析。
- 堆內(nèi)存溢出
相信這類(lèi)問(wèn)題大家多多少少都接觸過(guò),問(wèn)題發(fā)生的根本原因就是應(yīng)用申請(qǐng)的堆內(nèi)存超過(guò)了 Xmx 參數(shù)設(shè)置的值,進(jìn)而導(dǎo)致 JVM 基本處于一個(gè)不可用的狀態(tài)。如圖 2 所示,示例代碼模擬了堆內(nèi)存溢出,運(yùn)行時(shí)設(shè)置堆大小為 1MB,運(yùn)行后結(jié)果如圖3所示,拋出了一個(gè) OutOfMemoryError 的錯(cuò)誤異常,相應(yīng)的 Message 是 Java heap space,代表溢出的部分是堆內(nèi)存。
- 棧內(nèi)存溢出
這類(lèi)問(wèn)題主要是由于方法調(diào)用深度太深,或者不正確的遞歸方法調(diào)用,又或者是 Xss 參數(shù)設(shè)置不當(dāng)都會(huì)引發(fā)這個(gè)問(wèn)題,如圖 4 所示,一個(gè)簡(jiǎn)單的無(wú)限遞歸調(diào)用就會(huì)引發(fā)棧內(nèi)存溢出,出錯(cuò)結(jié)果如圖5所示,將會(huì)拋一個(gè) StackOverflowError 的錯(cuò)誤異常。Xss 參數(shù)可以設(shè)置每個(gè)線程棧內(nèi)存最大大小,JDK8 的默認(rèn)大小為 1MB,正常情況下一般不需要去修改該參數(shù),如果遇到 StackOverflowError 的報(bào)錯(cuò),那么就需要留意了,需要查證是程序的問(wèn)題還是參數(shù)設(shè)置的問(wèn)題,如果確實(shí)是方法調(diào)用深度很深,默認(rèn)的 1MB 不夠用,那么就需要調(diào)高 Xss 參數(shù)。
- Native內(nèi)存溢出
這種溢出發(fā)生在 JVM 使用堆外內(nèi)存時(shí),且超過(guò)一個(gè)進(jìn)程所支持的最大的內(nèi)存上限,或者堆外內(nèi)存超過(guò) MaxDirectMemorySize 參數(shù)指定的值時(shí)即會(huì)引發(fā) Native 內(nèi)存溢出。如圖 6 所示,需要配置 MaxDirectMemorySize 參數(shù),如果不配置這個(gè)參數(shù)估計(jì)很難模擬出這個(gè)問(wèn)題,作者的機(jī)器的 64 位的機(jī)器,堆外內(nèi)存的大小可想而知了。運(yùn)行該程序得到的運(yùn)行結(jié)果如圖 7 所示,拋出來(lái)的異常也是 OutOfMemoryError,這個(gè)跟堆內(nèi)存異常類(lèi)似,但是 Message 是 Direct buffer memory,這個(gè)跟堆內(nèi)存溢出的 Message 是不一樣的,請(qǐng)?zhí)貏e留意這條 Message,這對(duì)精準(zhǔn)定位問(wèn)題是非常重要的。
- Metaspace內(nèi)存溢出
Metaspace 是在 JDK8 中才出現(xiàn)的,之前的版本中都叫 Perm 空間,大概用途都相差不大。模擬 Metaspace 溢出的方式很簡(jiǎn)單,如圖 8 所示通過(guò) cglib 不斷動(dòng)態(tài)創(chuàng)建類(lèi)并加載到 JVM,這些類(lèi)信息就是保存在 Metaspace 內(nèi)存里面的,在這里為了快速模擬出問(wèn)題,將 MaxMetaspaceSize 設(shè)置為 10MB。執(zhí)行結(jié)果如圖 9 所示,依然是拋出 OutOfMemoryError 的錯(cuò)誤異常,但是 Message 變成了 Metaspace。
JVM 的內(nèi)存溢出最常見(jiàn)的就這四種,如果能知道每一種內(nèi)存溢出出現(xiàn)的原因,那么就能快速而精準(zhǔn)地進(jìn)行定位。下面對(duì)一些遇到的真實(shí)的經(jīng)典案例進(jìn)行分析。
- 案例:堆外內(nèi)存溢出
這種問(wèn)題也比較好查,前提是在堆內(nèi)存發(fā)生溢出時(shí)必須自動(dòng)轉(zhuǎn)儲(chǔ)堆內(nèi)存到文件中,如果壓測(cè)過(guò)程中通過(guò) kill -3 或者 jmap 命令觸發(fā)堆內(nèi)存轉(zhuǎn)儲(chǔ)。然后通過(guò)一些堆內(nèi)存分析工具比如 IBM 的 Heap Analyzer 等工具找出是哪種對(duì)象占用內(nèi)存最多,最終可以把問(wèn)題原因揪出來(lái)。
如果需要在發(fā)生 OOM 時(shí)自動(dòng)轉(zhuǎn)儲(chǔ)堆內(nèi)存,那么需要在啟動(dòng)參數(shù)中加入如下參數(shù):
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/local/oom
如果需要手工獲取線程轉(zhuǎn)儲(chǔ)或者內(nèi)存轉(zhuǎn)儲(chǔ),那么請(qǐng)使用 kill -3 命令,或者使用 jstack 和 jmap 命令。
jstack -l pid > stackinfo,這條命令可以把線程信息轉(zhuǎn)儲(chǔ)到文本文件,把文件下載到本地然后用諸如 IBM Core file analyze 工具進(jìn)行分析。
jmap -dump:format=b,file=./jmap.hprof pid,這條命令可以把堆內(nèi)存信息到當(dāng)前目錄的 jmap.hprof 文件中,下載到本地,然后用諸如 IBM Heap Analyze 等堆內(nèi)存分析工具進(jìn)行分析,根據(jù)二八定律,找準(zhǔn)最耗內(nèi)存的對(duì)象就可以解決 80% 的問(wèn)題。
圖 10 就是一個(gè)真實(shí)發(fā)生的案例,該問(wèn)題的發(fā)生現(xiàn)象是這樣的,壓測(cè)開(kāi)始后,前十分鐘一切正常,但是在經(jīng)歷大約十分鐘后,TPS 逐漸下降,直到后面客戶(hù)端的 TCP 連接都建不上去,客戶(hù)一度認(rèn)為是服務(wù)端Linux的網(wǎng)絡(luò)棧的參數(shù)設(shè)置有問(wèn)題,導(dǎo)致 TCP 無(wú)法建連,給出的證據(jù)是,服務(wù)端存在大量的 TIME_WAIT 狀態(tài)的連接,然后要求調(diào)整Linux內(nèi)核網(wǎng)絡(luò)參數(shù),減少 TIME_WAIT 狀態(tài)的連接數(shù)。什么是 TIME_WAIT?在這個(gè)時(shí)候就不得不祭出祖?zhèn)?TCP 狀態(tài)機(jī)的那張圖了,如圖 11 所示。對(duì)照這個(gè)圖就能知道 TIME_WAIT 的來(lái)朧去脈了,TIME_WAIT 主要出現(xiàn)在主動(dòng)關(guān)閉連接方,當(dāng)然了,如果雙方剛好同時(shí)關(guān)閉連接的時(shí)候,那么雙方都會(huì)出現(xiàn) TIME_WAIT 狀態(tài)。在進(jìn)行關(guān)閉連接四路握手協(xié)議時(shí),最后的 ACK 是由主動(dòng)關(guān)閉端發(fā)出的,如果這個(gè)最終的 ACK 丟失,服務(wù)器將重發(fā)最終的 FIN,因此客戶(hù)端必須維護(hù)狀態(tài)信息以允許它重發(fā)最終的 ACK。如果不維持這個(gè)狀態(tài)信息,那么客戶(hù)端將響應(yīng) RST 分節(jié),服務(wù)器將此分節(jié)解釋成一個(gè)錯(cuò)誤(在 java 中會(huì)拋出 connection reset的SocketException)。因而,要實(shí)現(xiàn) TCP 全雙工連接的正常終止,必須處理終止序列四個(gè)分節(jié)中任何一個(gè)分節(jié)的丟失情況,主動(dòng)關(guān)閉的客戶(hù)端必須維持狀態(tài)信息進(jìn)入 TIME_WAIT 狀態(tài)。
圖 10 真實(shí)堆內(nèi)存溢出案例一
圖 11 TCP 狀態(tài)機(jī)
順著客戶(hù)提供的這些信息,查了一下壓測(cè)客戶(hù)端,采用的是 HTTP 協(xié)議,keep-alive 為開(kāi),而且采用的是連接池的方式與服務(wù)端進(jìn)行交互,理論上在服務(wù)器端不應(yīng)該出現(xiàn)如此之多的 TIME_WAIT 連接,猜測(cè)一種可能性是由于客戶(hù)側(cè)剛開(kāi)始?jí)簻y(cè)的時(shí)候 TPS 比較高,占用連接數(shù)多,后續(xù)性能下來(lái)后,連接數(shù)空閑且來(lái)不及跟服務(wù)端進(jìn)行?;钐幚恚瑢?dǎo)致連接被服務(wù)端給主動(dòng)關(guān)閉掉了,但這也僅限于是猜測(cè)了。
為了更精準(zhǔn)地定位問(wèn)題,決定去一線現(xiàn)場(chǎng)看下情況,在 TPS 嚴(yán)重往下掉的時(shí)候,通過(guò) top、vmstat 等命令進(jìn)行初步探測(cè),發(fā)現(xiàn) cpu 占比并不十分高,大約 70% 左右。但是 JVM 占用的內(nèi)存已經(jīng)快接近 Xmx 參數(shù)配置的值了,然后用 jstat -gcutil -h10 pid 5s 100 命令看一下 GC 情況,不查不知道一查嚇一跳,如圖 12 所示,初看這就是一份不太正常的 GC 數(shù)據(jù),首先老年代占比直逼 100%,然后 5 秒內(nèi)居然進(jìn)行了 7 次 FullGC,eden 區(qū)占比 100%,因?yàn)槔夏甏呀?jīng)滿了,年輕代的 GC 都已經(jīng)停滯了,這明顯不正常,趁 JVM 還活著,趕緊執(zhí)行 jmap -dump:format=b,file=./jmap.hprof pid,把整個(gè)堆文件快照拿下來(lái),整整 5 個(gè) G。取下來(lái)后通過(guò) IBM 的 HeapAnalyzer 工具分析堆文件,結(jié)果如圖 10 所示,經(jīng)過(guò)一番查找,發(fā)現(xiàn)某個(gè)對(duì)象占比特別大,占比達(dá) 98%,繼續(xù)追蹤持有對(duì)象,最終定位出問(wèn)題,申請(qǐng)了某個(gè)資源,但是一直沒(méi)有釋放,修改后問(wèn)題得到完美解決,后續(xù)再經(jīng)過(guò)長(zhǎng)達(dá) 8 個(gè)小時(shí)的耐久性測(cè),沒(méi)能再發(fā)現(xiàn)問(wèn)題,TPS 一直非常穩(wěn)定。
圖 12 GC 情況統(tǒng)計(jì)分析
再來(lái)看看為何會(huì)出現(xiàn)那么多的 TIME_WAIT 連接,跟開(kāi)始的猜測(cè)是一致的,由于大量的閑置連接被服務(wù)端主動(dòng)關(guān)閉掉,所以才會(huì)出現(xiàn)那么多的 TIME_WAIT 狀態(tài)的連接。
CPU高
- 案例
某金融銀行客戶(hù)在壓測(cè)過(guò)程中發(fā)現(xiàn)一個(gè)問(wèn)題,導(dǎo)致 TPS 極低,交易響應(yīng)時(shí)長(zhǎng)甚至接近驚人的 30S,嚴(yán)重不達(dá)票,服務(wù)響應(yīng)時(shí)間如圖 23 所示,這是應(yīng)用打的 tracer log,顯示的耗時(shí)很不樂(lè)觀。應(yīng)用采用 SOFA 構(gòu)建,部署在專(zhuān)有云容器上面,容器規(guī)格為 4C8G,使用 OceanBase 數(shù)據(jù)庫(kù)。交易緩慢過(guò)程中客戶(hù)在相應(yīng)容器里面用 top、vmstat 命令獲取 OS 信息,發(fā)現(xiàn)內(nèi)存使用正常,但是 CPU 接近 100%,通過(guò) jstack 命令取線程轉(zhuǎn)儲(chǔ)文件,如圖 22 所示,客戶(hù)發(fā)現(xiàn)大量的線程都卡在了獲取數(shù)據(jù)庫(kù)連接上面,再上應(yīng)用日志中也報(bào)了大量的獲取 DB 連接失敗的錯(cuò)誤日志,這讓客戶(hù)以為是連接池中的連接數(shù)不夠,所以不斷繼續(xù)加大 MaxActive 這個(gè)參數(shù),DB 連接池使用的是 Druid,在加大參數(shù)后,性能沒(méi)有任何改善,且獲取不到連接的問(wèn)題依舊。客戶(hù)在排查該問(wèn)題大概兩周且沒(méi)有任何實(shí)質(zhì)性進(jìn)展后,開(kāi)始向阿里 GTS 的同學(xué)求助。
筆者剛好在客戶(hù)現(xiàn)場(chǎng),介入該性能問(wèn)題的定位工作。跟客戶(hù)一番溝通,并查閱了了歷史定位信息記錄后,根據(jù)以往的經(jīng)驗(yàn),這個(gè)問(wèn)題肯定不是由于連接池中的最大連接數(shù)不夠的原因?qū)е碌?,因?yàn)檫@個(gè)時(shí)候客戶(hù)已經(jīng)把 MaxActive 的參數(shù)已經(jīng)調(diào)到了恐怖的 500,但問(wèn)題依舊,在圖 22 中還能看到一些有用的信息,比如正在 Waiting 的線程高達(dá) 908 個(gè),Runnable 的線程高達(dá) 295 個(gè),都是很恐怖的數(shù)字,大量的線程處于 Runnable 狀態(tài),CPU 忙著進(jìn)行線程上下文的切換,CPU 呼呼地轉(zhuǎn),但實(shí)際并沒(méi)有干多少有實(shí)際有意義的事。后經(jīng)詢(xún)問(wèn),客戶(hù)將 SOFA 的業(yè)務(wù)處理線程數(shù)調(diào)到了 1000,默認(rèn)是 200。
圖 22 線程卡在獲取 DB 連接池中的連接
圖 23 交易緩慢截圖
查到這里基本可以斷定客戶(hù)陷入了“頭痛醫(yī)頭,腳痛醫(yī)腳”,“治標(biāo)不治本”的窘境,進(jìn)一步跟客戶(hù)溝通后,果然如此。剛開(kāi)始的時(shí)候,是由于 SOFA 報(bào)了線程池滿的錯(cuò)誤,然后客戶(hù)不斷加碼 SOFA 業(yè)務(wù)線程池中最大線程數(shù),最后加到了 1000,性能提升不明顯,然后報(bào)了一個(gè)獲取不到數(shù)據(jù)庫(kù)連接的錯(cuò)誤,客戶(hù)又認(rèn)為這是數(shù)據(jù)庫(kù)連接不夠了,調(diào)高 Druid 的 MaxActive 參數(shù),最后無(wú)論怎么調(diào)性能也都上不來(lái),甚至到后面把內(nèi)存都快要壓爆了,如圖 24 所示,內(nèi)存中被一些業(yè)務(wù) DO 對(duì)象給填滿了,后面客戶(hù)一度以為存在內(nèi)存泄露。對(duì)于這類(lèi)問(wèn)題,只要像是出現(xiàn)了數(shù)據(jù)庫(kù)連接池不夠用、或者從連接池中獲取連接超時(shí),又或者是線程池耗盡這類(lèi)問(wèn)題,只要參數(shù)設(shè)置是在合理的范圍,那么十有八九就是交易本身處理太慢了。后面經(jīng)過(guò)進(jìn)一步的排查最終定位是某個(gè) SQL 語(yǔ)句和內(nèi)部的一些處理不當(dāng)導(dǎo)致的交易緩慢。修正后,TPS 正常,最后把線程池最大大小參數(shù)、DB 連接池的參數(shù)都往回調(diào)成最佳實(shí)踐中推薦的值,再次壓測(cè)后,TPS 依然保持正常水平,問(wèn)題得到最終解決。
圖 24 內(nèi)存填滿了業(yè)務(wù)領(lǐng)域?qū)ο?/p>
這個(gè)案例一雖說(shuō)是因?yàn)?CPU 沖高且交易持續(xù)緩慢的這一類(lèi)典型問(wèn)題,但其實(shí)就這個(gè)案例所述的那樣,在定位和調(diào)優(yōu)的時(shí)候很容易陷進(jìn)一種治標(biāo)不治本的困境,很容易被一些表象所迷惑。如何撥開(kāi)云霧見(jiàn)月明,筆者的看法是 5 分看經(jīng)驗(yàn),1 分看靈感和運(yùn)氣,還有 4 分得靠不斷分析。如果沒(méi)經(jīng)驗(yàn)怎么辦?那就只能沉下心來(lái)分析相關(guān)性能文件,無(wú)論是線程轉(zhuǎn)儲(chǔ)文件還是 JFR,又或者其他采集工具采集到性能信息,反正不要放過(guò)任何蛛絲馬跡,最后實(shí)在沒(méi)轍了再請(qǐng)求經(jīng)驗(yàn)豐富的專(zhuān)家的協(xié)助排查解決。
- 使用 JMC+JFR 定位問(wèn)題
如果超長(zhǎng)問(wèn)題偶然發(fā)生,這里介紹一個(gè)比較簡(jiǎn)單且非常實(shí)用的方法,使用 JMC+JFR,可以參考鏈接進(jìn)行使用。但是使用前必須開(kāi)啟 JMX 和 JFR 特性,需要在啟動(dòng)修改啟動(dòng)參數(shù),具體參數(shù)如下,該參數(shù)不要帶入生產(chǎn),另外如果將容器所屬宿主機(jī)的端口也暴露成跟 jmxremote.port 一樣的端口,如下示例為 32433,那么還可以使用 JConsole 或者 JVisualvm 工具實(shí)時(shí)觀察虛擬機(jī)的狀況,這里不再做詳細(xì)介紹。
-Dcom.sun.management.jmxremote.port=32433
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.
authenticate=false
-XX:+UnlockCommercialFeatures -XX:+FlightRecorder
下面以一個(gè)實(shí)際的 JFR 實(shí)例為例。
首先要開(kāi)啟 JMX 和 JFR 功能,需要在啟動(dòng)參數(shù)中加 JMX 開(kāi)啟參數(shù)和 JFR 開(kāi)啟參數(shù),如上面所述,然后在容器里面執(zhí)行下述命令,執(zhí)行后顯示“Started recording pid. The result will be written to xxxx”,即表示已經(jīng)開(kāi)始錄制,這個(gè)時(shí)候開(kāi)始進(jìn)行壓測(cè),下述命令中的 duration 是 90 秒,也就表示會(huì)錄制 90S 后才會(huì)停止錄制,錄制完后將文件下載到本地,用 jmc 工具進(jìn)行分析,如果沒(méi)有這個(gè)工具,也可以使用 IDEA 進(jìn)行分析。
jcmd pid JFR.start name=test duratinotallow=90s filename=output.jfr
通過(guò)分析火焰圖,具體怎么看火焰圖請(qǐng)參考鏈接。通過(guò)這個(gè)圖可以看到主要的耗時(shí)是在哪個(gè)方法上面,給我們分析問(wèn)題提供了很大的便利。
還可以查看 call tree,也能看出耗時(shí)主要發(fā)生在哪里。
JMC 工具下載地址:JDK Mission Control (JMC) 8 Downloads (oracle.com)
最后再介紹一款工具,阿里巴巴開(kāi)源的 arthas,也是性能分析和定位的一把利器,具體使用就不在這里介紹了,可以參考 arthas 官網(wǎng)。
- 如何定位 CPU 耗時(shí)過(guò)高的線程及方法
首先找到 JAVA 進(jìn)程的 PID,然后執(zhí)行 top -H -p pid,這樣可以找到最耗時(shí)的線程,如下圖所示。然后使用 printf "%x\n" 17880,將線程號(hào)轉(zhuǎn)成 16 進(jìn)制,最終通過(guò)這個(gè) 16 進(jìn)制值去 jstack 線程轉(zhuǎn)儲(chǔ)文件中去查找是哪個(gè)線程占用 CPU 最高。
其他問(wèn)題案例
這類(lèi)問(wèn)題在發(fā)生的時(shí)候,JVM 表現(xiàn)得靜如止水,CPU 和內(nèi)存的使用都在正常水位,但是交易就是緩慢,對(duì)于這一類(lèi)問(wèn)題可以參考 CPU 沖高類(lèi)問(wèn)題來(lái)進(jìn)行解決,通過(guò)使用線程轉(zhuǎn)儲(chǔ)文件或者使用JFR來(lái)錄制一段 JVM 運(yùn)行記錄。這類(lèi)問(wèn)題大概率的原因是由于大部分線程卡在某個(gè) IO 或者被某個(gè)鎖個(gè) Block 住了,下面也帶來(lái)一個(gè)真實(shí)的案例。
- 案例一
某金融保險(xiǎn)頭部客戶(hù),反應(yīng)某個(gè)交易非常緩慢,經(jīng)常響應(yīng)時(shí)間在 10S 以上,應(yīng)用部署在公有云的容器上,容器規(guī)格為 2C4G,數(shù)據(jù)庫(kù)是 OceanBase。問(wèn)題每次都能重現(xiàn),通過(guò)分布式鏈路工具只能定位到在某個(gè)服務(wù)上面慢,并不能精確定是卡在哪個(gè)方法上面。在交易緩慢期間,通過(guò) top、vmstat 命令查看 OS 的狀態(tài),CPU 和內(nèi)存資源都在正常水位。因此,需要看在交易期間的線程的狀態(tài)。在交易執(zhí)行緩慢期間,將交易的線程給轉(zhuǎn)儲(chǔ)出來(lái),如圖 29 所示,可以定位相應(yīng)的線程卡在哪個(gè)方法上面,案例中的線程卡在了執(zhí)行 socket 讀數(shù)據(jù)階段,從堆??梢詳喽ㄊ强ㄔ诹俗x數(shù)據(jù)庫(kù)上面了。如果這個(gè)方法依然不好用,那么還可以借助抓包方式來(lái)進(jìn)行定位。
圖 29 交易被 hang 住示例圖
- 案例二
某金融銀行客戶(hù)壓測(cè)過(guò)程中發(fā)現(xiàn) TPS 上不去,10TPS 不到,響應(yīng)時(shí)間更是高到令人發(fā)指,在經(jīng)過(guò)一段時(shí)間的培訓(xùn)賦能和磨合,該客戶(hù)已經(jīng)具備些性能定位的能力。給反饋的信息是 SQL 執(zhí)行時(shí)間、CPU 和內(nèi)存使用一切正常,客戶(hù)打了一份線程轉(zhuǎn)儲(chǔ)文件,發(fā)現(xiàn)大多數(shù)線程都卡在了使用 RedissionLock 的分布式鎖上面,如圖 30 所示,后經(jīng)查是客戶(hù)沒(méi)有合理使用分布式鎖導(dǎo)致的問(wèn)題,解決后,TPS 翻了 20 倍。
圖 30 分布式鎖使用不當(dāng)導(dǎo)致的問(wèn)題示例
這兩個(gè)案例其實(shí)都不算復(fù)雜,也很容易進(jìn)行排查,放到這里只是想重述一下排查這類(lèi)問(wèn)題的一個(gè)整體的思路和方法。如果交易緩慢且資源使用都正常,可以通過(guò)分析線程轉(zhuǎn)儲(chǔ)文件或者 JFR 文件來(lái)定位問(wèn)題,這類(lèi)問(wèn)題一般是由于 IO 存在瓶頸,又或者被鎖 Block 住的原因?qū)е碌摹?/span>
總結(jié)
問(wèn)題千千萬(wàn),但只要修練了足夠深厚的內(nèi)功,形成一套屬于自己的排查問(wèn)題思路和打法,再加上一套支撐問(wèn)題排查的工具,憑借已有的經(jīng)驗(yàn)還有偶發(fā)到來(lái)的那一絲絲靈感,相信所有的問(wèn)題都會(huì)迎刃而解。