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