1,、性能調(diào)優(yōu)的步驟
1.1,、衡量系統(tǒng)現(xiàn)狀
包括請求次數(shù)、響應(yīng)時間,、資源消耗等;如:A系統(tǒng)目前95%的請求響應(yīng)為1s。
1.2,、設(shè)定調(diào)優(yōu)目標
根據(jù)用戶所能接受的響應(yīng)速度,、系統(tǒng)現(xiàn)有的機器、所支撐的用戶量制定出來的,,因此通常會設(shè)定調(diào)優(yōu)目標:95%的 請求在500ms內(nèi)返回,。
1.3、尋找性能瓶頸
在【2,、尋找性能瓶頸】會專門介紹,。通常性能瓶頸的表像是:
1.3.1、資源消耗過多(CPU,、文件IO,、網(wǎng)絡(luò)IO、內(nèi)存)
1.3.2,、外部系統(tǒng)處理不足(所調(diào)用的其他系統(tǒng)提供的功能——多數(shù)情況也是資源消耗過多,、數(shù)據(jù)的操作響應(yīng)速度 不夠——根據(jù)數(shù)據(jù)庫SQL執(zhí)行速度、數(shù)據(jù)庫機器的IOPS,、數(shù)據(jù)庫的Active Sessions等分析出來的)
1.3.3,、程序代碼運行效率不夠高,未充分使用資源或程序結(jié)構(gòu)不合理,。
1.4,、性能調(diào)優(yōu)
在后面的【3,、性能調(diào)優(yōu)】會專門介紹
1.5、衡量是否達到目標值
優(yōu)化部署后,,達到目標則結(jié)束,,如果沒有則重復(fù)1.3、1.4步驟
2,、尋找性能瓶頸
2.1,、CPU消耗分析(top、pidstat等方式查看cpu消耗狀況;vmstat查看cpu的上下文切換,、運行隊列,、利用率)
在Linux中,CPU消耗主要用于中斷,、內(nèi)核以及用戶進程的任務(wù)處理,。優(yōu)先級為:中斷>內(nèi)核>用戶進程。cpu消耗嚴重時,,主要體現(xiàn)在:
①us—用戶進程所占的%
過高的原因:1,、線程一直處于可運行(Runnable)狀態(tài),通常線程在執(zhí)行無阻塞,、循環(huán),、正則或純粹的計算等動作引起的。2,、頻繁的GC操作引起,。如:每次請求需要分配較多內(nèi)存,當訪問量高的時,,就不斷的進行GC,,系統(tǒng)響應(yīng)速度下降。進而造成堆積的請求更多,,消耗內(nèi)存更嚴重,,最嚴重的時候可能導(dǎo)致系統(tǒng) 不斷的進行FULL GC??赏ㄟ^JVM內(nèi)存的消耗分析來查找原因,。
可通過kill -3 [javapid]、jstack [pid] | grep 'nid=0X....' 的方式dump出應(yīng)用的java線程信息,。通過轉(zhuǎn)換出的十六進制的值就可以找到對應(yīng)的nid值的線程,。該線程即為消耗CPU的線程。在采樣時多執(zhí)行幾次上訴過程,,以確保找到真實的消耗CPU的線程,。也可以通過intel vtune 這樣的商業(yè)軟件進行分析
②sy—內(nèi)核線程所占的%
過高的原因:Linux花費更多的時間在進行線程切換,。Java應(yīng)用造成這個原因是:因為啟動了的線程比較多,,且這些線程多數(shù)都處于不斷的阻塞(鎖等待,、IO等待狀態(tài))和執(zhí)行狀態(tài)的變化過程,導(dǎo)致了操作系統(tǒng)需要不斷的切換執(zhí)行的線程,。從而產(chǎn)生大量的上下文切換,。
可通過kill -3 [javapid]、jstack -1 [javapid] 的方式dump出Java應(yīng)用線程信息,,查看線程的狀態(tài),、鎖信息找出等待狀態(tài)或鎖競爭過多的線程。結(jié)合vvmstat 查看CPU消耗狀況,。如cs(上下文切換),、sy等。
③ni—被nice命令改變優(yōu)先級的任務(wù)所占的%
④id—CPU空閑時間所占的%
⑤wa—執(zhí)行過程中等待io所占的%
⑥hi—硬件中斷所占的%
⑦si—軟件中斷所占的%
2.2,、文件IO消耗分析(通過pidstat,、iostat命令分析)
Java應(yīng)用造成io消耗嚴重主要是:
① 多個線程需要大量內(nèi)容寫入(如頻繁的log寫入)動作;
② 磁盤設(shè)備本身的處理速度慢
③ 文件系統(tǒng)慢
④ 操作的文件本身已經(jīng)很大
2.3、網(wǎng)絡(luò)IO消耗分析(通過sar命令,,如需跟著TCP/IP通信過程的信息,,則可通過tcpdump來進行)
對于分布式Java應(yīng)用而言,網(wǎng)絡(luò)IO的消耗非常值得關(guān)注,,尤其要注意網(wǎng)絡(luò)中斷是不是均衡地分配到各CPU的(通過cat/proc/interrupts命令查看),。對于網(wǎng)卡只分配到一個CPU的現(xiàn)象采用修改kernle方法(Google使用)、 采用支持MSI-X的網(wǎng)卡進行修復(fù),。
由于沒辦法分析具體每個線程所消耗的網(wǎng)絡(luò)IO,,因此當網(wǎng)絡(luò)IO消耗高時,對于Java應(yīng)用而言只能對線程進dump,。
查找產(chǎn)生大量網(wǎng)絡(luò)IO操作的線程,,這些線程的特征是讀取或?qū)懭刖W(wǎng)絡(luò)流,在Java網(wǎng)絡(luò)通信時,,通常要對對象進行序列
化為字節(jié)流,,進行發(fā)送,或者讀取,。并反序列化為對象,。這個過程要消耗JVM堆內(nèi)存,JVM對內(nèi)存通常是有限的,。因此,,Java應(yīng)用一般不會造成網(wǎng)絡(luò)IO消耗嚴重。
2.4,、內(nèi)存消耗分析(vmstat,、sar、pidstat、top)
目前Java應(yīng)用只有在創(chuàng)建線程和使用Direct ByteBuffer時才會操作JVM堆意外的內(nèi)存,。對于JVM堆以外的內(nèi)存方面消耗,,最為值得關(guān)注的是swpd的消耗以及物理內(nèi)存的消耗(可通過vmstat、sar,、top,、pidstat等方式查看swap和物理內(nèi)存的消耗狀況)。
2.5,、消耗資源不多,,在訪問量不大的情況。但程序執(zhí)行慢的原因,,主要有3方面
① 鎖競爭激烈(如:數(shù)據(jù)庫連接池數(shù),,但是請求數(shù)多于連接池數(shù))
② 未充分使用硬件資源(如:多核CPU,但程序卻采用單線程串行操作,。)
③ 數(shù)據(jù)量增長(如:數(shù)據(jù)量的海量增長,。可對數(shù)據(jù)庫的表拆分,、庫拆分)
3,、性能調(diào)優(yōu)
3.1JVM調(diào)優(yōu)
3.1.1、代大小調(diào)優(yōu)
① 避免新生代大小設(shè)置過小
1,、避免頻繁進行minor GC;2,、可能導(dǎo)致minor GC對象直接進入舊生代,占據(jù)舊生代空間,,觸發(fā)FULL GC,。
② 避免新生代設(shè)置過大
1、導(dǎo)致舊生代變小,,可能導(dǎo)致FULL GC頻繁執(zhí)行;2,、導(dǎo)致minor GC的耗時大幅度增加。
③ 避免survivor space過小或者過大
④ 根據(jù)具體代碼合理設(shè)置新生代的存活周期,。
3.2.1,、GC策略調(diào)優(yōu)
串行GC性能太差,因此實際應(yīng)用時主要是應(yīng)用并行和并發(fā)GC,,大部分Web應(yīng)用在處理請求時設(shè)置了一個最大可同時處理的請求數(shù),,當超出此請求數(shù)時,會將之后的請求放 入等待隊列中,,而這個等待隊列也限制了大小,。當?shù)却犃袧M了后,仍然有請求進入,,那么這些請求將丟棄,,所有的請求又都是有超時限制度,。
在這種情況下如果觸發(fā)了FULL GC造成應(yīng)用暫停時間較長的FULL GC,則有可能等這次FULL GC之后,,應(yīng)用中很多請求就超時或者被丟棄了,。
從上面可以看出,Web應(yīng)用非常需要一個對應(yīng)用造成暫停時間較短的GC,,再加上大部分Web應(yīng)用的瓶頸都不在CPU上。因此對于Web應(yīng)用而言,,在G1還不夠成熟的情況下,,CMS GC是不錯的選擇。
3.2,、程序調(diào)優(yōu)
3.2.1,、CPU us高的解決方法
① 執(zhí)行線程無任何掛起動作,且一直運行,,導(dǎo)致CPU沒有機會去調(diào)度執(zhí)行其他的線程,,造成線程餓死的現(xiàn)象。
解決:對這種線程的動作增加Thread.sleep(int),,以釋放CPU的執(zhí)行權(quán),,降低CPU的消耗。
原理:以損失單次執(zhí)行性能為代價,,但由于降低了CPU消耗,,在多線程的情況下,反而提高了平均性能,。
② 狀態(tài)掃描,。如:某線程要等其他線程改變了值才可以繼續(xù)執(zhí)行。
解決:改為采用wait/notify機制,。
③ 循環(huán)次數(shù)過多,、正則、計算等造成CPU us過高的情況,。結(jié)合業(yè)務(wù)需求進行調(diào)優(yōu),。code review是王道。
④ 頻繁GC造成us高的情況,,通過JVM調(diào)優(yōu)或程序調(diào)優(yōu),,降低GC的執(zhí)行次數(shù)。
3.2.2,、CPU sy高的解決方法
① 線程運行狀態(tài)經(jīng)常切換
解決:減少線程數(shù),,且使用線程池
② 線程之間鎖競爭激烈
解決:盡可能降低鎖的競爭。
1,、使用并發(fā)包中的類(java.util.concurrent.*)
2,、使用Treiber算法
3,、使用Michael-Scott非阻塞隊列算法(ConcurrentLinkedQueue就是典型的該算法的非阻塞隊列)
4、通常沒必要對整個方法加鎖,,只對需要控制資源的地方做加鎖操作,。
5、拆分鎖,,把獨占鎖拆分為多把鎖,,如:ConcurrentHashMap。很大程度上可以提高讀寫速度,。
6,、去除讀寫操作的互斥鎖
③ 較多網(wǎng)絡(luò)IO操作或者確實需要一些鎖競爭機制(如數(shù)據(jù)庫連接池),但為了能夠支持高的并發(fā)量,,在Java應(yīng)用中又只能借助更多的線程來支撐,。
解決:采用協(xié)程(Coroutine)來支持更高的并發(fā)量,避免并發(fā)量上漲之后造成CPU sy消耗嚴重,、系統(tǒng)load迅速上漲和系統(tǒng)性能下降,。
Java中目前主要可用于實現(xiàn)協(xié)程的框架為Kilim,早使用Kilim執(zhí)行一項任務(wù),,并不創(chuàng)建Thread,,而是采用Task。
3.3,、文件IO消耗嚴重的解決方法
從程序角度看,,造成文件IO消耗嚴重的原因主要是多個線程在寫大量的數(shù)據(jù)到同一文件。導(dǎo)致文件很快變大,。
從而寫入速度越來越慢,,并造成各線程激烈爭搶文件鎖,對于這種情況解決方法:
1,、異步寫入文件;2,、批量讀寫;3、限流;4,、限制文件大小;5,、盡可能采用緩沖區(qū)等方式來讀取文件內(nèi)容
3.4、網(wǎng)絡(luò)IO消耗嚴重的解決方法
從程序角度而言,,造成網(wǎng)絡(luò)IO消耗嚴重的主要原因是同時需要發(fā)送或接受的包太多,。可采用限流,。限流通常是限制發(fā)送packet的頻率,,從而在網(wǎng)絡(luò)IO消耗可接受的情況下來發(fā)送packet。
3.5,、內(nèi)存消耗嚴重的情況
1,、對JVM調(diào)優(yōu);2,、代碼調(diào)優(yōu);
代碼調(diào)優(yōu)的方式:
① 釋放不必要的引用。如使用ThreadLocal,,由于線程復(fù)用,,ThreadLocal中存放的對象如未主動釋放的話,不會被GC,。應(yīng)該在執(zhí)行完畢執(zhí)行ThreadLocal.set把對象清除,,避免此有不必要的對象引用。
② 使用對象緩存池(享元模式)
③ 采用合理的緩存失效算法(FIFO,、LRU,、LFU等)
當緩存池達到最大容量后,如果再加入新對象時采用FIFO,、LRU、LFU等失效算法,。
④ 對于占據(jù)內(nèi)存但又不是必須存在的對象使用SoftReference,、WeakReference的方式進行緩存。
SoftReference在內(nèi)存不夠用的情況進行回收;WeakReference在FULL GC的情況下進行回收,。
3.6,、對于資源消耗不多,但程序執(zhí)行慢的情況
3.6.1,、鎖競爭激烈—見3.2.2②
3.6.2,、未充分利用硬件資源。
① 未充分利用CPU
啟動多線程,,但是單線程演變?yōu)槎嗑€程要加鎖,,如:單線程計算,拆分為多線程分別計算,,最后合并結(jié)果 如:JDK7的fork-join框架,。
② 未充分使用內(nèi)存
數(shù)據(jù)庫緩存、耗時資源緩存(數(shù)據(jù)庫連接的創(chuàng)建,、網(wǎng)絡(luò)連接的創(chuàng)建等),、頁面片段的緩存等。
結(jié)束語:從純粹的軟件角度調(diào)優(yōu)來講,,充分而不過分的使用硬件資源,,合理調(diào)整JVM以及合理使用JDK包是調(diào)優(yōu)的三大有效原則,調(diào)優(yōu)沒有“銀彈”,。結(jié)合系統(tǒng)現(xiàn)狀和多嘗試不同的調(diào)優(yōu)策略是找到合適調(diào)優(yōu)方法的唯一途徑,。