天天看點

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

在最前面 

今天我将和大家一起探讨我負責的某項目的性能變遷之路。

我們以前看到的很多架構變遷或者演進方面的文章大多都是針對架構方面的介紹,很少有針對代碼級别的性能優化介紹,這就好比蓋樓一樣,樓房的基礎架子搭得很好,但是蓋房的勞工不夠專業,有很多需要注意的地方忽略了,那麼在往裡面填磚加瓦的時候出了問題,後果就是房子經常漏雨,牆上有裂縫等各種問題出現,雖然不至于樓房塌陷,但樓房也已經變成了危樓。

那麼今天我們就将針對一些代碼細節方面的東西進行介紹,歡迎大家吐槽以及提建議。

一、伺服器環境 

伺服器配置:4核cpu 8g記憶體 共4台

mq:rabbitmq

資料庫:db2

soa架構:公司内部封裝的dubbo

緩存架構:redis,memcached

統一配置管理系統:公司内部開發的系統

二、問題描述 

單台40tps,加到4台伺服器能到60tps,擴充性幾乎沒有。

在實際生産環境中,經常出現資料庫死鎖導緻整個服務中斷不可用。

資料庫事務亂用,導緻事務占用時間太長。

在實際生産環境中,伺服器經常出現記憶體溢出和cpu時間被占滿。

程式開發的過程中,考慮不全面,容錯很差,經常因為一個小bug而導緻服務不可用。

程式中沒有列印關鍵日志,或者列印了日志,資訊卻是無用資訊沒有任何參考價值。

配置資訊和變動不大的資訊依然會從資料庫中頻繁讀取,導緻資料庫io很大。

項目拆分不徹底,一個tomcat中會布署多個項目war包。

因為基礎平台的bug,或者功能缺陷導緻程式可用性降低。

程式接口中沒有限流政策,導緻很多vip商戶直接拿我們的生産環境進行壓測,直接影響真正的服務可用性。

沒有故障降級政策,項目出了問題後解決的時間較長,或者直接粗暴的復原項目,但是不一定能解決問題。

沒有合适的監控系統,不能準實時或者提前發現項目瓶頸。

三、優化解決方案 

1、資料庫死鎖優化解決

我們從第二條開始分析,先看一個基本例子展示資料庫死鎖的發生:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

注:在上述事例中,會話b會抛出死鎖異常,死鎖的原因就是a和b二個會話互相等待。

分析:出現這種問題就是我們在項目中混雜了大量的事務+for update語句,針對資料庫鎖來說有下面三種基本鎖:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

當for update語句和gap lock和next-key lock鎖相混合使用,又沒有注意用法的時候,就非常容易出現死鎖的情況。

那我們用大量的鎖的目的是什麼,經過業務分析發現,其實就是為了防重,同一時刻有可能會有多筆支付單發到相應系統中,而防重措施是通過在某條記錄上加鎖的方式來進行。

針對以上問題完全沒有必要使用悲觀鎖的方式來進行防重,不僅對資料庫本身造成極大的壓力,同時也會把對于項目擴充性來說也是很大的擴充瓶頸,我們采用了三種方法來解決以上問題:

使用redis來做分布式鎖,redis采用多個來進行分片,其中一個redis挂了也沒關系,重新争搶就可以了。

使用主鍵防重方法,在方法的入口處使用防重表,能夠攔截所有重複的訂單,當重複插入時資料庫會報一個重複錯,程式直接傳回。

使用版本号的機制來防重。

以上三種方式都必須要有過期時間,當鎖定某一資源逾時的時候,能夠釋放資源讓競争重新開始。

2、資料庫事務占用時間過長

僞代碼示例:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

項目中類似這樣的程式有很多,經常把類似httpclient,或者有可能會造成長時間逾時的操作混在事務代碼中,不僅會造成事務執行時間超長,而且也會嚴重降低并發能力。

那麼我們在用事務的時候,遵循的原則是快進快出,事務代碼要盡量小。針對以上僞代碼,我們要用httpclient這一行拆分出來,避免同僚務性的代碼混在一起,這不是一個好習慣。

3、cpu時間被占滿分析

下面以我之前分析的一個案例作為問題的起始點,首先看下面的圖:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

項目在壓測的過程中,cpu一直居高不下,那麼通過分析得出如下分析:

資料庫連接配接池影響

我們針對線上的環境進行模拟,盡量真實的在測試環境中再現,采用資料庫連接配接池為咱們預設的c3p0。

