當前位置:
首頁 > 科技 > 從代碼層面優化系統性能應該怎麼做?

從代碼層面優化系統性能應該怎麼做?

作者|程超

編輯|小智

我們以前看到的很多架構變遷或者演進方面的文章大多都是針對架構方面的介紹,很少有針對代碼級別的性能優化介紹。本文將針對一些代碼細節方面的東西進行介紹,歡迎大家吐槽以及提建議。

伺服器環境

伺服器配置: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 語句,針對資料庫鎖來說有下面三種基本鎖:

Record Lock:單個行記錄上的鎖

Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄本身

Next-Key Lock:Gap Lock + Record Lock,鎖定一個範圍,並且鎖定記錄本身

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

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

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

使用 Redis 來做分散式鎖,Redis 採用多個來進行分片,其中一個 Redis 掛了也沒關係,重新爭搶就可以了。

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

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

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

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

偽代碼示例:

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

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

3、CPU 時間被佔滿分析

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

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

資料庫連接池影響

我們針對線上的環境進行模擬,盡量真實的在測試環境中再現,採用資料庫連接池為咱們默認的 C3P0。

那麼當壓測到二萬批,100 個用戶同時訪問的時候,並發量突然降為零!報錯如下:

那麼針對以上錯誤跟蹤 C3P0 源碼,以及在網上搜索資料發現 C3P0 在大並發下表現的性能不佳。

線程池使用不當引起

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

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

private static final ExecutorService executorService = Executors.newCachedThreadPool();

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

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

private static final ExecutorService executorService = Executors.newFixedThreadPool(50);

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

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

最終線程池技術方案

方案一

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

方案二

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

http://www.jianshu.com/p/6d62256e3327

4、日誌列印問題

先看下面這段日誌列印程序:

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

首先日誌的列印必須是以 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

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

作者介紹

程超,易寶支付架構師,10 年 Java 工作經驗,擅長和感興趣的技術領域是分散式和大數據方面,目前主要從事金融支付類方向。

今日薦文

點擊展開全文

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 InfoQ 的精彩文章:

微博&蘑菇街&UCloud:混合雲架構實踐和系統建設經驗談
如何成為一個合格的技術Leader?一位高顏值的女性研發總監如是說
京東物流系統自動化運維平台技術揭密
網路安全這條路,如何打怪升級幹掉大Boss?

TAG:InfoQ |

您可能感興趣

電腦應該這樣優化可以減少系統垃圾,讓系統運行變快
為什麼沒有一種手機系統能夠結合安卓和蘋果?
微軟做電腦系統那麼好,為什麼做不好手機系統?
黑鯊手機裡面的冷液系統是什麼原理?
系統的練字會改變原來的字體嗎?
如何從系統層面優化深度學習計算?
「新風系統」 是什麼?能代替空調嗎?
安卓系統怎麼就沒有蘋果系統的「流暢性」和「穩定性」?
都有空氣凈化功能,空氣凈化器能替代新風系統嗎?
如何在閉環控制應用中提高系統性能?
怎樣更好的運用知識管理系統?
手機系統蘋果比安卓好在哪裡?為什麼蘋果系統不容易卡頓,原理是怎樣的呢?
膽不好有什麼癥狀?消化系統首當其衝,癥狀多樣
應怎樣理解單兵綜合系統?效能遠大於步槍加榴彈發射器
手機總是提示更新系統,那我要不要更新呢?
分散式存儲系統的一致性是什麼?
電池容量大還不夠!手機長續航還要像它一樣做好系統優化
怎麼樣才算是系統健身?如何定義系統健身?
阿里雲系統能不能代替安卓系統,你們怎麼看?
呼叫中心系統都這麼智能了?!智能化客服解決方案,必備!