性能之巔:定位和優(yōu)化程序CPU、內(nèi)存、IO瓶頸
一、思維導(dǎo)圖

二、什么是性能優(yōu)化?
性能優(yōu)化指在不影響系統(tǒng)運(yùn)行正確性的前提下,使之運(yùn)行得更快,完成特定功能所需的時(shí)間更短,或擁有更強(qiáng)大的服務(wù)能力。
關(guān)注
不同程序有不同的性能關(guān)注點(diǎn),比如科學(xué)計(jì)算關(guān)注運(yùn)算速度,游戲引擎注重渲染效率,而服務(wù)程序追求吞吐能力。
服務(wù)器一般都是可水平擴(kuò)展的分布式系統(tǒng),系統(tǒng)處理能力取決于單機(jī)負(fù)載能力和水平擴(kuò)展能力,所以,提升單機(jī)性能和提升水平擴(kuò)展能力是兩個(gè)主要方向,理論上系統(tǒng)水平方向可以無限擴(kuò)展,但水平擴(kuò)展后往往導(dǎo)致通信成本飆升(甚至瓶頸),同時(shí)面臨單機(jī)處理能力下降的問題。
指標(biāo)
衡量單機(jī)性能有很多指標(biāo),比如:QPS(Query Per Second)、TPS、OPS、IOPS、最大連接數(shù)、并發(fā)數(shù)等評估吞吐的指標(biāo)。
CPU為了提高吞吐,會把指令執(zhí)行分為多個(gè)階段,會搞指令Pipeline,同樣,軟件系統(tǒng)為了提升處理能力,往往會引入批處理(攢包),跟CPU流水線會引起指令執(zhí)行Latency增加一樣,伴隨著系統(tǒng)負(fù)載增加也會導(dǎo)致延遲(Latency)增加,可見,系統(tǒng)吞吐和延遲是兩個(gè)沖突的目標(biāo)。
顯然,過高的延遲是不能接受的,所以,服務(wù)器性能優(yōu)化的目標(biāo)往往變成:追求可容忍延遲(Latency)下的最大吞吐(Throughput)。
延遲(也叫響應(yīng)時(shí)間:RT)不是固定的,通常在一個(gè)范圍內(nèi)波動(dòng),我們可以用平均時(shí)延去評估系統(tǒng)性能,但有時(shí)候,平均時(shí)延是不夠的,這很容易理解,比如80%的請求都在10毫秒以內(nèi)得到響應(yīng),但20%的請求時(shí)延超過2秒,而這20%的高延遲可能會引發(fā)投訴,同樣不可接受。
一個(gè)改進(jìn)措施是使用TP90、TP99之類的指標(biāo),它不是取平均,而是需確保排序后90%、99%請求滿足時(shí)延的要求。
通常,執(zhí)行效率(CPU)是我們的重點(diǎn)關(guān)注,但有時(shí)候,我們也需要關(guān)注內(nèi)存占用、網(wǎng)絡(luò)帶寬、磁盤IO等,影響性能的因素很多,它是一個(gè)復(fù)雜而有趣的問題。
三、基礎(chǔ)知識
能編寫運(yùn)行正確的程序不一定能做性能優(yōu)化,性能優(yōu)化有更高的要求,這樣講并不是想要嚇阻想做性能優(yōu)化的工程師,而是實(shí)事求是講,性能優(yōu)化既需要扎實(shí)的系統(tǒng)知識,又需要豐富的實(shí)踐經(jīng)驗(yàn),只有這樣,你才能具備case by case分析問題解決問題的能力。
所以,相比直接給出結(jié)論,我更愿意多花些篇幅講一些基礎(chǔ)知識,我堅(jiān)持認(rèn)為底層基礎(chǔ)是理解并掌握性能優(yōu)化技能的前提,值得花費(fèi)一些時(shí)間研究并掌握這些根技術(shù)。
CPU架構(gòu)
你需要了解CPU架構(gòu),理解運(yùn)算單元、記憶單元、控制單元是如何既各司其職又相互配合完成工作的。
你需要了解CPU如何讀取數(shù)據(jù),CPU如何執(zhí)行任務(wù)。
你需要了解數(shù)據(jù)總線,地址總線和控制總線的區(qū)別和作用。
你需要了解指令周期:取指、譯指、執(zhí)行、寫回。
你需要了解CPU Pipeline,超標(biāo)量流水線,亂序執(zhí)行。
你需要了解多CPU、多核心、邏輯核、超線程、多線程、協(xié)程這些概念。
存儲金字塔
CPU的速度和訪存速度相差200倍,高速緩存是跨越這個(gè)鴻溝的橋梁,你需要理解存儲金字塔,而這個(gè)層次結(jié)構(gòu)思維基于著一個(gè)稱為局部性原理(principle of locality)的思想,它對軟硬件系統(tǒng)的設(shè)計(jì)和性能有著極大的影響。
局部性又分為時(shí)間局部性和空間局部性。
緩存
現(xiàn)代計(jì)算機(jī)系統(tǒng)一般有L1-L2-L3三級緩存。
比如在我的系統(tǒng),我通過進(jìn)入?/sys/devices/system/cpu/cpu0/cache/index0?1 2 3目錄下查看。
size對應(yīng)大小、type對應(yīng)類型、coherency_line_size對應(yīng)cache line大小。
每個(gè)CPU核心有獨(dú)立的L1、L2高速緩存,所以L1和L2是on-chip緩存;L3是多個(gè)CPU核心共享的,它是off-chip緩存。
L1緩存又分為i-cache(指令緩存)和d-cache(數(shù)據(jù)緩存),L1緩存通常只有32K/64KB,速度高達(dá)4 cycles。
L2緩存能到256KB,速度在8 cycles左右。
L3則高達(dá)30MB,速度32 cycles左右。
而內(nèi)存高達(dá)數(shù)G,訪存時(shí)延則在200 cycles左右。
所以CPU->寄存器->L1->L2->L3->內(nèi)存->磁盤構(gòu)成存儲層級結(jié)構(gòu):越靠近CPU,存儲容量越小、速度越快、單位成本越高,越遠(yuǎn)離CPU,存儲容量越大、速度越慢、單位成本越低。
虛擬存儲器(VM)
進(jìn)程和虛擬地址空間是操作系統(tǒng)的2個(gè)核心抽象。
系統(tǒng)中的所有進(jìn)程共享CPU和主存資源,虛擬存儲是對主存的抽象,它為每個(gè)進(jìn)程提供一個(gè)大的、一致的、私有的地址空間,我們gdb調(diào)試的時(shí)候,打印出來的變量地址是虛擬地址。
操作系統(tǒng)+CPU硬件(MMU)緊密合作完成虛擬地址到物理地址的翻譯(映射),這個(gè)過程總是沉默的自動(dòng)的進(jìn)行,不需要應(yīng)用程序員的任何干預(yù)。
每個(gè)進(jìn)程有一個(gè)單獨(dú)的頁表(Page Table),頁表是一個(gè)頁表?xiàng)l目(PTE)的數(shù)組,該表的內(nèi)容由操作系統(tǒng)管理,虛擬地址空間中的每個(gè)頁(4K或者8K)通過查找頁表找到物理地址,頁表往往是層級式的,多級頁表減少了頁表對存儲的需求,命失(Page Fault)將導(dǎo)致頁面調(diào)度(Swapping或者Paging),這個(gè)懲罰很重,所以,我們要改善程序的行為,讓它有更好的局部性,如果一段時(shí)間內(nèi)訪存的地址過于發(fā)散,將導(dǎo)致顛簸(Thrashing),從而嚴(yán)重影響程序性能。
為了加速地址翻譯,MMU中增加了一個(gè)關(guān)于PTE的小的緩存,叫翻譯后備緩沖器(TLB),地址翻譯單元做地址翻譯的時(shí)候,會先查詢TLB,只有TLB命失才會查詢高速緩存(L1-2-3)。
匯編基礎(chǔ)
雖然寫匯編的場景越來越少,但讀懂匯編依然很有必要,理解高級語言的程序是怎么轉(zhuǎn)化為匯編語言有助于我們編寫高質(zhì)量高性能的代碼。
對于匯編,至少需要了解幾種尋址模式,了解數(shù)據(jù)操作、分支、傳送、控制跳轉(zhuǎn)指令。
理解C語言的if else、while/do while/for、switch case、函數(shù)調(diào)用是怎么翻譯成匯編代碼。
理解ebp+esp寄存器在函數(shù)調(diào)用過程中是如何構(gòu)建和撤銷棧幀的。
理解函數(shù)參數(shù)和返回值是怎么傳遞的。
異常和系統(tǒng)調(diào)用
異常會導(dǎo)致控制流突變,異常控制流發(fā)生在計(jì)算機(jī)系統(tǒng)的各個(gè)層次,異常可以分為四類:
中斷(interrupt):中斷是異步發(fā)生的,來自處理器外部IO設(shè)備信號,中斷處理程序分上下部。
陷阱(trap):陷阱是有意的異常,是執(zhí)行一條指令的結(jié)果,系統(tǒng)調(diào)用是通過陷阱實(shí)現(xiàn)的,陷阱在用戶程序和內(nèi)核之間提供一個(gè)像過程調(diào)用一樣的接口:系統(tǒng)調(diào)用。
故障(fault):故障由錯(cuò)誤情況引起,它有可能被故障處理程序修復(fù),故障發(fā)生,處理器將控制轉(zhuǎn)移到故障處理程序,缺頁(Page Fault)是經(jīng)典的故障實(shí)例。
終止(abort):終止是不可恢復(fù)的致命錯(cuò)誤導(dǎo)致的結(jié)果,通常是硬件錯(cuò)誤,會終止程序的執(zhí)行。
系統(tǒng)調(diào)用:
內(nèi)核態(tài)和用戶態(tài)
你需要了解操作系統(tǒng)的一些概念,比如內(nèi)核態(tài)和用戶態(tài),應(yīng)用程序在用戶態(tài)運(yùn)行我們編寫的邏輯,一旦調(diào)用系統(tǒng)調(diào)用,便會通過一個(gè)特定的陷阱陷入內(nèi)核,通過系統(tǒng)調(diào)用號標(biāo)識功能,不同于普通函數(shù)調(diào)用,陷入內(nèi)核態(tài)和從內(nèi)核態(tài)返回需要做上下文切換,需要做環(huán)境變量的保存和恢復(fù)工作,它會帶來額外的消耗,我們編寫的程序應(yīng)避免頻繁做context swap,提升用戶態(tài)的CPU占比是性能優(yōu)化的一個(gè)目標(biāo)。
進(jìn)程、線程、協(xié)程
在linux內(nèi)核中,進(jìn)程和線程是同樣的系統(tǒng)調(diào)用(clone),進(jìn)程跟線程的區(qū)別:線程是共享存儲空間的,每個(gè)執(zhí)行流有一個(gè)執(zhí)行控制結(jié)構(gòu)體,這里面會有一個(gè)指針,指向地址空間結(jié)構(gòu),一個(gè)進(jìn)程內(nèi)的多個(gè)線程,通過指向同一地址結(jié)構(gòu)實(shí)現(xiàn)共享同一虛擬地址空間。
通過fork創(chuàng)建子進(jìn)程的時(shí)候,不會馬上copy一份數(shù)據(jù),而是推遲到子進(jìn)程對地址空間進(jìn)行改寫,這樣做是合理的,此即為COW(Copy On Write),在應(yīng)用開發(fā)中,也有大量的類似借鑒。
協(xié)程是用戶態(tài)的多執(zhí)行流,C語言提供makecontext/getcontext/swapcontext系列接口,很多協(xié)程庫也是基于這些接口實(shí)現(xiàn)的,微信的協(xié)程庫libco(已開源)通過hook慢速系統(tǒng)調(diào)用(比如write,read)做到靜默替換,非常巧妙。
鏈接
C/C++源代碼經(jīng)編譯鏈接后產(chǎn)生可執(zhí)行程序,其中數(shù)據(jù)和代碼分段存儲,我們寫的函數(shù)將進(jìn)入text節(jié),全局?jǐn)?shù)據(jù)將進(jìn)入數(shù)據(jù)段,未初始化的全局變量進(jìn)入bss,堆和棧向著相反的方向生長,局部變量在棧里,參數(shù)通過棧傳遞,返回值一般通過eax寄存器返回。
想要程序運(yùn)行的更快,最好把相互調(diào)用,關(guān)系緊密的函數(shù)放到代碼段相近的地方,這樣能提高icache命中性。減少代碼量、減少函數(shù)調(diào)用、減少函數(shù)指針同樣能提高i-cache命中性。
內(nèi)聯(lián)既避免了棧幀建立撤銷的開銷,又避免了控制跳轉(zhuǎn)對i-cache的沖刷,所以有利于性能。同樣,關(guān)鍵路徑的性能敏感函數(shù)也應(yīng)該避免遞歸函數(shù)。
減少函數(shù)調(diào)用(就地展開)跟封裝是相違背的,有時(shí)候,為了性能,我們不得不破壞封裝和損傷可讀性的代碼,這是一個(gè)權(quán)衡利弊的問題。
常識和數(shù)據(jù)
CPU拷貝數(shù)據(jù)一般一秒鐘能做到幾百兆,當(dāng)然每次拷貝的數(shù)據(jù)長度不同,吞吐不同。
一次函數(shù)執(zhí)行如果耗費(fèi)超過1000 cycles就比較大了(刨除調(diào)用子函數(shù)的開銷)。
pthread_mutex_t是futex實(shí)現(xiàn),不用每次都進(jìn)入內(nèi)核,首次加解鎖大概耗時(shí)4000-5000 cycles左右,之后,每次加解鎖大概120 cycles,O2優(yōu)化的時(shí)候100 cycles,spinlock耗時(shí)略少。
lock內(nèi)存總線+xchg需要50 cycles,一次內(nèi)存屏障要50 cycles。
有一些無鎖的技術(shù),比如CAS,比如linux kernel里的kfifo,主要利用了整型回繞+內(nèi)存屏障。
四、怎么做性能優(yōu)化(TODO)
兩個(gè)?向:提?運(yùn)?速度 + 減少計(jì)算量。
性能優(yōu)化監(jiān)控先?,要基于數(shù)據(jù)??基于猜測,要搭建能盡量模擬真實(shí)運(yùn)?狀態(tài)的壓?測試環(huán)境,在此基于上獲取的profiling數(shù)據(jù)才是有?的。
方法論:監(jiān)控 -> 分析 -> 優(yōu)化?三部曲。
工具:
perf是linux內(nèi)核自帶的profiling工具,除之之外還有g(shù)prof,但gprof是侵入式的(插樁),編譯的時(shí)候需要加-pg參數(shù),會導(dǎo)致運(yùn)行變慢(慢很多)。
perf采集的數(shù)據(jù),可以用來生成火焰圖,也可以用gprof2dot.py這個(gè)工具來產(chǎn)生比火焰圖更直觀的調(diào)用圖,這些工具就是我經(jīng)常用的。
gprof2dot.py鏈接:https://github.com/jrfonseca/gprof2dot/blob/master/gprof2dot.py
性能優(yōu)化一個(gè)重要原則就是用數(shù)據(jù)說話,而不能憑空猜測。
瓶頸點(diǎn)可能有多個(gè),如果不解決最狹窄的瓶頸點(diǎn),性能優(yōu)化就不能達(dá)到預(yù)期效果。所以性能優(yōu)化之前一定要先進(jìn)行性能測試,摸清家底,建立測試基線。
例子:之前做SIP協(xié)議棧,公司的產(chǎn)品需要提高SIP性能。美國的一個(gè)團(tuán)隊(duì)經(jīng)過理論分析,單憑理論分析認(rèn)為主要是動(dòng)態(tài)內(nèi)存分配是主要瓶頸,把內(nèi)存申請成一大塊內(nèi)存,指針都變成的一大塊內(nèi)存的偏移量,非常難于調(diào)試,最后效果也不好。我們又通過測試分析的方式重構(gòu)了程序,性能是它們的五倍。
另外,性能優(yōu)化要一個(gè)點(diǎn)一個(gè)點(diǎn)的做,做完一點(diǎn),馬上做性能驗(yàn)證。這樣可以避免無用的修改。
五、幾個(gè)具體問題(TODO)
如何定位CPU瓶頸?
CPU是通常大家最先關(guān)注的性能指標(biāo),宏觀維度有核的CPU使用率,微觀有函數(shù)的CPU cycle數(shù),根據(jù)性能的模型,性能規(guī)格與CPU使用率是互相關(guān)聯(lián)的,規(guī)格越高,CPU使用率越高,但是處理器的性能往往又受到內(nèi)存帶寬、Cache、發(fā)熱等因素的影響,所以CPU使用率和規(guī)格參數(shù)之間并不是簡單的線性關(guān)系,所以性能規(guī)格翻倍并不能簡單地翻譯成我們的CPU使用率要優(yōu)化一倍。
至于CPU瓶頸的定位工具,最有名也是最有用的工具就是perf,它是性能分析的第一步,可以幫我們找到系統(tǒng)的熱點(diǎn)函數(shù)。就像人看病一樣,只知道癥狀是不夠的,需要通過醫(yī)療機(jī)器進(jìn)一步分析病因,才能對癥下藥。
所以我們通過性能分析工具PMU或者其他工具去進(jìn)一步分析CPU熱點(diǎn)的原因比如是指令數(shù)本身就比較多,還是Cache miss導(dǎo)致的等,這樣在做性能優(yōu)化的時(shí)候不會走偏。
如何定位IO瓶頸?
系統(tǒng)IO的瓶頸可以通過CPU和負(fù)載的非線性關(guān)系體現(xiàn)出來。當(dāng)負(fù)載增大時(shí),系統(tǒng)吞吐量不能有效增大,CPU不能線性增長,其中一種可能是IO出現(xiàn)阻塞。
系統(tǒng)的隊(duì)列長度特別是發(fā)送、寫磁盤線程的隊(duì)列長度也是IO瓶頸的一個(gè)間接指標(biāo)。
對于網(wǎng)絡(luò)系統(tǒng)來講,我建議先從外部觀察系統(tǒng)。所謂外部觀察是指通過觀察外部的網(wǎng)絡(luò)報(bào)文交換,可以用tcpdump, wireshark等工具,抓包看一下。
比如我們優(yōu)化一個(gè)RPC項(xiàng)目,它的吞吐量是10TPS,客戶希望是100TPS。我們使用wireshark抓取TCP報(bào)文流,可以分析報(bào)文之間的時(shí)間戳,響應(yīng)延遲等指標(biāo)來判斷是否是由網(wǎng)絡(luò)引起來的。
然后可以通過netstat -i/-s選項(xiàng)查看網(wǎng)絡(luò)錯(cuò)誤、重傳等統(tǒng)計(jì)信息。
還可以通過iostat查看cpu等待IO的比例。
IO的概念也可以擴(kuò)展到進(jìn)程間通信。
對于磁盤類的應(yīng)用程序,我們最希望看到寫磁盤有沒有時(shí)延、頻率如何。其中一個(gè)方法就是通過內(nèi)核ftrace、perf-event事件來動(dòng)態(tài)觀測系統(tǒng)。比如記錄寫塊設(shè)備的起始和返回時(shí)間,這樣我們就可以知道磁盤寫是否有延時(shí),也可以統(tǒng)計(jì)寫磁盤時(shí)間耗費(fèi)分布。有一個(gè)開源的工具包perf-tools里面包含著iolatency, iosnoop等工具。
如何定位IO瓶頸?
應(yīng)用程序常用的IO有兩種:Disk IO和網(wǎng)絡(luò)IO。判斷系統(tǒng)是否存在IO瓶頸可以通過觀測系統(tǒng)或進(jìn)程的CPU的IO等待比例來進(jìn)行,比如使用mpstat、top命令。
系統(tǒng)的隊(duì)列長度特別是發(fā)送、寫磁盤線程的隊(duì)列長度也是IO瓶頸的一個(gè)重要指標(biāo)。
對于網(wǎng)絡(luò) IO來講,我們可以先使用netstat -i/-s查看網(wǎng)絡(luò)錯(cuò)誤、重傳等統(tǒng)計(jì)信息,然后使用sar -n DEV 1和sar -n TCP,ETCP 1查看網(wǎng)路實(shí)時(shí)的統(tǒng)計(jì)信息。ss (Socket Statistics)工具可以提供每個(gè)socket相關(guān)的隊(duì)列、緩存等詳細(xì)信息。
更直接的方法可以用tcpdump, wireshark等工具,抓包看一下。
對于Disk IO,我們可以通過iostat -x -p xxx來查看具體設(shè)備使用率和讀寫平均等待時(shí)間。如果使用率接近100%,或者等待時(shí)間過長,都說明Disk IO出現(xiàn)飽和。
一個(gè)更細(xì)致的觀察方法就是通過內(nèi)核ftrace、perf-event來動(dòng)態(tài)觀測Linux內(nèi)核。比如記錄寫塊設(shè)備的起始和返回時(shí)間,這樣我們就可以知道磁盤寫是否有延時(shí),也可以統(tǒng)計(jì)寫磁盤時(shí)間耗費(fèi)分布。有一個(gè)開源的工具包perf-tools里面包含著iolatency, iosnoop等工具。
4.如何定位鎖的問題?
大家都知道鎖會引入額外開銷,但鎖的開銷到底有多大,估計(jì)很多人沒有實(shí)測過,我可以給一個(gè)數(shù)據(jù),一般單次加解鎖100 cycles,spinlock或者cas更快一點(diǎn)。
使用鎖的時(shí)候,要注意鎖的粒度,但鎖的粒度也不是越小越好,太大會增加撞鎖的概率,太小會導(dǎo)致代碼更難寫。
多線程場景下,如果cpu利用率上不去,而系統(tǒng)吞吐也上不去,那就有可能是鎖導(dǎo)致的性能下降,這個(gè)時(shí)候,可以觀察程序的sys cpu和usr cpu,這個(gè)時(shí)候通過perf如果發(fā)現(xiàn)lock的開銷大,那就沒錯(cuò)了。
如果程序卡住了,可以用pstack把堆棧打出來,定位死鎖的問題。
如何提?Cache利用率?
內(nèi)存/Cache問題是我們常見的負(fù)載瓶頸問題,通常可利用perf等一些通用工具來輔助分析,優(yōu)化cache的思想可以從兩方面來著手,一個(gè)是增加局部數(shù)據(jù)/代碼的連續(xù)性,提升cacheline的利用率,減少cache miss,另一個(gè)是通過prefetch,降低miss帶來的開銷。
通過對數(shù)據(jù)/代碼根據(jù)冷熱進(jìn)行重排分區(qū),可提升cacheline的有效利用率,當(dāng)然觸發(fā)false-sharing另當(dāng)別論,這個(gè)需要根據(jù)運(yùn)行trace進(jìn)行深入調(diào)整了;
說到prefetch,用過的人往往都有一種體會,現(xiàn)實(shí)效果比預(yù)期差的比較遠(yuǎn),確實(shí)無論是數(shù)據(jù)prefetch還是代碼prefetch,不確定性太大,我們和無線做過一些實(shí)踐,最終以無線輸出預(yù)取pattern,編譯器自動(dòng)插入prefetch的方案,效果還算可以。
高性能計(jì)算 云計(jì)算
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。