性能优化 = 改改代码?
大家好,我是Z哥。
好久沒(méi)寫(xiě)技術(shù)文章了,最近正好有進(jìn)行一些思考,順手寫(xiě)出來(lái)分享給大家。如果不是程序員的話,可以快速掃一眼正文的幾個(gè)小標(biāo)題,快速略過(guò)即可,畢竟思路和專業(yè)無(wú)關(guān),很多是相通的。(如果愿意的話,順手點(diǎn)個(gè)在看:D)
好了,正文開(kāi)始。
上了一定規(guī)模的系統(tǒng),特別是To C的系統(tǒng),性能優(yōu)化或多或少都會(huì)被逼著去做一下。否則,系統(tǒng)便無(wú)法支撐業(yè)務(wù)的發(fā)展,技術(shù)成了拖后腿,不是引領(lǐng)業(yè)務(wù)了。
一旦線上出現(xiàn)了性能問(wèn)題,就會(huì)很棘手。因?yàn)樗蜆I(yè)務(wù)功能上的Bug不同,后者的分析和解決思路更清晰,只要日志記錄到位,沿著一條已知的業(yè)務(wù)邏輯線,很容易就能找到問(wèn)題根源。
而性能問(wèn)題就會(huì)復(fù)雜的多,導(dǎo)致的因素有很多,甚至?xí)嵌喾N因素共同作用下的結(jié)果。比如,代碼質(zhì)量低下、業(yè)務(wù)發(fā)展太快、架構(gòu)設(shè)計(jì)不合理等等。
而且一般情況下,性能問(wèn)題處理起來(lái)比較耗時(shí),涉及到的分析鏈路可能會(huì)很長(zhǎng),特別是自己小組之外的上下游系統(tǒng),很多人不愿意干,或者說(shuō)有心無(wú)力。最多采用一些臨時(shí)性的補(bǔ)救手段,碰碰運(yùn)氣。比如,擴(kuò)容增加機(jī)器、重啟大招、……。
有些臨時(shí)性的補(bǔ)救措施,有時(shí)候不但不能解決問(wèn)題,還會(huì)埋下新的隱患。
比如,從表象上看到某個(gè)程序因?yàn)榻o的資源不足導(dǎo)致產(chǎn)生性能問(wèn)題。臨時(shí)增加更多資源給它,可能從表面上看,問(wèn)題是解決了。但是實(shí)則可能是因?yàn)槌绦騼?nèi)部對(duì)資源的使用上存在不合理的地方,增加資源只是延緩問(wèn)題發(fā)作的時(shí)間,而且還可能會(huì)侵占其它程序的運(yùn)行資源。
為了避免陷入如此的窘境,我們應(yīng)當(dāng)盡量提前進(jìn)行性能優(yōu)化,未雨綢繆。甚至最好是將它作為一個(gè)周期性的工作來(lái)進(jìn)行。
接下去就來(lái)分享一下我對(duì)做性能優(yōu)化的思路。
/01 明確優(yōu)化目的/
很多人優(yōu)化優(yōu)化著慢慢變成了為了優(yōu)化而優(yōu)化,目的丟了,或者甚至一開(kāi)始就沒(méi)考慮過(guò)。如此會(huì)陷入到無(wú)意義的性能黑洞中,無(wú)法自拔,只是不斷追求更好看的性能指標(biāo)。
優(yōu)化的目的可以是增強(qiáng)用戶體驗(yàn),比如消除一些有明顯卡頓的頁(yè)面和操作。還可以是節(jié)省服務(wù)器帶寬流量、減少服務(wù)器壓力這些。無(wú)論如何,你需要有一個(gè)目的。
/02 定標(biāo)準(zhǔn),做到什么程度/
優(yōu)化這事是永無(wú)止境的,為了避免陷入到前面說(shuō)的無(wú)意義的性能黑洞中,我們最好能夠根據(jù)實(shí)際的業(yè)務(wù)情況定義出一個(gè)相對(duì)客觀的標(biāo)準(zhǔn),代表優(yōu)化到什么程度。
我自己慣用的標(biāo)準(zhǔn)是確保比預(yù)期高50%,如果條件允許則爭(zhēng)取到100%。
比如,根據(jù)當(dāng)下的性能指標(biāo)與業(yè)務(wù)量對(duì)比,發(fā)現(xiàn)最大并發(fā)數(shù)可能會(huì)超過(guò)當(dāng)前的2倍,那么此時(shí)優(yōu)化到爭(zhēng)取優(yōu)化提升3倍,至少保證能提升2.5倍,是一個(gè)比較合理的標(biāo)準(zhǔn)。
之前專門(mén)寫(xiě)過(guò)一篇關(guān)于容量預(yù)估的文章《做「容量預(yù)估」可沒(méi)有true和false》,可以在文末跳轉(zhuǎn)過(guò)去看下,這里就不展開(kāi)了。
/03 找到瓶頸點(diǎn)/
很多人做優(yōu)化的時(shí)候,逮著代碼就開(kāi)始改。的確,只要有一定的知識(shí)積累,很容易就能從代碼中發(fā)現(xiàn),寫(xiě)法A不如寫(xiě)法B這樣的代碼。
但其實(shí)大部分情況下,「流程上的優(yōu)化遠(yuǎn)勝于語(yǔ)法級(jí)別的優(yōu)化」。比如將每一個(gè)字符串拼接改成用StringBuilder來(lái)實(shí)現(xiàn),大多數(shù)情況下帶來(lái)的成果其實(shí)很小,甚至在某些情況下還不如不改。
所以,我們最好還是能夠借助一些客觀數(shù)據(jù),以獲得更多的運(yùn)行環(huán)境相關(guān)的信息,來(lái)找到整個(gè)“木桶”上最短的一塊“板”。如整個(gè)系統(tǒng)的總體架構(gòu)、服務(wù)器的信息等,便于定位到底性能的瓶頸點(diǎn)在哪。
「流程上的優(yōu)化遠(yuǎn)勝于語(yǔ)法級(jí)別的優(yōu)化」中的“流程”除了業(yè)務(wù)流程之外,還包括技術(shù)層面的流程,比如數(shù)據(jù)在網(wǎng)絡(luò)中的流轉(zhuǎn)過(guò)程。
/04 著手優(yōu)化/
最后才是著手優(yōu)化。
做優(yōu)化的時(shí)候需要避免兩個(gè)常見(jiàn)的誤區(qū)。
第一,不要過(guò)度追求應(yīng)用的單機(jī)性能,如果單機(jī)表現(xiàn)良好,還應(yīng)該從整體的角度去思考。
第二,不要過(guò)度追求單一維度上的極致優(yōu)化,比如過(guò)度追求 CPU 的性能而忽略了內(nèi)存方面的瓶頸。
正確的思路一般符合下面兩個(gè)方向。
第一,空間換性能。一個(gè)節(jié)點(diǎn)頂不住就多復(fù)制一個(gè)節(jié)點(diǎn)出來(lái),獨(dú)一份的數(shù)據(jù)導(dǎo)致資源競(jìng)爭(zhēng)得厲害,就多復(fù)制一份數(shù)據(jù)出來(lái)。
第二,距離換性能。數(shù)據(jù)從服務(wù)端經(jīng)過(guò)層層處理返回到客戶端覺(jué)得慢的話,那么能不能直接保存在客戶端,或者至少是離客戶端盡可能近的地方。
好了,思路清楚了,具體在做的時(shí)候我建議你根據(jù)下面小標(biāo)題的順序進(jìn)行。不管是主動(dòng)地性能優(yōu)化,還是被動(dòng)地排查性能問(wèn)題都一樣。
/01 應(yīng)用程序?qū)用?
不管你愿不愿意承認(rèn),現(xiàn)實(shí)中的大部分性能問(wèn)題皆是應(yīng)用程序自身部分的代碼導(dǎo)致的。
我們總是不太愿意承認(rèn)自己的錯(cuò)誤,我見(jiàn)過(guò)太多程序員總是習(xí)慣性的將問(wèn)題先歸結(jié)于硬件問(wèn)題,網(wǎng)絡(luò)問(wèn)題等等,然后最終排查下來(lái)的根源往往還是在coding的應(yīng)用程序上。
所以,我們更應(yīng)該先從應(yīng)用程序本身入手進(jìn)行分析。而且,應(yīng)用程序所處的位置更「上游」,可操作性更強(qiáng),讓我們可以有更多的手段進(jìn)行優(yōu)化。
01??緩存
首先,最常見(jiàn)的便是「緩存」,這是用空間換性能的經(jīng)典。
數(shù)據(jù)必然是存儲(chǔ)在非易失性的數(shù)據(jù)庫(kù)中的,但是一些會(huì)被高頻訪問(wèn)的數(shù)據(jù),將它從數(shù)據(jù)庫(kù)中復(fù)制一份,存儲(chǔ)在易失性的內(nèi)存上做緩存,可以大大提高被訪問(wèn)的性能。這個(gè)道理大家都懂,就不多說(shuō)了。
但是值得提醒的一點(diǎn)是,緩存數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)很重要,沒(méi)有一種數(shù)據(jù)結(jié)構(gòu)是萬(wàn)能的。需要更多的權(quán)衡,因?yàn)閿?shù)據(jù)結(jié)構(gòu)設(shè)計(jì)的越簡(jiǎn)單、單一,緩存數(shù)據(jù)的二次運(yùn)算就越多;反之,所有都存儲(chǔ)「結(jié)果數(shù)據(jù)」的話,需要冗余的數(shù)據(jù)量又過(guò)大(緩存數(shù)據(jù)更新還麻煩)。
還得提醒一點(diǎn),如果緩存的數(shù)據(jù)量不小,還得考慮增加一個(gè)緩存淘汰算法,否則緩存命中率不堪入目,白白浪費(fèi)大量?jī)?nèi)存資源。
之前的《分布式系統(tǒng)系列》中有幾篇緩存相關(guān)的聊了很多細(xì)節(jié),可以在文末跳過(guò)去查閱。
02??異步
舉個(gè)現(xiàn)實(shí)生活中的例子,如果你在手機(jī)上點(diǎn)了一杯奶茶,去店里拿的時(shí)候發(fā)現(xiàn)前面還有20個(gè)號(hào),你會(huì)在這干等半小時(shí)么?
我想大部分人都不會(huì)吧,寧愿去別的地方溜溜。異步就是通過(guò)避免“干等著”來(lái)提升性能的手段。
做異步主要是以下兩種方式,
通過(guò)線程進(jìn)行異步。這主要用于涉及到I/O的地方,像磁盤(pán)I/O和網(wǎng)絡(luò)I/O。一旦產(chǎn)生I/O其實(shí)就意味著背后的操作是由另外一個(gè)程序在進(jìn)行,此時(shí)CPU就不用空著了,讓它忙別的去吧。
通過(guò)中間件異步,比如MQ。這用于更大的場(chǎng)景里,比如在某些流程中、上下游系統(tǒng)的銜接中,如果有些結(jié)果并不需要實(shí)時(shí)收到,那么通過(guò)MQ進(jìn)行異步就可以大大提高性能。畢竟MQ的性能更接近NoSQL,性能自然比關(guān)系型數(shù)據(jù)庫(kù)高的多。更何況,還將一些業(yè)務(wù)邏輯的預(yù)算給滯后了,當(dāng)下的性能會(huì)更好。
03??多線程&分布式
這兩點(diǎn)都是「分治」思想的體現(xiàn)。一個(gè)快遞員送1000個(gè)包裹比較慢,那么讓10個(gè)快遞員同時(shí)各送100個(gè)自然就快了。
但是切勿分的太狠,畢竟,多起一個(gè)線程相當(dāng)于多一個(gè)放養(yǎng)的娃,放出去太多的話,管理成本很高,可能反而會(huì)更慢。這就是線程切換的成本,分布式系統(tǒng)中也存在類似的管理成本。
不過(guò),一個(gè)小建議送給你。不到迫不得已,能通過(guò)「單機(jī)多線程」應(yīng)付的,就不要引入分布式了。因?yàn)?#xff0c;網(wǎng)絡(luò)這個(gè)東西實(shí)在太不靠譜了,你得為它做大量的額外工作。
04??延后運(yùn)算
這個(gè)和緩存的思路相反,將一些運(yùn)算盡可能的延后到用的時(shí)候。適用的場(chǎng)景也和緩存相反,適用于一些低頻的、運(yùn)算耗時(shí)的數(shù)據(jù)上。
延遲加載、插件化等等就是該思想的體現(xiàn)。
05??批量,合并
如果你需要在短時(shí)間內(nèi)頻繁的傳遞多個(gè)數(shù)據(jù)給同一個(gè)目的地,那么盡量考慮將他們打包到一起,一次性傳輸,特別是涉及到I/O的場(chǎng)景。
如果手頭的系統(tǒng)還是一個(gè)單點(diǎn)系統(tǒng),這招的性價(jià)比就非常高。在避開(kāi)分布式系統(tǒng)的復(fù)雜性的前提下,獲得性能提升。
數(shù)據(jù)庫(kù)的bulk操作,前端的sprite圖,都是該思想的體現(xiàn)。
應(yīng)用程序?qū)用娴钠渌鼉?yōu)化方式還有很多。比如,用長(zhǎng)鏈接代替頻繁打開(kāi)關(guān)閉的短鏈接、壓縮、重用等等。這些相對(duì)比較簡(jiǎn)單和好理解,就不多說(shuō)了。
應(yīng)用程序?qū)用娴氖虑樽龅轿涣酥?#xff0c;我們?cè)賮?lái)考慮組件層面的優(yōu)化。
/02 組件層面/
組件是指那些非業(yè)務(wù)性的東西,比如一些中間件、數(shù)據(jù)庫(kù)、運(yùn)行時(shí)的環(huán)境(JVM、WebServer)等。
數(shù)據(jù)庫(kù)的調(diào)優(yōu),總的來(lái)說(shuō)分為以下三部分:
SQL語(yǔ)句。
索引。
連接池。
其它的一些,比如JVM的調(diào)優(yōu)最主要的就是對(duì)「GC」相關(guān)的配置調(diào)優(yōu)。WebServer的調(diào)優(yōu)主要是針對(duì)「連接」相關(guān)的調(diào)優(yōu)。這些細(xì)節(jié)就不贅述了,資料多到看不過(guò)來(lái)。
/03 系統(tǒng)層面/
系統(tǒng)層面的一些調(diào)優(yōu)工作,涉及到運(yùn)維工程師的一些工作,我不是很擅長(zhǎng)就不誤人子弟了。但是我們可以借助系統(tǒng)層面的一些技術(shù)指標(biāo)來(lái)觀測(cè)并判斷我們的程序是否正常。比如,CPU、線程、網(wǎng)絡(luò)、磁盤(pán)、內(nèi)存。
01??CPU
判斷CPU是否正常,大多數(shù)情況下關(guān)注這三個(gè)指標(biāo)就夠了,CPU利用率、CPU平均負(fù)載、CPU上下文切換。CPU利用率大家基本上都知道,就不多說(shuō)了,那就說(shuō)說(shuō)后面兩個(gè)。
關(guān)注CPU平均負(fù)載的時(shí)候,特別需要注意趨勢(shì)的變化。如果 1 分鐘/5 分鐘/15 分鐘的三個(gè)值相差不大,那說(shuō)明系統(tǒng)負(fù)載很平穩(wěn),則不用關(guān)注,如果這三個(gè)值逐漸降低,說(shuō)明負(fù)載在漸漸升高,需要排查具體的原因。
CPU上下文切換。上下文切換的次數(shù)越多,就意味著更多的CPU時(shí)間消耗在寄存器、內(nèi)核棧以及虛擬內(nèi)存等數(shù)據(jù)的保存和恢復(fù)上,真正進(jìn)行你所期望的運(yùn)算工作的時(shí)間就越少,系統(tǒng)的整體性能自然就會(huì)下降。導(dǎo)致這個(gè)情況的原因主要有兩點(diǎn),
程序內(nèi)的磁盤(pán)I/O、網(wǎng)絡(luò)I/O比較多。
程序內(nèi)啟動(dòng)的線程過(guò)多。
02??線程
線程方面除了關(guān)注線程數(shù)之外,還需要關(guān)注一下處于「掛起」?fàn)顟B(tài)的線程數(shù)量有多少。
掛起狀態(tài)的線程數(shù)過(guò)多,意味著程序里鎖競(jìng)爭(zhēng)激烈,需要考慮通過(guò)其它的方案來(lái)縮小鎖的粒度、級(jí)別,甚至是避免用鎖。
03??網(wǎng)絡(luò)
通常在硬件層面內(nèi)網(wǎng)帶寬會(huì)遠(yuǎn)大于外網(wǎng)的帶寬,所以,外網(wǎng)帶寬被吃滿的情況更加常見(jiàn),特別是多圖、多流媒體類型的可對(duì)外訪問(wèn)系統(tǒng)。關(guān)于流量大小相關(guān)的問(wèn)題一般大家都能想到,就不多說(shuō)了。
但是,Z哥提醒你要特別關(guān)注端口的使用和每個(gè)端口上的連接狀態(tài)情況。比較常見(jiàn)的問(wèn)題是,連接用完有沒(méi)有及時(shí)釋放,導(dǎo)致端口被占滿,后續(xù)新的網(wǎng)絡(luò)請(qǐng)求無(wú)法建立連接通道。(可以通過(guò)netstat、ss獲取網(wǎng)絡(luò)相關(guān)的信息。)
?
04??磁盤(pán)
除非是規(guī)模非常大的系統(tǒng),否則一般情況下,從磁盤(pán)的指標(biāo)上看不出啥問(wèn)題。
平時(shí)看的時(shí)候,除了看看利用率、吞吐量和請(qǐng)求數(shù)量之外,有兩個(gè)容易被忽略的點(diǎn)可以多關(guān)注下。
第一點(diǎn),如果I/O利用率很高,但是吞吐量很小,則意味著存在較多的磁盤(pán)隨機(jī)讀寫(xiě),最好把隨機(jī)讀寫(xiě)優(yōu)化成順序讀寫(xiě)。(可以通過(guò) strace 或者 blktrace 觀察 I/O 是否連續(xù)判斷是否是順序的讀寫(xiě)行為)
其次,如果I/O等待隊(duì)列的長(zhǎng)度比較大,則該磁盤(pán)存在 I/O 性能問(wèn)題。一般來(lái)說(shuō),如果隊(duì)列長(zhǎng)度持續(xù)超過(guò)2就可以這么認(rèn)為。
05??內(nèi)存
關(guān)注內(nèi)存的時(shí)候除了內(nèi)存消耗之外,有一個(gè)Swap換入和換出的內(nèi)存大小需要特別注意一下。因?yàn)镾wap需要讀寫(xiě)磁盤(pán),所以性能不是很高。如果GC的時(shí)候遍歷到的對(duì)象恰巧被Swap 出去了,便會(huì)有磁盤(pán)I/O產(chǎn)生,性能自然會(huì)下降。所以這個(gè)指標(biāo)不應(yīng)該太高。
大多數(shù)內(nèi)存問(wèn)題,都和對(duì)象常駐內(nèi)存不及時(shí)釋放有關(guān),有很多工具可以觀察對(duì)象的內(nèi)存分配情況。如,jmap、VisualVM、heap dump等。
如果你的程序部署在linux系統(tǒng)上的話,不得不錯(cuò)過(guò)Brendan Gregg的大神整理的精華。下面就引用一張圖,給大家感受一下,具體可以去 http://www.brendangregg.com/linuxperf.html 自行查閱更多相關(guān)的內(nèi)容。
▲圖片來(lái)自于brendangregg.com
最后,雖然性能優(yōu)化是一件大家都知道的好事,但是再好的事做起來(lái)都有成本。所以,如非必要,不要過(guò)早、過(guò)度進(jìn)行性能優(yōu)化哦。
好了,總結(jié)一下。
這篇呢,Z哥和你聊了一下非常讓程序員們頭疼的程序性能問(wèn)題。想要避免受這個(gè)問(wèn)題困擾的前提是事前做好性能優(yōu)化工作。
做性能優(yōu)化不能走一步算一步。事先需要做三件事「明確優(yōu)化目的」、「定標(biāo)準(zhǔn)」、「找到瓶頸點(diǎn)」。
具體做優(yōu)化的時(shí)候建議從應(yīng)用程序?qū)用骈_(kāi)始,再到組件層面,最后才是系統(tǒng)層面,從上往下,層層深入。順帶分享了每個(gè)層面的常用一些方法和思路。
希望對(duì)你有所啟發(fā)。
在一個(gè)大系統(tǒng)中,數(shù)據(jù)就像水,整個(gè)系統(tǒng)就像是一個(gè)漏斗,漏斗的每一層代表每個(gè)子程序。上層的子程序?qū)π阅艿膿p耗越低,能流下去的水就越多,直到最后一層「數(shù)據(jù)庫(kù)」處,也可以理解為是存儲(chǔ)。
所以,趕緊行動(dòng)起來(lái),開(kāi)啟保衛(wèi)數(shù)據(jù)庫(kù)之戰(zhàn)吧。
推薦閱讀:
8個(gè)月打磨,一份送給程序員的「分布式系統(tǒng)」合集
做「容量預(yù)估」可沒(méi)有true和false
原創(chuàng)不易,如果你覺(jué)得這篇文章還不錯(cuò),就「在看」或者「分享」一下吧。鼓勵(lì)我的創(chuàng)作 :)
如果你有關(guān)于軟件架構(gòu)、分布式系統(tǒng)、產(chǎn)品、運(yùn)營(yíng)的困惑
可以試試點(diǎn)擊「閱讀原文」
總結(jié)
以上是生活随笔為你收集整理的性能优化 = 改改代码?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Kubernetes,多云和低代码数据科
- 下一篇: 从TimeSpan说起