性能优化—代码层面优化
原文地址:https://www.cnblogs.com/qmfsun/p/5589891.html
我們以前看到的很多架構(gòu)變遷或者演進(jìn)方面的文章大多都是針對(duì)架構(gòu)方面的介紹,很少有針對(duì)代碼級(jí)別的性能優(yōu)化介紹,這就好比蓋樓一樣,樓房的基礎(chǔ)架子搭的很好,但是蓋房的工人不夠?qū)I(yè),有很多需要注意的地方忽略了,那么在往里面填磚加瓦的時(shí)候出了問(wèn)題,后果就是房子經(jīng)常漏雨,墻上有裂縫等各種問(wèn)題出現(xiàn),雖然不至于樓房塌陷,但樓房也已經(jīng)變成了危樓。那么今天我們就將針對(duì)一些代碼細(xì)節(jié)方面的東西進(jìn)行介紹,歡迎大家吐槽以及提建議。
一、服務(wù)器環(huán)境
-
服務(wù)器配置:4核CPU,8G內(nèi)存,共4臺(tái)
-
MQ:RabbitMQ
-
數(shù)據(jù)庫(kù):DB2
-
SOA框架:公司內(nèi)部封裝的Dubbo
-
緩存框架:Redis、Memcached
-
統(tǒng)一配置管理系統(tǒng):公司內(nèi)部開(kāi)發(fā)的系統(tǒng)
二、問(wèn)題描述
單臺(tái)40TPS,加到4臺(tái)服務(wù)器能到60TPS,擴(kuò)展性幾乎沒(méi)有。
1、服務(wù)器相關(guān):
1)在實(shí)際生產(chǎn)環(huán)境中,服務(wù)器經(jīng)常出現(xiàn)內(nèi)存溢出和CPU時(shí)間被占滿。
2、數(shù)據(jù)庫(kù)相關(guān)
1)數(shù)據(jù)庫(kù)事務(wù)亂用,導(dǎo)致事務(wù)占用時(shí)間太長(zhǎng)。
2)在實(shí)際生產(chǎn)環(huán)境中,經(jīng)常出現(xiàn)數(shù)據(jù)庫(kù)死鎖導(dǎo)致整個(gè)服務(wù)中斷不可用。
3)配置信息和變動(dòng)不大的信息依然會(huì)從數(shù)據(jù)庫(kù)中頻繁讀取,導(dǎo)致數(shù)據(jù)庫(kù)IO很大。
3、程序容錯(cuò)能力
1)程序開(kāi)發(fā)的過(guò)程中,考慮不全面,容錯(cuò)很差,經(jīng)常因?yàn)橐粋€(gè)小bug而導(dǎo)致服務(wù)不可用。
2)因?yàn)榛A(chǔ)平臺(tái)的bug,或者功能缺陷導(dǎo)致程序可用性降低。
3)沒(méi)有故障降級(jí)策略,項(xiàng)目出了問(wèn)題后解決的時(shí)間較長(zhǎng),或者直接粗暴的回滾項(xiàng)目,但是不一定能解決問(wèn)題。
4、規(guī)范性
1)程序中沒(méi)有打印關(guān)鍵日志,或者打印了日志,信息卻是無(wú)用信息沒(méi)有任何參考價(jià)值。
2)項(xiàng)目拆分不徹底,一個(gè)Tomcat中會(huì)布署多個(gè)項(xiàng)目WAR包。
3)程序接口中沒(méi)有限流策略,導(dǎo)致很多VIP商戶直接拿我們的生產(chǎn)環(huán)境進(jìn)行壓測(cè),直接影響真正的服務(wù)可用性。
三、優(yōu)化解決方案
1、CPU時(shí)間被占滿分析
項(xiàng)目在壓測(cè)的過(guò)程中,CPU一直居高不下,看下面的圖:
那么通過(guò)分析得出找出如下瓶頸:
1)數(shù)據(jù)庫(kù)連接池影響
我們針對(duì)線上的環(huán)境進(jìn)行模擬,盡量真實(shí)的在測(cè)試環(huán)境中再現(xiàn),采用數(shù)據(jù)庫(kù)連接池為咱們默認(rèn)的C3P0。
那么當(dāng)壓測(cè)到二萬(wàn)批,100個(gè)用戶同時(shí)訪問(wèn)的時(shí)候,并發(fā)量突然降為零!報(bào)錯(cuò)如下:
com.yeepay.g3.utils.common.exception.YeepayRuntimeException: Could not get JDBC Connection; nested exception is java.sql.SQLException: An attempt by a client to checkout a Connection has timed out.
那么針對(duì)以上錯(cuò)誤跟蹤C(jī)3P0源碼,以及在網(wǎng)上搜索資料發(fā)現(xiàn)C3P0在大并發(fā)下表現(xiàn)的性能不佳。
2)線程池使用不當(dāng)引起
以上代碼的場(chǎng)景是每一次并發(fā)請(qǐng)求過(guò)來(lái),都會(huì)創(chuàng)建一個(gè)線程,將DUMP日志導(dǎo)出進(jìn)行分析發(fā)現(xiàn),項(xiàng)目中啟動(dòng)了一萬(wàn)多個(gè)線程,而且每個(gè)線程都極為忙碌,徹底將資源耗盡。
那么問(wèn)題到底在哪里呢???就在這一行!
private static final ExecutorService executorService = Executors.newCachedThreadPool();
在并發(fā)的情況下,無(wú)限制的申請(qǐng)線程資源造成性能嚴(yán)重下降,在圖表中顯拋物線形狀的元兇就是它!!!那么采用這種方式最大可以產(chǎn)生多少個(gè)線程呢??答案是:Integer的最大值!看如下源碼:
那么嘗試修改成如下代碼:
private static final ExecutorService executorService = Executors.newFixedThreadPool(50);
修改完成以后,并發(fā)量重新上升到100以上TPS,但是當(dāng)并發(fā)量非常大的時(shí)候,項(xiàng)目GC(垃圾回收能力下降),分析原因還是因?yàn)镋xecutors.newFixedThreadPool(50)這一行,雖然解決了產(chǎn)生無(wú)限線程的問(wèn)題,但是當(dāng)并發(fā)量非常大的時(shí)候,采用newFixedThreadPool這種方式,會(huì)造成大量對(duì)象堆積到隊(duì)列中無(wú)法及時(shí)消費(fèi),看源碼如下:
可以看到采用的是無(wú)界隊(duì)列,也就是說(shuō)隊(duì)列是可以無(wú)限的存放可執(zhí)行的線程,造成大量對(duì)象無(wú)法釋放和回收。
3)最終線程池技術(shù)方案
方案一:
注:因?yàn)榉?wù)器的CPU只有4核,有的服務(wù)器甚至只有2核,所以在應(yīng)用程序中大量使用線程的話,反而會(huì)造成性能影響,針對(duì)這樣的問(wèn)題,我們將所有異步任務(wù)全部拆出應(yīng)用項(xiàng)目,以任務(wù)的方式發(fā)送到專門的任務(wù)處理器處理,處理完成回調(diào)應(yīng)用程序器。后端定時(shí)任務(wù)會(huì)定時(shí)掃描任務(wù)表,定時(shí)將超時(shí)未處理的異步任務(wù)再次發(fā)送到任務(wù)處理器進(jìn)行處理。
方案二
使用AKKA技術(shù)框架,下面是我以前寫的一個(gè)簡(jiǎn)單的壓測(cè)情況:http://www.jianshu.com/p/6d62256e3327
2、數(shù)據(jù)庫(kù)事務(wù)占用時(shí)間過(guò)長(zhǎng)
偽代碼示例:
項(xiàng)目中類似這樣的程序有很多,經(jīng)常把類似httpClient,或者有可能會(huì)造成長(zhǎng)時(shí)間超時(shí)的操作混在事務(wù)代碼中,不
僅會(huì)造成事務(wù)執(zhí)行時(shí)間超長(zhǎng),而且也會(huì)嚴(yán)重降低并發(fā)能力。
那么我們?cè)谟檬聞?wù)的時(shí)候,遵循的原則是快進(jìn)快出,事務(wù)代碼要盡量小。針對(duì)以上偽代碼,我們要用httpClient這一行拆分出來(lái),避免同事務(wù)性的代碼混在一起,這不是一個(gè)好習(xí)慣。
3、數(shù)據(jù)庫(kù)死鎖優(yōu)化解決
我們從第二條開(kāi)始分析,先看一個(gè)基本例子展示數(shù)據(jù)庫(kù)死鎖的發(fā)生:
注:在上述事例中,會(huì)話B會(huì)拋出死鎖異常,死鎖的原因就是A和B二個(gè)會(huì)話互相等待。
分析:出現(xiàn)這種問(wèn)題就是我們?cè)陧?xiàng)目中混雜了大量的事務(wù)+for update語(yǔ)句,針對(duì)數(shù)據(jù)庫(kù)鎖來(lái)說(shuō)有下面三種基本鎖:
-
Record Lock:單個(gè)行記錄上的鎖
-
Gap Lock:間隙鎖,鎖定一個(gè)范圍,但不包含記錄本身
-
Next-Key Lock:Gap Lock + Record Lock,鎖定一個(gè)范圍,并且鎖定記錄本身
當(dāng)for update語(yǔ)句和gap lock和next-key lock鎖相混合使用,又沒(méi)有注意用法的時(shí)候,就非常容易出現(xiàn)死鎖的情況。
那我們用大量的鎖的目的是什么,經(jīng)過(guò)業(yè)務(wù)分析發(fā)現(xiàn),其實(shí)就是為了防重,同一時(shí)刻有可能會(huì)有多筆支付單發(fā)到相應(yīng)系統(tǒng)中,而防重措施是通過(guò)在某條記錄上加鎖的方式來(lái)進(jìn)行。
針對(duì)以上問(wèn)題完全沒(méi)有必要使用悲觀鎖的方式來(lái)進(jìn)行防重,不僅對(duì)數(shù)據(jù)庫(kù)本身造成極大的壓力,同時(shí)也會(huì)把對(duì)于項(xiàng)目擴(kuò)展性來(lái)說(shuō)也是很大的擴(kuò)展瓶頸,我們采用了三種方法來(lái)解決以上問(wèn)題:
-
使用Redis來(lái)做分布式鎖,Redis采用多個(gè)來(lái)進(jìn)行分片,其中一個(gè)Redis掛了也沒(méi)關(guān)系,重新?tīng)?zhēng)搶就可以了。
-
使用主鍵防重方法,在方法的入口處使用防重表,能夠攔截所有重復(fù)的訂單,當(dāng)重復(fù)插入時(shí)數(shù)據(jù)庫(kù)會(huì)報(bào)一個(gè)重復(fù)錯(cuò),程序直接返回。
-
使用版本號(hào)的機(jī)制來(lái)防重。
以上三種方式都必須要有過(guò)期時(shí)間,當(dāng)鎖定某一資源超時(shí)的時(shí)候,能夠釋放資源讓競(jìng)爭(zhēng)重新開(kāi)始。
4、日志打印問(wèn)題
先看下面這段日志打印程序:
像這樣的代碼是嚴(yán)格不符合規(guī)范的,雖然每個(gè)公司都有自己的打印要求。
- 首先日志的打印必須是以logger.error或者logger.warn的方式打印出來(lái)。
- 日志打印格式:[系統(tǒng)來(lái)源] 錯(cuò)誤描述 [關(guān)鍵信息],日志信息要能打印出能看懂的信息,有前因和后果。甚至有些方法的入?yún)⒑统鰠⒁惨紤]打印出來(lái)。
- 在輸入錯(cuò)誤信息的時(shí)候,Exception不要以e.getMessage的方式打印出來(lái)。
合理的日志格式是:
我們?cè)诔绦蛑写罅康拇蛴∪罩?#xff0c;雖然能夠打印很多有用信息幫助我們排查問(wèn)題,但是更多是日志量太多不僅影響磁盤IO,更多會(huì)造成線程阻塞對(duì)程序的性能造成較大影響。
在使用Log4j1.2.14版本的時(shí)候,使用如下格式:
%d %-5p %c:%L [%t] - %m%n
那么在壓測(cè)的時(shí)候會(huì)出現(xiàn)下面大量的線程阻塞,如下圖:
再看壓測(cè)圖如下:
原因可以根據(jù)log4j源碼分析如下:
注:Log4j源碼里用了synchronized鎖,然后又通過(guò)打印堆棧來(lái)獲取行號(hào),在高并發(fā)下可能就會(huì)出現(xiàn)上面的情況。
于是修改Log4j配置文件為:
%d %-5p %c [%t] - %m%n
上面問(wèn)題解決,線程阻塞的情況很少出現(xiàn),極大的提高了程序的并發(fā)能力,如下圖所示:
?
作者:Agoly?
出處:https://www.cnblogs.com/qmfsun/?
本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁(yè)面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。?
如果文中有什么錯(cuò)誤,歡迎指出。以免更多的人被誤導(dǎo)。?
總結(jié)
以上是生活随笔為你收集整理的性能优化—代码层面优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 数据库性能优化—全局优化思路
- 下一篇: 彻底理解cookie、session、t