那麼當壓測到二萬批,100個使用者同時通路的時候,并發量突然降為零!報錯如下:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

那麼針對以上錯誤跟蹤c3p0源碼,以及在網上搜尋資料:

<a target="_blank">http://blog.sina.com.cn/s/blog_53923f940100g6as.html</a>

發現c3p0在大并發下表現的性能不佳。

線程池使用不當引起

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

以上代碼的場景是每一次并發請求過來,都會建立一個線程,将dump日志導出進行分析發現,項目中啟動了一萬多個線程,而且每個線程都極為忙碌,徹底将資源耗盡。

那麼問題到底在哪裡呢???就在這一行!

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

在并發的情況下,無限制的申請線程資源造成性能嚴重下降,在圖表中顯抛物線形狀的元兇就是它!!!那麼采用這種方式最大可以産生多少個線程呢??答案是:integer的最大值!看如下源碼:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

那麼嘗試修改成如下代碼:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

修改完成以後,并發量重新上升到100以上tps,但是當并發量非常大的時候,項目gc(垃圾回收能力下降),分析原因還是因為executors.newfixedthreadpool(50)這一行,雖然解決了産生無限線程的問題,但是當并發量非常大的時候,采用newfixedthreadpool這種方式,會造成大量對象堆積到隊列中無法及時消費,看源碼如下:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

可以看到采用的是無界隊列,也就是說隊列是可以無限的存放可執行的線程,造成大量對象無法釋放和回收。

最終線程池技術方案

方案一:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

注:因為伺服器的cpu隻有4核,有的伺服器甚至隻有2核,是以在應用程式中大量使用線程的話,反而會造成性能影響,針對這樣的問題,我們将所有異步任務全部拆出應用項目,以任務的方式發送到專門的任務處理器處理,處理完成回調應用程式器。後端定時任務會定時掃描任務表,定時将逾時未處理的異步任務再次發送到任務處理器進行處理。

方案二:

使用akka技術架構,下面是我以前寫的一個簡單的壓測情況:

<a target="_blank">http://www.jianshu.com/p/6d62256e3327</a>

4、日志列印問題

先看下面這段日志列印程式:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

像這樣的代碼是嚴格不符合規範的,雖然每個公司都有自己的列印要求。

首先日志的列印必須是以logger.error或者logger.warn的方式列印出來。

日志列印格式:[系統來源] 錯誤描述 [關鍵資訊],日志資訊要能列印出能看懂的資訊,有前因和後果。甚至有些方法的入參和出參也要考慮列印出來。

在輸入錯誤資訊的時候,exception不要以e.getmessage的方式列印出來。

合理的日志格式是:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

我們在程式中大量的列印日志,雖然能夠列印很多有用資訊幫助我們排查問題,但是更多是日志量太多不僅影響磁盤io,更多會造成線程阻塞對程式的性能造成較大影響。

在使用log4j1.2.14版本的時候,使用如下格式:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

那麼在壓測的時候會出現下面大量的線程阻塞,如下圖:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

再看壓測圖如下:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)
天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

原因可以根據log4j源碼分析如下:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

注:log4j源碼裡用了synchronized鎖,然後又通過列印堆棧來擷取行号,在高并發下可能就會出現上面的情況。

于是修改log4j配置檔案為:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

上面問題解決,線程阻塞的情況很少出現,極大的提高了程式的并發能力,如下圖所示:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

5、緩存優化方案

我們在用緩存的時候,不管是redis或者memcached,基本上會通用遇到以下三個問題:

緩存穿透

緩存并發

緩存失效

(1) 緩存穿透

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)
天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)
天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

注:上面三個圖會有什麼問題呢?

我們在項目中使用緩存通常都是先檢查緩存中是否存在,如果存在直接傳回緩存内容,如果不存在就直接查詢資料庫然後再緩存查詢結果傳回。這個時候如果我們查詢的某一個資料在緩存中一直不存在,就會造成每一次請求都查詢db,這樣緩存就失去了意義,在流量大時,可能db就挂掉了。

那這種問題有什麼好辦法解決呢?

要是有人利用不存在的key頻繁攻擊我們的應用,這就是漏洞。

有一個比較巧妙的作法是,可以将這個不存在的key預先設定一個值。

比如,"key" , “&amp;&amp;”。

在傳回這個&amp;&amp;值的時候,我們的應用就可以認為這是不存在的key,那我們的應用就可以決定是否繼續等待繼續通路,還是放棄掉這次操作。如果繼續等待通路,過一個時間輪詢點後,再次請求這個key,如果取到的值不再是&amp;&amp;,則可以認為這時候key有值了,進而避免了透傳到資料庫,進而把大量的類似請求擋在了緩存之中。

(2) 緩存并發

有時候如果網站并發通路高,一個緩存如果失效,可能出現多個程序同時查詢db,同時設定緩存的情況,如果并發确實很大,這也可能造成db壓力過大,還有緩存頻繁更新的問題。

我現在的想法是對緩存查詢加鎖,如果key不存在,就加鎖,然後查db入緩存,然後解鎖;其他程序如果發現有鎖就等待,然後等解鎖後傳回資料或者進入db查詢。

這種情況和剛才說的預先設定值問題有些類似,隻不過利用鎖的方式,會造成部分請求等待。

(3) 緩存失效

引起這個問題的主要原因還是高并發的時候,平時我們設定一個緩存的過期時間時,可能有一些會設定1分鐘啊,5分鐘這些,并發很高時可能會出在某一個時間同時生成了很多的緩存,并且過期時間都一樣,這個時候就可能引發一當過期時間到後,這些緩存同時失效,請求全部轉發到db,db可能會壓力過重。

那如何解決這些問題呢?

其中的一個簡單方案就時講緩存失效時間分散開,比如我們可以在原有的失效時間基礎上增加一個随機值,比如1-5分鐘随機,這樣每一個緩存的過期時間的重複率就會降低,就很難引發集體失效的事件。

我們讨論的第二個問題時針對同一個緩存,第三個問題時針對很多緩存。

總結來看:

緩存穿透:查詢一個必然不存在的資料。比如文章表,查詢一個不存在的id,每次都會通路db,如果有人惡意破壞,很可能直接對db造成影響。

緩存失效:如果緩存集中在一段時間内失效,db的壓力凸顯。這個沒有完美解決辦法,但可以分析使用者行為,盡量讓失效時間點均勻分布。

當發生大量的緩存穿透,例如對某個失效的緩存的大并發通路就造成了緩存雪崩。

6、程式容錯優化方案

在這一塊我要先舉一個程式的例子說明一下什麼才是容錯,先看程式:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

注:那麼如果service層的方法調用dao層的方法,一旦資料插入失敗,那麼這種異常處理的方式是容錯嗎?

把異常給吃掉了,在service層調用的時候,雖然沒有列印報錯資訊,但是這能是容錯嗎?

所謂容錯是指在故障存在的情況下計算機系統不失效,仍然能夠正常工作的特性。

我們拿使用緩存來作為一個案例講解,先看一個圖:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

這是一個最簡單的圖,應用服務定期從redis中擷取配置資訊,可能會有朋友認為這樣已經很穩定了,但是如果redis出現問題呢?可能會有朋友說,redis會是叢集,分片或者主從,確定不會出現問題。

其實我是這樣認為的,雖然應用服務程式盡量的保持輕量級是不錯的,但是不能是以而把希望全部寄托在中間元件上面,換句話說,如果此時的redis是單點,那麼後果會是什麼樣的,那麼随着大量的并發請求到來的時候,程式中會報大量的錯誤,同時正常的流程也不能進行下去了業務也可能由此而中斷。

那麼在此種場景下我的解決方案是,要把緩存的使用分級别,有的緩存同步要求時效性非常高,比如支付限額配置,在背景修改完成以後前台立刻就能夠獲得感覺,并且能夠成功切換,這種情況隻能實時的從redis中擷取最新資料,但是每次擷取完最新的資料後都可以同步更新本地緩存,當單點的redis挂掉後,應用程式至少還能從本地讀取資訊而不至于服務瞬間挂掉。有的緩存對時效性要求不高,允許有一定延遲,那麼在這種情況下我采用的方案是,利用本地緩存和遠端緩存相結合的方式,如下圖所示:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

這種方式通過應用伺服器的ehcache定時輪詢redis緩存伺服器更同步更新本地緩存,缺點是因為每台伺服器定時ehcache的時間不一樣,那麼不同伺服器重新整理最新緩存的時間也不一樣,會産生資料不一緻問題,對一緻性要求不高可以使用。

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

通過引入了mq隊列,使每台應用伺服器的ehcache同步偵聽mq消息,這樣在一定程度上可以達到準同步更新資料,通過mq推送或者拉取的方式,但是因為不同伺服器之間的網絡速度的原因,是以也不能完全達到強一緻性。基于此原理使用zookeeper等分布式協調通知元件也是如此。

