論代碼級性能優(yōu)化變遷之路(二)

本文是“論代碼級性能優(yōu)化變遷之路一”(http://www.itdecent.cn/p/c4a748002e66) 的第二篇。

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

1、單臺40TPS,加到4臺服務(wù)器能到60TPS,擴展性幾乎沒有。
2、在實際生產(chǎn)環(huán)境中,經(jīng)常出現(xiàn)數(shù)據(jù)庫死鎖導(dǎo)致整個服務(wù)中斷不可用。
3、數(shù)據(jù)庫事務(wù)亂用,導(dǎo)致事務(wù)占用時間太長。
4、在實際生產(chǎn)環(huán)境中,服務(wù)器經(jīng)常出現(xiàn)內(nèi)存溢出和CPU時間被占滿。
5、程序開發(fā)的過程中,考慮不全面,容錯很差,經(jīng)常因為一個小bug而導(dǎo)致服務(wù)不可用。
6、程序中沒有打印關(guān)鍵日志,或者打印了日志,信息卻是無用信息沒有任何參考價值。
7、配置信息和變動不大的信息依然會從數(shù)據(jù)庫中頻繁讀取,導(dǎo)致數(shù)據(jù)庫IO很大。
8、項目拆分不徹底,一個tomcat中會布署多個項目WAR包。
9、因為基礎(chǔ)平臺的bug,或者功能缺陷導(dǎo)致程序可用性降低。
10、程序接口中沒有限流策略,導(dǎo)致很多vip商戶直接拿我們的生產(chǎn)環(huán)境進行壓測,直接影響真正的服務(wù)可用性。
11、沒有故障降級策略,項目出了問題后解決的時間較長,或者直接粗暴的回滾項目,但是不一定能解決問題。
12、沒有合適的監(jiān)控系統(tǒng),不能準實時或者提前發(fā)現(xiàn)項目瓶頸。

四、優(yōu)化解決方案

5、緩存優(yōu)化方案
針對配置信息和變動不大的信息可以放到緩存中,提高并發(fā)能力也能夠降低IO緩存,具體緩存優(yōu)化策略可以參考我之前寫的:
http://www.itdecent.cn/p/d96906140199

6、程序容錯優(yōu)化方案
在這一塊我要先舉一個程序的例子說明一下什么才是容錯,先看程序:

//Service層:
public void insertOrderInfo(OrderInfo orderInfo) {
        try {
            OrderDao.insertOrderInfo(orderInfo);
        } catch (Exception e) {
            logger.error("訂單信息插入數(shù)據(jù)庫失敗! orderId:"+orderInfo.getOrderId(), e);
        }
    }

//DAO層
 public void insertOrderInfo(OrderInfo orderInfo) {
        try {
            this.sqlMapClient.insert("Order.insertOrderInfo", orderInfo)
        } catch (Exception e) {}
    }

注:
那么如果service層的方法調(diào)用dao層的方法,一旦數(shù)據(jù)插入失敗,那么這種異常處理的方式是容錯嗎?
把異常給吃掉了,在service層調(diào)用的時候,雖然沒有打印報錯信息,但是這能是容錯嗎?
所謂容錯是指在故障存在的情況下計算機系統(tǒng)不失效,仍然能夠正常工作的特性。

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

Paste_Image.png

這是一個最簡單的圖,應(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ù)后都可以同步更新本地緩存,當單點的Redis掛掉后,應(yīng)用程序至少還能從本地讀取信息而不至于服務(wù)瞬間掛掉。有的緩存對時效性要求不高,允許有一定延遲,那么在這種情況下我采用的方案是,利用本地緩存和遠程緩存相結(jié)合的方式,如下圖所示:
方案一:

Paste_Image.png

這種方式通過應(yīng)用服務(wù)器的Ehcache定時輪詢Redis緩存服務(wù)器更同步更新本地緩存,缺點是因為每臺服務(wù)器定時Ehcache的時間不一樣,那么不同服務(wù)器刷新最新緩存的時間也不一樣,會產(chǎn)生數(shù)據(jù)不一致問題,對一致性要求不高可以使用。

方案二:

Paste_Image.png

通過引入了MQ隊列,使每臺應(yīng)用服務(wù)器的Ehcache同步偵聽MQ消息,這樣在一定程度上可以達到準同步更新數(shù)據(jù),通過MQ推送或者拉取的方式,但是因為不同服務(wù)器之間的網(wǎng)絡(luò)速度的原因,所以也不能完全達到強一致性?;诖嗽硎褂肸ookeeper等分布式協(xié)調(diào)通知組件也是如此。

7、部分項目拆分不徹底

  • 拆分前

    Paste_Image.png

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

  • 拆分后

    Paste_Image.png

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

8、因基礎(chǔ)平臺組件功能不完善導(dǎo)致性能下降
先看一段代碼:

public void purchase(PurchaseParam purchaseParam, long timeoutSencond) {
        Future<String> future = threadPool.submit(new TestRunnable(purchaseParam, testService));
        logger.info("超時時間="+timeoutSencond);
        if(timeoutSencond > 0){
            try {
                future.get(timeoutSencond, TimeUnit.SECONDS);
                logger.info("超時返回,超時時間="+timeoutSencond);
            } catch (InterruptedException e) {
                logger.info("",e);
            } catch (ExecutionException e) {
                logger.info("",e);
            } catch (TimeoutException e) {
                logger.info("",e);
            }
        }
    }

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

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

我相信在定位程序性能問題的時候,大家有很多種辦法,比如用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ù)器的性能,從而得到解決方案。示例如下:

輸入命令:pstack 30222

顯示如下:
Thread 9 (Thread 0x7f729adc1700 (LWP 30251)):
#0  0x00007f72a429b720 in sem_wait () from /lib64/libpthread.so.0
#1  0x0000000000ac5eb6 in Semaphore::down() ()
#2  0x0000000000ac5cac in Queue::get() ()
#3  0x00000000009a583f in DBManager::processUpdate(Queue*) ()
#4  0x00000000009a4bfb in dbUpdateThread(void*) ()
#5  0x00007f72a4295851 in start_thread () from /lib64/libpthread.so.0
#6  0x00007f72a459267d in clone () from /lib64/libc.so.6
Thread 1 (Thread 0x7f72a60ae7e0 (LWP 30222)):
#0  0x00007f72a4584c95 in _xstat () from /lib64/libc.so.6
#1  0x00007f72a45483e0 in __tzfile_read () from /lib64/libc.so.6
#2  0x00007f72a4547864 in tzset_internal () from /lib64/libc.so.6
#3  0x00007f72a4547b20 in tzset () from /lib64/libc.so.6
#4  0x00007f72a4546699 in timelocal () from /lib64/libc.so.6
#5  0x0000000000b0b08d in Achieve::GetRemainTime(AchieveTemplate*) ()
#6  0x0000000000b115ca in Achieve::update() ()
#7  0x0000000000a197ce in Player::update() ()
#8  0x0000000000b1b272 in PlayerMng::Tick() ()
#9  0x0000000000a73105 in GameServer::FrameTick(unsigned int) ()
#10 0x0000000000a6ff80 in GameServer::run() ()
#11 0x0000000000a773a1 in main ()

輸入命令:ps  -eLo pid,lwp,pcpu | grep 30222
顯示如下:

30222 30222 31.4
30222 30251  0.0
30222 30252  0.0
30222 30253  0.0

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


Paste_Image.png

10、關(guān)于索引的優(yōu)化

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

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

  • 索引不會包含有NULL值的列
    只要列中包含有NULL值都將不會被包含在索引中,復(fù)合索引中只要有一列含有NULL值,那么這一列對于此復(fù)合索引就是無效的。所以我們在數(shù)據(jù)庫設(shè)計時不要讓字段的默認值為NULL。

  • 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ù)字的索引和檢索要比對字符串的索引和檢索效率更高。

** 11、使用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要?。?br> 缺點是Protobuf的學(xué)習(xí)曲線比JSON大得多)

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

Paste_Image.png

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

** 12、關(guān)于長耗時方法的拆分**
我們拆分長耗時方法的一般技巧是:

  • 尋找業(yè)務(wù)的冗余點,代碼中有很多重復(fù)性的代碼,可以適當簡化。
  • 檢查庫表索引是否合理加入。
  • 利用單元測試或者壓力測試長耗時的操作進行算法級別優(yōu)化,比如從庫中大批量讀取數(shù)據(jù),或者長時間循環(huán)操作,或者死循環(huán)操作等等。
  • 尋找業(yè)務(wù)的拆分點,根據(jù)業(yè)務(wù)需求拆分同步操作為異步,比如可以使用消息隊列或者多線程異步化。

經(jīng)過以上幾個分析后如果方法執(zhí)行時間仍然非常的長,這樣可能就是業(yè)務(wù)方面的需求使然,如下圖:


Paste_Image.png

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

Paste_Image.png

在接下來的第三篇文章中我們就介紹系統(tǒng)的降級,限流,還有監(jiān)控的一些方案。謝謝大家

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容