本文是“論代碼級(jí)性能優(yōu)化變遷之路一”(http://www.reibang.com/p/c4a748002e66) 的第二篇。
在上一篇我們主要介紹了所遇到問題的五點(diǎn)厘灼,那么今天接下來討論剩下的問題,我們先再回顧一下之前討論的問題:
1脸候、單臺(tái)40TPS婿脸,加到4臺(tái)服務(wù)器能到60TPS,擴(kuò)展性幾乎沒有烛占。
2、在實(shí)際生產(chǎn)環(huán)境中,經(jīng)常出現(xiàn)數(shù)據(jù)庫死鎖導(dǎo)致整個(gè)服務(wù)中斷不可用忆家。
3犹菇、數(shù)據(jù)庫事務(wù)亂用,導(dǎo)致事務(wù)占用時(shí)間太長芽卿。
4揭芍、在實(shí)際生產(chǎn)環(huán)境中,服務(wù)器經(jīng)常出現(xiàn)內(nèi)存溢出和CPU時(shí)間被占滿卸例。
5称杨、程序開發(fā)的過程中,考慮不全面筷转,容錯(cuò)很差姑原,經(jīng)常因?yàn)橐粋€(gè)小bug而導(dǎo)致服務(wù)不可用。
6呜舒、程序中沒有打印關(guān)鍵日志锭汛,或者打印了日志,信息卻是無用信息沒有任何參考價(jià)值袭蝗。
7店乐、配置信息和變動(dòng)不大的信息依然會(huì)從數(shù)據(jù)庫中頻繁讀取,導(dǎo)致數(shù)據(jù)庫IO很大呻袭。
8、項(xiàng)目拆分不徹底腺兴,一個(gè)tomcat中會(huì)布署多個(gè)項(xiàng)目WAR包左电。
9、因?yàn)榛A(chǔ)平臺(tái)的bug页响,或者功能缺陷導(dǎo)致程序可用性降低篓足。
10、程序接口中沒有限流策略闰蚕,導(dǎo)致很多vip商戶直接拿我們的生產(chǎn)環(huán)境進(jìn)行壓測(cè)栈拖,直接影響真正的服務(wù)可用性。
11没陡、沒有故障降級(jí)策略涩哟,項(xiàng)目出了問題后解決的時(shí)間較長,或者直接粗暴的回滾項(xiàng)目盼玄,但是不一定能解決問題贴彼。
12、沒有合適的監(jiān)控系統(tǒng)埃儿,不能準(zhǔn)實(shí)時(shí)或者提前發(fā)現(xiàn)項(xiàng)目瓶頸器仗。
四、優(yōu)化解決方案
5、緩存優(yōu)化方案
針對(duì)配置信息和變動(dòng)不大的信息可以放到緩存中精钮,提高并發(fā)能力也能夠降低IO緩存威鹿,具體緩存優(yōu)化策略可以參考我之前寫的:
http://www.reibang.com/p/d96906140199
6、程序容錯(cuò)優(yōu)化方案
在這一塊我要先舉一個(gè)程序的例子說明一下什么才是容錯(cuò)轨香,先看程序:
//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ù)插入失敗,那么這種異常處理的方式是容錯(cuò)嗎弹沽?
把異常給吃掉了檀夹,在service層調(diào)用的時(shí)候,雖然沒有打印報(bào)錯(cuò)信息策橘,但是這能是容錯(cuò)嗎炸渡?
所謂容錯(cuò)是指在故障存在的情況下計(jì)算機(jī)系統(tǒng)不失效,仍然能夠正常工作的特性丽已。
我們拿使用緩存來作為一個(gè)案例講解蚌堵,先看一個(gè)圖:
這是一個(gè)最簡(jiǎn)單的圖,應(yīng)用服務(wù)定期從redis中獲取配置信息沛婴,可能會(huì)有朋友認(rèn)為這樣已經(jīng)很穩(wěn)定了吼畏,但是如果Redis出現(xiàn)問題呢?可能會(huì)有朋友說嘁灯,Redis會(huì)是集群泻蚊,分片或者主從,確保不會(huì)出現(xiàn)問題丑婿。其實(shí)我是這樣的認(rèn)為的性雄,雖然應(yīng)用服務(wù)程序盡量的保持輕量級(jí)是不錯(cuò)的,但是不能因此而把希望全部寄托在中間組件上面羹奉,換句話說秒旋,如果此時(shí)的Redis是單點(diǎn),那么后果會(huì)是什么樣的诀拭,那么隨著大量的并發(fā)請(qǐng)求到來的時(shí)候迁筛,程序中會(huì)報(bào)大量的錯(cuò)誤,同時(shí)正常的流程也不能進(jìn)行下去了業(yè)務(wù)也可能由此而中斷耕挨。
那么在此種場(chǎng)景下我的解決方案是细卧,要把緩存的使用分級(jí)別,有的緩存同步要求時(shí)效性非常高筒占,比如支付限額配置酒甸,在后臺(tái)修改完成以后前臺(tái)立刻就能夠獲得感知,并且能夠成功切換赋铝,這種情況只能實(shí)時(shí)的從Redis中獲取最新數(shù)據(jù)插勤,但是每次獲取完最新的數(shù)據(jù)后都可以同步更新本地緩存,當(dāng)單點(diǎn)的Redis掛掉后,應(yīng)用程序至少還能從本地讀取信息而不至于服務(wù)瞬間掛掉农尖。有的緩存對(duì)時(shí)效性要求不高析恋,允許有一定延遲,那么在這種情況下我采用的方案是盛卡,利用本地緩存和遠(yuǎn)程緩存相結(jié)合的方式助隧,如下圖所示:
方案一:
這種方式通過應(yīng)用服務(wù)器的Ehcache定時(shí)輪詢Redis緩存服務(wù)器更同步更新本地緩存,缺點(diǎn)是因?yàn)槊颗_(tái)服務(wù)器定時(shí)Ehcache的時(shí)間不一樣滑沧,那么不同服務(wù)器刷新最新緩存的時(shí)間也不一樣并村,會(huì)產(chǎn)生數(shù)據(jù)不一致問題,對(duì)一致性要求不高可以使用滓技。
方案二:
通過引入了MQ隊(duì)列哩牍,使每臺(tái)應(yīng)用服務(wù)器的Ehcache同步偵聽MQ消息,這樣在一定程度上可以達(dá)到準(zhǔn)同步更新數(shù)據(jù)令漂,通過MQ推送或者拉取的方式膝昆,但是因?yàn)椴煌?wù)器之間的網(wǎng)絡(luò)速度的原因,所以也不能完全達(dá)到強(qiáng)一致性叠必〖苑酰基于此原理使用Zookeeper等分布式協(xié)調(diào)通知組件也是如此。
7纬朝、部分項(xiàng)目拆分不徹底
-
拆分前
注:
一個(gè)Tomcat中布署多個(gè)應(yīng)用war包收叶,彼此之間互相牽制在并發(fā)量非常大的情況下性能降低非常明顯。 -
拆分后
注:
拆分前的這種情況其實(shí)還是挺普遍共苛,之前我一直認(rèn)為項(xiàng)目中不會(huì)存在這種情況但是事實(shí)上還是存在了判没。解決的方法很簡(jiǎn)單,每一個(gè)應(yīng)用war只布在一個(gè)tomcat中俄讹,這樣應(yīng)用程序之間就不會(huì)存在資源和連接數(shù)的競(jìng)爭(zhēng)情況,性能和并發(fā)能力提交較為明顯绕德。
8患膛、因基礎(chǔ)平臺(tái)組件功能不完善導(dǎo)致性能下降
先看一段代碼:
public void purchase(PurchaseParam purchaseParam, long timeoutSencond) {
Future<String> future = threadPool.submit(new TestRunnable(purchaseParam, testService));
logger.info("超時(shí)時(shí)間="+timeoutSencond);
if(timeoutSencond > 0){
try {
future.get(timeoutSencond, TimeUnit.SECONDS);
logger.info("超時(shí)返回,超時(shí)時(shí)間="+timeoutSencond);
} catch (InterruptedException e) {
logger.info("",e);
} catch (ExecutionException e) {
logger.info("",e);
} catch (TimeoutException e) {
logger.info("",e);
}
}
}
注:
首先我們先不說這段代碼的格式如何如何耻蛇,先看功能實(shí)現(xiàn)踪蹬,使用Future來做超時(shí)控制,這是為何呢臣咖?原因其實(shí)是在我們調(diào)用的Dubbo接口上面跃捣,因?yàn)槭荄ubbo已經(jīng)經(jīng)過二次封裝,結(jié)果把自帶的timeout給淹沫了夺蛇,程序員只能通過這種方式來控制超時(shí)疚漆,可以看到這種用法非常差勁,對(duì)程序性能造成一定的影響。
9娶聘、如何快速定位程序性能瓶頸
我相信在定位程序性能問題的時(shí)候闻镶,大家有很多種辦法,比如用jdk自帶的命令丸升,如Jcmd铆农,Jstack,jmap狡耻,jhat墩剖,jstat,iostat夷狰,vmstat等等命令岭皂,還可以用VisualVM,MAT孵淘,JRockit等可視化工具蒲障,我今天想說的是利用一個(gè)最簡(jiǎn)單的命令就能夠定位到哪段程序可能存在性能問題,請(qǐng)看下面介紹:
一般我們會(huì)通過top命令查看各個(gè)進(jìn)程的cpu和內(nèi)存占用情況瘫证,獲得到了我們的進(jìn)程id揉阎,然后我們將會(huì)通過pstack命令查看里邊的各個(gè)線程id以及對(duì)應(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這個(gè)線程產(chǎn)生了性能問題,執(zhí)行時(shí)間長達(dá)31.4毫秒的時(shí)間毡庆,再觀察無非就是下面的幾個(gè)語句出現(xiàn)的問題坑赡,只需要簡(jiǎn)單排查就知道了問題瓶頸。
10么抗、關(guān)于索引的優(yōu)化
組合索引的原則是偏左原則毅否,所以在使用的時(shí)候需要多加注意
索引的數(shù)量不需要過多的添加,在添加的時(shí)候要考慮聚集索引和輔助索引蝇刀,這二者的性能是有區(qū)別的
索引不會(huì)包含有NULL值的列
只要列中包含有NULL值都將不會(huì)被包含在索引中螟加,復(fù)合索引中只要有一列含有NULL值,那么這一列對(duì)于此復(fù)合索引就是無效的吞琐。所以我們?cè)跀?shù)據(jù)庫設(shè)計(jì)時(shí)不要讓字段的默認(rèn)值為NULL捆探。MySQL索引排序
MySQL查詢只使用一個(gè)索引,因此如果where子句中已經(jīng)使用了索引的話站粟,那么order by中的列是不會(huì)使用索引的黍图。因此數(shù)據(jù)庫默認(rèn)排序可以符合要求的情況下不要使用排序操作;盡量不要包含多個(gè)列的排序奴烙,如果需要最好給這些列創(chuàng)建復(fù)合索引助被。使用索引的注意事項(xiàng)
以下操作符可以應(yīng)用索引:
大于等于
Between
IN
LIKE 不以%開頭
以下操作符不能應(yīng)用索引:
NOT IN
LIKE %_開頭
-
索引技巧
同樣是1234567890剖张,數(shù)值類型存儲(chǔ)遠(yuǎn)比字符串節(jié)約存儲(chǔ)空間。
節(jié)約存儲(chǔ)就是節(jié)約IO恰起,減少IO就是提升性能
通常對(duì)數(shù)字的索引和檢索要比對(duì)字符串的索引和檢索效率更高修械。
** 11、使用Redis需要注意的一些點(diǎn)**
在增加key的時(shí)候盡量設(shè)置過期時(shí)間检盼,不然Redis Server的內(nèi)存使用會(huì)達(dá)到
系統(tǒng)物理內(nèi)存的最大值肯污,導(dǎo)致Redis使用VM降低系統(tǒng)性能Redis Key設(shè)計(jì)時(shí)應(yīng)該盡可能短,Value盡量不要使用復(fù)雜對(duì)象。
將對(duì)象轉(zhuǎn)換成JSON對(duì)象(利用現(xiàn)成的JSON庫)后存入Redis吨枉,
將對(duì)象轉(zhuǎn)換成Google開源二進(jìn)制協(xié)議對(duì)象(Google Protobuf蹦渣,和JSON數(shù)據(jù)
格式類似,但是因?yàn)槭嵌M(jìn)制表現(xiàn)貌亭,所以性能效率以及空間占用都比JSON要屑砦ā;
缺點(diǎn)是Protobuf的學(xué)習(xí)曲線比JSON大得多)Redis使用完以后一定要釋放連接圃庭,如下圖示例:
不管是返回到連接池中還是直接釋放掉锄奢,總之就是要將連接還回去。
** 12剧腻、關(guān)于長耗時(shí)方法的拆分**
我們拆分長耗時(shí)方法的一般技巧是:
- 尋找業(yè)務(wù)的冗余點(diǎn)拘央,代碼中有很多重復(fù)性的代碼,可以適當(dāng)簡(jiǎn)化书在。
- 檢查庫表索引是否合理加入灰伟。
- 利用單元測(cè)試或者壓力測(cè)試長耗時(shí)的操作進(jìn)行算法級(jí)別優(yōu)化,比如從庫中大批量讀取數(shù)據(jù)儒旬,或者長時(shí)間循環(huán)操作栏账,或者死循環(huán)操作等等。
- 尋找業(yè)務(wù)的拆分點(diǎn)栈源,根據(jù)業(yè)務(wù)需求拆分同步操作為異步挡爵,比如可以使用消息隊(duì)列或者多線程異步化。
經(jīng)過以上幾個(gè)分析后如果方法執(zhí)行時(shí)間仍然非常的長甚垦,這樣可能就是業(yè)務(wù)方面的需求使然茶鹃,如下圖:
那么我們是否可以考慮將一個(gè)長耗時(shí)方法進(jìn)行拆分,拆分為多個(gè)短耗時(shí)方法由發(fā)起端分別調(diào)用制轰,這樣在高并發(fā)的情況下不會(huì)造成某一個(gè)方法的長時(shí)間阻塞前计,在一定程度上能夠提高并發(fā)能力胞谭,如下圖:
在接下來的第三篇文章中我們就介紹系統(tǒng)的降級(jí)垃杖,限流,還有監(jiān)控的一些方案丈屹。謝謝大家