7、部分項目拆分不徹底

拆分前

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

注:一個tomcat中布署多個應用war包,彼此之間互相牽制在并發量非常大的情況下性能降低非常明顯。

拆分後

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

注:拆分前的這種情況其實還是挺普遍,之前我一直認為項目中不會存在這種情況但是事實上還是存在了。解決的方法很簡單,每一個應用war隻布在一個tomcat中,這樣應用程式之間就不會存在資源和連接配接數的競争情況,性能和并發能力送出較為明顯。

8、因基礎平台元件功能不完善導緻性能下降

先看一段代碼:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

注:首先我們先不說這段代碼的格式如何如何,先看功能實作,使用future來做逾時控制,這是為何呢?原因其實是在我們調用的dubbo接口上面,因為是dubbo已經經過二次封裝,結果把自帶的timeout給淹沫了,程式員隻能通過這種方式來控制逾時,可以看到這種用法非常差勁,對程式性能造成一定的影響。

9、如何快速定位程式性能瓶頸

我相信在定位程式性能問題的時候,大家有很多種辦法,比如用jdk自帶的指令,如jcmd,jstack,jmap,jhat,jstat,iostat,vmstat等等指令,還可以用visualvm,mat,jrockit等可視化工具,我今天想說的是利用一個最簡單的指令就能夠定位到哪段程式可能存在性能問題,請看下面介紹:

一般我們會通過top指令檢視各個程序的cpu和記憶體占用情況,獲得到了我們的程序id,然後我們将會通過pstack指令檢視裡邊的各個線程id以及對應的線程現在正在做什麼事情,分析多組資料就可以獲得哪些線程裡有慢操作影響了伺服器的性能,進而得到解決方案。示例如下:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

由此可以判斷出來在lwp 30222這個線程産生了性能問題,執行時間長達31.4毫秒的時間,再觀察無非就是下面的幾個語句出現的問題,隻需要簡單排查就知道了問題瓶頸。

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

10、關于索引的優化

組合索引的原則是偏左原則,是以在使用的時候需要多加注意

索引的數量不需要過多的添加,在添加的時候要考慮聚集索引和輔助索引,這二者的性能是有差別的

索引不會包含有null值的列

隻要列中包含有null值都将不會被包含在索引中,複合索引中隻要有一列含有null值,那麼這一列對于此複合索引就是無效的。是以我們在資料庫設計時不要讓字段的預設值為null。

mysql索引排序

mysql查詢隻使用一個索引,是以如果where子句中已經使用了索引的話,那麼order by中的列是不會使用索引的。是以資料庫預設排序可以符合要求的情況下不要使用排序操作;盡量不要包含多個列的排序,如果需要最好給這些列建立複合索引。

使用索引的注意事項

以下操作符可以應用索引:

大于等于

between

in

like 不以%開頭

以下操作符不能應用索引:

not in

like %_開頭

索引技巧

同樣是1234567890,數值類型存儲遠比字元串節約存儲空間。

節約存儲就是節約io,減少io就是提升性能

通常對數字的索引和檢索要比對字元串的索引和檢索效率更高。

11、使用redis需要注意的一些點

在增加key的時候盡量設定過期時間,不然redis server的記憶體使用會達到系統實體記憶體的最大值,導緻redis使用vm降低系統性能

redis key設計時應該盡可能短,value盡量不要使用複雜對象。

将對象轉換成json對象(利用現成的json庫)後存入redis。

将對象轉換成google開源二進制協定對象(google protobuf,和json資料格式類似,但是因為是二進制表現,是以性能效率以及空間占用都比json要小;缺點是protobuf的學習曲線比json大得多)

redis使用完以後一定要釋放連接配接,如下圖示例:

天天低頭寫代碼,可你知道什麼是代碼級性能優化嗎?(上)

不管是傳回到連接配接池中還是直接釋放掉,總之就是要将連接配接還回去。

在接下來的第二篇文章中我們将介紹系統的降級、限流,還有監控的一些方案。敬請期待!

 本文經作者同意,授權釋出。

作者介紹  程超

易寶支付架構師,10年java工作經驗,擅長分布式和大資料技術領域,目前主要從事金融支付類方向。

<b></b>

<b>本文來自雲栖社群合作夥伴"dbaplus",原文釋出時間:2016-06-16</b>