咱們之前看到的不少架構變遷或者演進方面的文章大多都是針對架構方面的介紹,不多有針對代碼級別的性能優化介紹,這就比如蓋樓同樣,樓房的基礎架子搭的很好,可是蓋房的工人不夠專業,有不少須要注意的地方忽略了,那麼在往裏面填磚加瓦的時候出了問題,後果就是房子常常漏雨,牆上有裂縫等各類問題出現,雖然不至於樓房塌陷,但樓房也已經變成了危樓。那麼今天咱們就將針對一些代碼細節方面的東西進行介紹,歡迎你們吐槽以及提建議。java
服務器環境web
服務器配置:4核CPU,8G內存,共4臺sql
MQ:RabbitMQ數據庫
數據庫:DB2後端
SOA框架:公司內部封裝的Dubbo緩存
緩存框架:Redis、Memcached性能優化
統一配置管理系統:公司內部開發的系統服務器
問題描述架構
單臺40TPS,加到4臺服務器能到60TPS,擴展性幾乎沒有。併發
在實際生產環境中,常常出現數據庫死鎖致使整個服務中斷不可用。
數據庫事務亂用,致使事務佔用時間太長。
在實際生產環境中,服務器常常出現內存溢出和CPU時間被佔滿。
程序開發的過程當中,考慮不全面,容錯不好,常常由於一個小bug而致使服務不可用。
程序中沒有打印關鍵日誌,或者打印了日誌,信息倒是無用信息沒有任何參考價值。
配置信息和變更不大的信息依然會從數據庫中頻繁讀取,致使數據庫IO很大。
項目拆分不完全,一個Tomcat中會佈署多個項目WAR包。
由於基礎平臺的bug,或者功能缺陷致使程序可用性下降。
程序接口中沒有限流策略,致使不少VIP商戶直接拿咱們的生產環境進行壓測,直接影響真正的服務可用性。
沒有故障降級策略,項目出了問題後解決的時間較長,或者直接粗暴的回滾項目,可是不必定能解決問題。
沒有合適的監控系統,不能準實時或者提早發現項目瓶頸。
優化解決方案
一、數據庫死鎖優化解決
咱們從第二條開始分析,先看一個基本例子展現數據庫死鎖的發生:
注:在上述事例中,會話B會拋出死鎖異常,死鎖的緣由就是A和B二個會話互相等待。
分析:出現這種問題就是咱們在項目中混雜了大量的事務+for update語句,針對數據庫鎖來講有下面三種基本鎖:
Record Lock:單個行記錄上的鎖
Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄自己
Next-Key Lock:Gap Lock + Record Lock,鎖定一個範圍,而且鎖定記錄自己
當for update語句和gap lock和next-key lock鎖相混合使用,又沒有注意用法的時候,就很是容易出現死鎖的狀況。
那咱們用大量的鎖的目的是什麼,通過業務分析發現,其實就是爲了防重,同一時刻有可能會有多筆支付單發到相應系統中,而防重措施是經過在某條記錄上加鎖的方式來進行。
針對以上問題徹底沒有必要使用悲觀鎖的方式來進行防重,不只對數據庫自己形成極大的壓力,同時也會把對於項目擴展性來講也是很大的擴展瓶頸,咱們採用了三種方法來解決以上問題:
使用Redis來作分佈式鎖,Redis採用多個來進行分片,其中一個Redis掛了也不要緊,從新爭搶就能夠了。
使用主鍵防重方法,在方法的入口處使用防重表,可以攔截全部重複的訂單,當重複插入時數據庫會報一個重複錯,程序直接返回。
使用版本號的機制來防重。
以上三種方式都必需要有過時時間,當鎖定某一資源超時的時候,可以釋放資源讓競爭從新開始。
二、數據庫事務佔用時間過長
僞代碼示例:
項目中相似這樣的程序有不少,常常把相似httpClient,或者有可能會形成長時間超時的操做混在事務代碼中,不只會形成事務執行時間超長,並且也會嚴重下降併發能力。
那麼咱們在用事務的時候,遵循的原則是快進快出,事務代碼要儘可能小。針對以上僞代碼,咱們要用httpClient這一行拆分出來,避免同事務性的代碼混在一塊兒,這不是一個好習慣。
三、CPU時間被佔滿分析
下面以我以前分析的一個案例做爲問題的起始點,首先看下面的圖:
項目在壓測的過程當中,CPU一直居高不下,那麼經過分析得出以下分析:
1)數據庫鏈接池影響
咱們針對線上的環境進行模擬,儘可能真實的在測試環境中再現,採用數據庫鏈接池爲我們默認的C3P0。
那麼當壓測到二萬批,100個用戶同時訪問的時候,併發量忽然降爲零!報錯以下:
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.
那麼針對以上錯誤跟蹤C3P0源碼,以及在網上搜索資料發現C3P0在大併發下表現的性能不佳。
2)線程池使用不當引發
以上代碼的場景是每一次併發請求過來,都會建立一個線程,將DUMP日誌導出進行分析發現,項目中啓動了一萬多個線程,並且每一個線程都極爲忙碌,完全將資源耗盡。
那麼問題到底在哪裏呢???就在這一行!
private static final ExecutorService executorService = Executors.newCachedThreadPool();
在併發的狀況下,無限制的申請線程資源形成性能嚴重降低,在圖表中顯拋物線形狀的元兇就是它!!!那麼採用這種方式最大能夠產生多少個線程呢??答案是:Integer的最大值!看以下源碼:
那麼嘗試修改爲以下代碼:
private static final ExecutorService executorService = Executors.newFixedThreadPool(50);
修改完成之後,併發量從新上升到100以上TPS,可是當併發量很是大的時候,項目GC(垃圾回收能力降低),分析緣由仍是由於Executors.newFixedThreadPool(50)這一行,雖然解決了產生無限線程的問題,可是當併發量很是大的時候,採用newFixedThreadPool這種方式,會形成大量對象堆積到隊列中沒法及時消費,看源碼以下:
能夠看到採用的是×××隊列,也就是說隊列是能夠無限的存放可執行的線程,形成大量對象沒法釋放和回收。
3)最終線程池技術方案
方案一
注:由於服務器的CPU只有4核,有的服務器甚至只有2核,因此在應用程序中大量使用線程的話,反而會形成性能影響,針對這樣的問題,咱們將全部異步任務所有拆出應用項目,以任務的方式發送到專門的任務處理器處理,處理完成回調應用程序器。後端定時任務會定時掃描任務表,定時將超時未處理的異步任務再次發送到任務處理器進行處理。
方案二
使用AKKA技術框架,下面是我之前寫的一個簡單的壓測狀況:
http://www.jianshu.com/p/6d62256e3327
四、日誌打印問題
先看下面這段日誌打印程序:
像這樣的代碼是嚴格不符合規範的,雖然每一個公司都有本身的打印要求。
首先日誌的打印必須是以logger.error或者logger.warn的方式打印出來。
日誌打印格式:[系統來源] 錯誤描述 [關鍵信息],日誌信息要能打印出能看懂的信息,有前因和後果。甚至有些方法的入參和出參也要考慮打印出來。
在輸入錯誤信息的時候,Exception不要以e.getMessage的方式打印出來。
合理的日誌格式是:
咱們在程序中大量的打印日誌,雖然可以打印不少有用信息幫助咱們排查問題,可是更可能是日誌量太多不只影響磁盤IO,更多會形成線程阻塞對程序的性能形成較大影響。
在使用Log4j1.2.14版本的時候,使用以下格式:
%d %-5p %c:%L [%t] - %m%n
那麼在壓測的時候會出現下面大量的線程阻塞,以下圖:
再看壓測圖以下:
緣由能夠根據log4j源碼分析以下:
注:Log4j源碼裏用了synchronized鎖,而後又經過打印堆棧來獲取行號,在高併發下可能就會出現上面的狀況。
因而修改Log4j配置文件爲:
%d %-5p %c [%t] - %m%n
上面問題解決,線程阻塞的狀況不多出現,極大的提升了程序的併發能力,以下圖所示: