從代碼層面優化系統性能的解決方案

編者按:本文來自微信公眾號"InfoQ"(ID: infoqchina),作者:程超,InfoQ 簽約作者,10 年 Java 工作經驗,擅長和感興趣的技術領域是分佈式和大數據方面,目前主要從事金融支付類方向;36氪經授權發佈。

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

寫在前面

上一篇 我們主要介紹了所遇到問題的五點,那麼今天接下來討論剩下的問題,我們先再回顧一下之前討論的問題:

  • 單台 40TPS,加到 4 台服務器能到 60TPS,擴展性幾乎沒有。

  • 在實際生產環境中,經常出現數據庫死鎖導致整個服務中斷不可用。

  • 數據庫事務亂用,導致事務佔用時間太長。

  • 在實際生產環境中,服務器經常出現內存溢出和 CPU 時間被佔滿。

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

  • 程序中沒有打印關鍵日誌,或者打印了日誌,信息卻是無用信息沒有任何參考價值。

  • 配置信息和變動不大的信息依然會從數據庫中頻繁讀取,導致數據庫 IO 很大。

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

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

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

  • 沒有故障降級策略,項目出了問題后解決的時間較長,或者直接粗暴的回滾項目,但是不一定能解決問題。

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

優化解決方案

緩存優化方案

針對配置信息和變動不大的信息可以放到緩存中,提高併發能力也能夠降低 IO 緩存,具體緩存優化策略可以參考我之前寫的:

http://www.jianshu.com/p/d96906140199

程序容錯優化方案

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

註:

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

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

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

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

這是一個最簡單的圖,應用服務定期從 redis 中獲取配置信息,可能會有朋友認為這樣已經很穩定了,但是如果 Redis 出現問題呢?可能會有朋友說,Redis 會是集群,分片或者主從,確保不會出現問題。其實我是這樣的認為的,雖然應用服務程序盡量的保持輕量級是不錯的,但是不能因此而把希望全部寄托在中間組件上面,換句話說,如果此時的 Redis 是單點,那麼後果會是什麼樣的,那麼隨着大量的併發請求到來的時候,程序中會報大量的錯誤,同時正常的流程也不能進行下去了業務也可能由此而中斷。

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

方案一:

這種方式通過應用服務器的 Ehcache 定時輪詢 Redis 緩存服務器更同步更新本地緩存,缺點是因為每台服務器定時 Ehcache 的時間不一樣,那麼不同服務器刷新最新緩存的時間也不一樣,會產生數據不一致問題,對一致性要求不高可以使用。

方案二:

通過引入了 MQ 隊列,使每台應用服務器的 Ehcache 同步偵聽 MQ 消息,這樣在一定程度上可以達到 准同步 更新數據,通過 MQ 推送或者拉取的方式,但是因為不同服務器之間的網絡速度的原因,所以也不能完全達到強一致性。基於此原理使用 Zookeeper 等分佈式協調通知組件也是如此。

部分項目拆分不徹底

拆分前

註:

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

拆分后

註:

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

因基礎平台組件功能不完善導致性能下降

先看一段代碼:

註:

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

如何快速定位程序性能瓶頸

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

一般我們會通過 top 命令查看各個進程的 cpu 和內存佔用情況,獲得到了我們的進程 id,然後我們將會通過 pstack 命令查看裡邊的各個線程 id 以及對應的線程現在正在做什麼事情,分析多組數據就可以獲得哪些線程里有慢操作影響了服務器的性能,從而得到解決方案。示例如下:

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

關於索引的優化

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

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

索引不會包含有 NULL 值的列:只要列中包含有 NULL 值都將不會被包含在索引中,複合索引中只要有一列含有 NULL 值,那麼這一列對於此複合索引就是無效的。所以我們在數據庫設計時不要讓字段的默認值為 NULL。

MySQL 索引排序:MySQL 查詢只使用一個索引,因此如果 where 子句中已經使用了索引的話,那麼 order by 中的列是不會使用索引的。因此數據庫默認排序可以符合要求的情況下不要使用排序操作;盡量不要包含多個列的排序,如果需要最好給這些列創建複合索引。

使用索引的注意事項

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

  • 大於等於

  • Between

  • IN

  • LIKE 不以 % 開頭

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

  • NOT IN

  • LIKE %_ 開頭

索引技巧

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

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

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

  • 使用 Redis 需要注意的一些點
  • 在增加 key 的時候盡量設置過期時間,不然 Redis Server 的內存使用會達到系統物理內存的最大值,導致 Redis 使用 VM 降低系統性能

  • Redis Key 設計時應該儘可能短,Value 盡量不要使用複雜對象。

  • 將對象轉換成 JSON 對象(利用現成的 JSON 庫)后存入 Redis,

  • 將對象轉換成 Google 開源二進制協議對象(Google Protobuf,和 JSON 數據格式類似,但是因為是二進製表現,所以性能效率以及空間佔用都比 JSON 要小;缺點是 Protobuf 的學習曲線比 JSON 大得多)

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

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

關於長耗時方法的拆分

我們拆分長耗時方法的一般技巧是:

  • 尋找業務的冗餘點,代碼中有很多重複性的代碼,可以適當簡化。

  • 檢查庫表索引是否合理加入。

  • 利用單元測試或者壓力測試長耗時的操作進行算法級別優化,比如從庫中大批量讀取數據,或者長時間循環操作,或者死循環操作等等。

  • 尋找業務的拆分點,根據業務需求拆分同步操作為異步,比如可以使用消息隊列或者多線程異步化。

經過以上幾個分析后如果方法執行時間仍然非常的長,這樣可能就是業務方面的需求使然,如下圖:

那麼我們是否可以考慮將一個長耗時方法進行拆分,拆分為多個短耗時方法由發起端分別調用,這樣在高併發的情況下不會造成某一個方法的長時間阻塞,在一定程度上能夠提高併發能力,如下圖:


想在手機閱讀更多程式設計資訊?下載【香港矽谷】Android應用
分享到Facebook
技術平台: Nasthon Systems