從代碼層面優(yōu)化系統(tǒng)性能的解決方案.
- 標簽 :
我們以前看到的很多架構(gòu)變遷或者演進方面的文章大多都是針對架構(gòu)方面的介紹,很少有針對代碼級別的性能優(yōu)化介紹。本文將針對一些代碼細節(jié)方面的東西進行介紹,歡迎大家吐槽以及提建議。
-
單臺 40TPS,加到 4 臺服務(wù)器能到 60TPS,擴展性幾乎沒有。
-
在實際生產(chǎn)環(huán)境中,經(jīng)常出現(xiàn)數(shù)據(jù)庫死鎖導(dǎo)致整個服務(wù)中斷不可用。
-
數(shù)據(jù)庫事務(wù)亂用,導(dǎo)致事務(wù)占用時間太長。
-
在實際生產(chǎn)環(huán)境中,服務(wù)器經(jīng)常出現(xiàn)內(nèi)存溢出和 CPU 時間被占滿。
-
程序開發(fā)的過程中,考慮不全面,容錯很差,經(jīng)常因為一個小 bug 而導(dǎo)致服務(wù)不可用。
-
程序中沒有打印關(guān)鍵日志,或者打印了日志,信息卻是無用信息沒有任何參考價值。
-
配置信息和變動不大的信息依然會從數(shù)據(jù)庫中頻繁讀取,導(dǎo)致數(shù)據(jù)庫 IO 很大。
-
項目拆分不徹底,一個 tomcat 中會布署多個項目 WAR 包。
-
因為基礎(chǔ)平臺的 bug,或者功能缺陷導(dǎo)致程序可用性降低。
-
程序接口中沒有限流策略,導(dǎo)致很多 vip 商戶直接拿我們的生產(chǎn)環(huán)境進行壓測,直接影響真正的服務(wù)可用性。
-
沒有故障降級策略,項目出了問題后解決的時間較長,或者直接粗暴的回滾項目,但是不一定能解決問題。
-
沒有合適的監(jiān)控系統(tǒng),不能準實時或者提前發(fā)現(xiàn)項目瓶頸。
優(yōu)化解決方案
緩存優(yōu)化方案
針對配置信息和變動不大的信息可以放到緩存中,提高并發(fā)能力也能夠降低 IO 緩存。
程序容錯優(yōu)化方案
在這一塊我要先舉一個程序的例子說明一下什么才是容錯,先看程序:
注:
那么如果 service 層的方法調(diào)用 dao 層的方法,一旦數(shù)據(jù)插入失敗,那么這種異常處理的方式是容錯嗎?
把異常給吃掉了,在 service 層調(diào)用的時候,雖然沒有打印報錯信息,但是這能是容錯嗎?
所謂容錯是指在故障存在的情況下計算機系統(tǒng)不失效,仍然能夠正常工作的特性。
我們拿使用緩存來作為一個案例講解,先看一個圖:
這是一個最簡單的圖,應(yīng)用服務(wù)定期從 redis 中獲取配置信息,可能會有朋友認為這樣已經(jīng)很穩(wěn)定了,但是如果 Redis 出現(xiàn)問題呢?可能會有朋友說,Redis 會是集群,分片或者主從,確保不會出現(xiàn)問題。其實我是這樣的認為的,雖然應(yīng)用服務(wù)程序盡量的保持輕量級是不錯的,但是不能因此而把希望全部寄托在中間組件上面,換句話說,如果此時的 Redis 是單點,那么后果會是什么樣的,那么隨著大量的并發(fā)請求到來的時候,程序中會報大量的錯誤,同時正常的流程也不能進行下去了業(yè)務(wù)也可能由此而中斷。
那么在此種場景下我的解決方案是,要把緩存的使用分級別,有的緩存同步要求時效性非常高,比如支付限額配置,在后臺修改完成以后前臺立刻就能夠獲得感知,并且能夠成功切換,這種情況只能實時的從 Redis 中獲取最新數(shù)據(jù),但是每次獲取完最新的數(shù)據(jù)后都可以同步更新本地緩存,當(dāng)單點的 Redis 掛掉后,應(yīng)用程序至少還能從本地讀取信息而不至于服務(wù)瞬間掛掉。有的緩存對時效性要求不高,允許有一定延遲,那么在這種情況下我采用的方案是,利用本地緩存和遠程緩存相結(jié)合的方式,如下圖所示:
方案一:
這種方式通過應(yīng)用服務(wù)器的 Ehcache 定時輪詢 Redis 緩存服務(wù)器更同步更新本地緩存,缺點是因為每臺服務(wù)器定時 Ehcache 的時間不一樣,那么不同服務(wù)器刷新最新緩存的時間也不一樣,會產(chǎn)生數(shù)據(jù)不一致問題,對一致性要求不高可以使用。
方案二:
通過引入了 MQ 隊列,使每臺應(yīng)用服務(wù)器的 Ehcache 同步偵聽 MQ 消息,這樣在一定程度上可以達到 準同步更新數(shù)據(jù),通過 MQ 推送或者拉取的方式,但是因為不同服務(wù)器之間的網(wǎng)絡(luò)速度的原因,所以也不能完全達到強一致性?;诖嗽硎褂?Zookeeper 等分布式協(xié)調(diào)通知組件也是如此。
部分項目拆分不徹底
拆分前
注:
一個 Tomcat 中布署多個應(yīng)用 war 包,彼此之間互相牽制在并發(fā)量非常大的情況下性能降低非常明顯。
拆分后
注:
拆分前的這種情況其實還是挺普遍,之前我一直認為項目中不會存在這種情況但是事實上還是存在了。解決的方法很簡單,每一個應(yīng)用 war 只布在一個 tomcat 中,這樣應(yīng)用程序之間就不會存在資源和連接數(shù)的競爭情況,性能和并發(fā)能力提交較為明顯。
因基礎(chǔ)平臺組件功能不完善導(dǎo)致性能下降
先看一段代碼:
注:
首先我們先不說這段代碼的格式如何如何,先看功能實現(xiàn),使用 Future 來做超時控制,這是為何呢?原因其實是在我們調(diào)用的 Dubbo 接口上面,因為是 Dubbo 已經(jīng)經(jīng)過二次封裝,結(jié)果把自帶的 timeout 給淹沫了,程序員只能通過這種方式來控制超時,可以看到這種用法非常差勁,對程序性能造成一定的影響。
如何快速定位程序性能瓶頸
我相信在定位程序性能問題的時候,大家有很多種辦法,比如用 jdk 自帶的命令,如 Jcmd,Jstack,jmap,jhat,jstat,iostat,vmstat 等等命令,還可以用 VisualVM,MAT,JRockit 等可視化工具,我今天想說的是利用一個最簡單的命令就能夠定位到哪段程序可能存在性能問題,請看下面介紹:
一般我們會通過 top 命令查看各個進程的 cpu 和內(nèi)存占用情況,獲得到了我們的進程 id,然后我們將會通過 pstack 命令查看里邊的各個線程 id 以及對應(yīng)的線程現(xiàn)在正在做什么事情,分析多組數(shù)據(jù)就可以獲得哪些線程里有慢操作影響了服務(wù)器的性能,從而得到解決方案。示例如下:
由此可以判斷出來在 LWP 30222 這個線程產(chǎn)生了性能問題,執(zhí)行時間長達 31.4 毫秒的時間,再觀察無非就是下面的幾個語句出現(xiàn)的問題,只需要簡單排查就知道了問題瓶頸。
關(guān)于索引的優(yōu)化
組合索引的原則是偏左原則,所以在使用的時候需要多加注意;
索引的數(shù)量不需要過多的添加,在添加的時候要考慮聚集索引和輔助索引,這二者的性能是有區(qū)別的;
索引不會包含有 值的列:只要列中包含有 值都將不會被包含在索引中,復(fù)合索引中只要有一列含有 值,那么這一列對于此復(fù)合索引就是無效的。所以我們在數(shù)據(jù)庫設(shè)計時不要讓字段的默認值為 。
MySQL 索引排序:MySQL 查詢只使用一個索引,因此如果 where 子句中已經(jīng)使用了索引的話,那么 order by 中的列是不會使用索引的。因此數(shù)據(jù)庫默認排序可以符合要求的情況下不要使用排序操作;盡量不要包含多個列的排序,如果需要最好給這些列創(chuàng)建復(fù)合索引。
使用索引的注意事項
以下操作符可以應(yīng)用索引:
-
大于等于
-
Between
-
IN
-
LIKE 不以 % 開頭
以下操作符不能應(yīng)用索引:
-
NOT IN
-
LIKE %_ 開頭
索引技巧
-
同樣是 1234567890,數(shù)值類型存儲遠比字符串節(jié)約存儲空間。
-
節(jié)約存儲就是節(jié)約 IO,減少 IO 就是提升性能
-
通常對數(shù)字的索引和檢索要比對字符串的索引和檢索效率更高。
使用 Redis 需要注意的一些點
-
在增加 key 的時候盡量設(shè)置過期時間,不然 Redis Server 的內(nèi)存使用會達到系統(tǒng)物理內(nèi)存的最大值,導(dǎo)致 Redis 使用 VM 降低系統(tǒng)性能
-
Redis Key 設(shè)計時應(yīng)該盡可能短,Value 盡量不要使用復(fù)雜對象。
-
將對象轉(zhuǎn)換成 JSON 對象(利用現(xiàn)成的 JSON 庫)后存入 Redis,
-
將對象轉(zhuǎn)換成 Google 開源二進制協(xié)議對象(Google Protobuf,和 JSON 數(shù)據(jù)格式類似,但是因為是二進制表現(xiàn),所以性能效率以及空間占用都比 JSON 要??;缺點是 Protobuf 的學(xué)習(xí)曲線比 JSON 大得多)
-
Redis 使用完以后一定要釋放連接,如下圖示例:
不管是返回到連接池中還是直接釋放掉,總之就是要將連接還回去。
關(guān)于長耗時方法的拆分
我們拆分長耗時方法的一般技巧是:
-
尋找業(yè)務(wù)的冗余點,代碼中有很多重復(fù)性的代碼,可以適當(dāng)簡化。
-
檢查庫表索引是否合理加入。
-
利用單元測試或者壓力測試長耗時的操作進行算法級別優(yōu)化,比如從庫中大批量讀取數(shù)據(jù),或者長時間循環(huán)操作,或者死循環(huán)操作等等。
-
尋找業(yè)務(wù)的拆分點,根據(jù)業(yè)務(wù)需求拆分同步操作為異步,比如可以使用消息隊列或者多線程異步化。
經(jīng)過以上幾個分析后如果方法執(zhí)行時間仍然非常的長,這樣可能就是業(yè)務(wù)方面的需求使然,如下圖:
那么我們是否可以考慮將一個長耗時方法進行拆分,拆分為多個短耗時方法由發(fā)起端分別調(diào)用,這樣在高并發(fā)的情況下不會造成某一個方法的長時間阻塞,在一定程度上能夠提高并發(fā)能力,如下圖:

