1. 程式人生 > >效能優化相關筆記摘要

效能優化相關筆記摘要

前言

新的專案中,碰到一片從前很少接觸過的效能調優的區域,學習了以下內容,做此摘要和記錄。
本文的思路先講下系統性能的定義和測試,之後講如何定位效能瓶頸以及如何優化的方法。

一、系統性能定義

總體來說,系統性能就是兩個事:

Throughput ,吞吐量。也就是每秒鐘可以處理的請求數,任務數。
Latency, 系統延遲。也就是系統在處理一個請求或一個任務時的延遲。

一般來說,一個系統的效能受到這兩個條件的約束,缺一不可。比如,我的系統可以頂得住一百萬的併發,但是系統的延遲是2分鐘以上,那麼,這個一百萬的負載毫無意義。系統延遲很短,但是吞吐量很低,同樣沒有意義。所以,一個好的系統的效能測試必然受到這兩個條件的同時作用。 有經驗的朋友一定知道,這兩個東西的一些關係:

  • Throughput越大,Latency會越差。因為請求量過大,系統太繁忙,所以響應速度自然會低
  • Latency越好,能支援的Throughput就會越高。因為Latency短說明處理速度快,於是就可以處理更多的請求

二、系統性能測試

經過上述的說明,我們知道要測試系統的效能,需要我們收集系統的Throughput和Latency這兩個值。

首先,需要定義Latency這個值,比如說,對於網站系統響應時間必需是5秒以內(對於某些實時系統可能需要定義的更短,比如5ms以內,這個更根據不同的業務來定義)
其次,開發效能測試工具,一個工具用來製造高強度的Throughput,另一個工具用來測量Latency。對於第一個工具,你可以參考一下“十個免費的Web壓力測試工具”,關於如何測量Latency,你可以在程式碼中測量,但是這樣會影響程式的執行,而且只能測試到程式內部的Latency,真正的Latency是整個系統都算上,包括作業系統和網路的延時,你可以使用Wireshark來抓網路包來測量。這兩個工具具體怎麼做,這個還請大家自己思考去了。
我目前需要的測試工具

,是從本地讀取檔案內容放到共享記憶體中,然後一定數量後發包到伺服器通知伺服器可以取資料了,但是由於種種原因導致資料接入的速度上不去,這就是我主要的效能測試難題。之後,做了多路併發控制以及先將檔案內容讀取到記憶體中,再從記憶體中放入共享記憶體中去的程式碼修改

最後,開始效能測試。你需要不斷地提升測試的Throughput,然後觀察系統的負載情況,如果系統頂得住,那就觀察Latency的值。這樣,你就可以找到系統的最大負載,並且你可以知道系統的響應延時是多少。

再多說一些,

關於Latency,如果吞吐量很少,這個值估計會非常穩定,當吞吐量越來越大時,系統的Latency會出現非常劇烈的抖動,所以,我們在測量Latency的時候,我們需要注意到Latency的分佈,也就是說,有百分之幾的在我們允許的範圍,有百分之幾的超出了,有百分之幾的完全不可接受。也許,平均下來的Latency達標了,但是其中僅有50%的達到了我們可接受的範圍。那也沒有意義。

關於效能測試,我們還需要定義一個時間段。比如:在某個吞吐量上持續15分鐘。因為當負載到達的時候,系統會變得不穩定,當過了一兩分鐘後,系統才會穩定。另外,也有可能是,你的系統在這個負載下前幾分鐘還表現正常,然後就不穩定了,甚至垮了。所以,需要這麼一段時間。這個值,我們叫做峰值極限。

效能測試還需要做Soak Test,也就是在某個吞吐量下,系統可以持續跑一週甚至更長。這個值,我們叫做系統的正常執行的負載極限。

效能測試有很多很復要的東西,比如:burst test等。 這裡不能一一詳述,這裡只說了一些和效能調優相關的東西。總之,效能測試是一細活和累活(是挺累的哈,一開始不知道該怎麼做的時候,頭髮都被抓下來不少哈:))

三、定位效能瓶頸

3.1)檢視作業系統負載

首先,當我們系統有問題的時候,我們不要急於去調查我們程式碼,這個毫無意義。我們首要需要看的是作業系統的報告。看看作業系統的CPU利用率,看看記憶體使用率,看看作業系統的IO,還有網路的IO,網路連結數,等等。Windows下的perfmon是一個很不錯的工具,Linux下也有很多相關的命令和工具,比如:SystemTap,LatencyTOP,vmstat, sar, iostat, top, tcpdump等等 。通過觀察這些資料,我們就可以知道我們的軟體的效能基本上出在哪裡。比如:

1)先看CPU利用率

如果CPU利用率不高,但是系統的Throughput和Latency上不去了,這說明我們的程式並沒有忙於計算,而是忙於別的一些事,比如IO。(另外,CPU的利用率還要看核心態的和使用者態的,核心態的一上去了,整個系統的效能就下來了;而對於多核CPU來說,CPU 0 是相當關鍵的,如果CPU 0的負載高,那麼會影響其它核的效能,因為CPU各核間是需要有排程的,這靠CPU0完成)

2)然後,我們可以看一下IO大不大

IO和CPU一般是反著來的,CPU利用率高則IO不大,IO大則CPU就小。關於IO,我們要看三個事,一個是磁碟檔案IO,一個是驅動程式的IO(如:網絡卡),一個是記憶體換頁率。這三個事都會影響系統性能。

3)然後,檢視一下網路頻寬使用情況

在Linux下,你可以使用iftop, iptraf, ntop, tcpdump這些命令來檢視。或是用Wireshark來檢視。

4)如果CPU不高,IO不高,記憶體使用不高,網路頻寬使用不高。但是系統的效能上不去。

這說明你的程式有問題。比如,你的程式被阻塞了。可能是因為等那個鎖,可能是因為等某個資源,或者是在切換上下文。

通過瞭解作業系統的效能,我們才知道效能的問題,比如:頻寬不夠,記憶體不夠,TCP緩衝區不夠,等等,很多時候,不需要調整程式的,只需要調整一下硬體或作業系統的配置就可以了。

3.2)使用Profiler測試

接下來,我們需要使用效能檢測工具,也就是使用某個Profiler來差看一下我們程式的執行效能。如:Java的JProfiler/TPTP/CodePro Profiler,GNU的gprof,IBM的PurifyPlus,Intel的VTune,AMD的CodeAnalyst,還有Linux下的OProfile/perf,後面兩個可以讓你對你的程式碼優化到CPU的微指令級別,如果你關心CPU的L1/L2的快取調優,那麼你需要考慮一下使用VTune。 使用這些Profiler工具,可以讓你程式中各個模組函式甚至指令的很多東西,如:執行的時間 ,呼叫的次數,CPU的利用率,等等。這些東西對我們來說非常有用。

我們重點觀察執行時間最多,呼叫次數最多的那些函式和指令。這裡注意一下,對於呼叫次數多但是時間很短的函式,你可能只需要輕微優化一下,你的效能就上去了(比如:某函式一秒種被呼叫100萬次,你想想如果你讓這個函式提高0.01毫秒的時間 ,這會給你帶來多大的效能)

使用Profiler有個問題我們需要注意一下,因為Profiler會讓你的程式執行的效能變低,像PurifyPlus這樣的工具會在你的程式碼中插入很多程式碼,會導致你的程式執行效率變低,從而無法測試出在高吞吐量下的系統的效能,對此,一般有兩個方法來定位系統瓶頸:

1)在你的程式碼中自己做統計

使用微秒級的計時器和函式呼叫計算器,每隔10秒把統計log到檔案中。

2)分段註釋你的程式碼塊

讓一些函式空轉,做Hard Code的Mock,然後再測試一下系統的Throughput和Latency是否有質的變化,如果有,那麼被註釋的函式就是效能瓶頸,再在這個函式體內註釋程式碼,直到找到最耗效能的語句。

最後再說一點,對於效能測試,不同的Throughput會出現不同的測試結果,不同的測試資料也會有不同的測試結果。所以,用於效能測試的資料非常重要,效能測試中,我們需要觀測試不同Throughput的結果

四、常見的系統瓶頸

一般來說,效能優化也就是下面的幾個策略:

  • 用空間換時間。各種cache如CPU L1/L2/RAM到硬碟,都是用空間來換時間的策略。這樣策略基本上是把計算的過程一步一步的儲存或快取下來,這樣就不用每次用的時候都要再計算一遍,比如資料緩衝,CDN,等。這樣的策略還表現為冗餘資料,比如資料鏡象,負載均衡什麼的。

  • 用時間換空間。有時候,少量的空間可能效能會更好,比如網路傳輸,如果有一些壓縮資料的演算法(如前些天說的“Huffman 編碼壓縮演算法” 和 “rsync 的核心演算法”),這樣的演算法其實很耗時,但是因為瓶頸在網路傳輸,所以用時間來換空間反而能省時間。

  • 簡化程式碼。最高效的程式就是不執行任何程式碼的程式,所以,程式碼越少效能就越高。關於程式碼級優化的技術大學裡的教科書有很多示例了。如:減少迴圈的層數,減少遞迴,在迴圈中少宣告變數,少做分配和釋放記憶體的操作,儘量把迴圈體內的表示式抽到迴圈外,條件表達的中的多個條件判斷的次序,儘量在程式啟動時把一些東西準備好,注意函式呼叫的開銷(棧上開銷),注意面嚮物件語言中臨時物件的開銷,小心使用異常(不要用異常來檢查一些可接受可忽略並經常發生的錯誤),…… 等等,等等,這連東西需要我們非常瞭解程式語言和常用的庫。

  • 並行處理。如果CPU只有一個核,你要玩多程序,多執行緒,對於計算密集型的軟體會反而更慢(因為作業系統排程和切換開銷很大),CPU的核多了才能真正體現出多程序多執行緒的優勢。並行處理需要我們的程式有Scalability,不能水平或垂直擴充套件的程式無法進行並行處理。從架構上來說,這表現為——是否可以做到不改程式碼只是加加機器就可以完成效能提升?

總之,根據2:8原則來說,20%的程式碼耗了你80%的效能,找到那20%的程式碼,你就可以優化那80%的效能。 下面的一些東西都是我的一些經驗,我只例舉了一些最有價值的效能調優的的方法

4.1)演算法調優。

演算法非常重要,好的演算法會有更好的效能。舉幾個我經歷過的專案的例子,大家可以感覺一下。

  • 一個是過濾演算法,系統需要對收到的請求做過濾,我們把可以被filter in/out的東西配置在了一個檔案中,原有的過濾演算法是遍歷過濾配置,後來,我們找到了一種方法可以對這個過濾配置進行排序,這樣就可以用二分折半的方法來過濾,系統性能增加了50%。

  • 一個是雜湊演算法。計算雜湊演算法的函式並不高效,一方面是計算太費時,另一方面是碰撞太高,碰撞高了就跟單向連結串列一個性能(可參看Hash Collision DoS 問題)。我們知道,演算法都是和需要處理的資料很有關係的,就算是被大家所嘲笑的“氣泡排序”在某些情況下(大多數資料是排好序的)其效率會高於所有的排序演算法。雜湊演算法也一樣,廣為人知的雜湊演算法都是用英文字典做測試,但是我們的業務在資料有其特殊性,所以,對於還需要根據自己的資料來挑選適合的雜湊演算法。對於我以前的一個專案,公司內某牛人給我發來了一個雜湊演算法,結果讓我們的系統性能上升了150%。(關於各種雜湊演算法,你一定要看看StackExchange上的這篇關於各種hash演算法的文章 )

  • 分而治之和預處理。以前有一個程式為了生成月報表,每次都需要計算很長的時間,有時候需要花將近一整天的時間。於是我們把我們找到了一種方法可以把這個演算法發成增量式的,也就是說我每天都把當天的資料計算好了後和前一天的報表合併,這樣可以大大的節省計算時間,每天的資料計算量只需要20分鐘,但是如果我要算整個月的,系統則需要10個小時以上(SQL語句在大資料量面前效能成級數性下降)。這種分而治之的思路在大資料面前對效能有很幫助,就像merge排序一樣。SQL語句和資料庫的效能優化也是這一策略,如:使用巢狀式的Select而不是笛卡爾積的Select,使用檢視,等等。

4.2)程式碼調優。

從我的經驗上來說,程式碼上的調優有下面這幾點:

  • 字串操作。這是最費系統性能的事了,無論是strcpy, strcat還是strlen,最需要注意的是字串子串匹配。所以,能用整型最好用整型。舉幾個例子,第一個例子是N年前做銀行的時候,我的同事喜歡把日期存成字串(如:2012-05-29 08:30:02),我勒個去,一個select where between語句相當耗時。另一個例子是,我以前有個同事把一些狀態碼用字串來處理,他的理由是,這樣可以在介面上直接顯示,後來效能調優的時候,我把這些狀態碼全改成整型,然後用位操作查狀態,因為有一個每秒鐘被呼叫了150K次的函式裡面有三處需要檢查狀態,經過改善以後,整個系統的效能上升了30%左右。還有一個例子是,我以前從事的某個產品程式設計規範中有一條是要在每個函式中把函式名定義出來,如:const char fname[]=”functionName()”, 這是為了好打日誌,但是為什麼不宣告成 static型別的呢?

  • 多執行緒調優。有人說,thread is evil,這個對於系統性能在某些時候是個問題。因為多執行緒瓶頸就在於互斥和同步的鎖上,以及執行緒上下文切換的成本,怎麼樣的少用鎖或不用鎖是根本(比如:多版本併發控制(MVCC)在分散式系統中的應用 中說的樂觀鎖可以解決效能問題),此外,還有讀寫鎖也可以解決大多數是讀操作的併發的效能問題。這裡多說一點在C++中,我們可能會使用執行緒安全的智慧指標AutoPtr或是別的一些容器,只要是執行緒安全的,其不管三七二十一都要上鎖,上鎖是個成本很高的操作,使用AutoPtr會讓我們的系統性能下降得很快,如果你可以保證不會有執行緒併發問題,那麼你應該不要用AutoPtr。我記得我上次我們同事去掉智慧指標的引用計數,讓系統性能提升了50%以上。對於Java物件的引用計數,如果我猜的沒錯的話,到處都是鎖,所以,Java的效能問題一直是個問題。另外,執行緒不是越多越好,執行緒間的排程和上下文切換也是很誇張的事,儘可能的在一個執行緒裡幹,儘可能的不要同步執行緒。這會讓你有很多的效能浪費。

  • 記憶體分配。不要小看程式的記憶體分配。malloc/realloc/calloc這樣的系統調非常耗時,尤其是當記憶體出現碎片的時候。我以前的公司出過這樣一個問題——在使用者的站點上,我們的程式有一天不響應了,用GDB跟進去一看,系統hang在了malloc操作上,20秒都沒有返回,重啟一些系統就好了。這就是記憶體碎片的問題。這就是為什麼很多人抱怨STL有嚴重的記憶體碎片的問題,因為太多的小記憶體的分配釋放了。有很多人會以為用記憶體池可以解決這個問題,但是實際上他們只是重新發明了Runtime-C或作業系統的記憶體管理機制,完全於事無補。當然解決記憶體碎片的問題還是通過記憶體池,具體來說是一系列不同尺寸的記憶體池(這個留給大家自己去思考)。當然,少進行動態記憶體分配是最好的。說到記憶體池就需要說一下池化技術。比如執行緒池,連線池等。池化技術對於一些短作業來說(如http服務) 相當相當的有效。這項技術可以減少連結建立,執行緒建立的開銷,從而提高效能。(對於這些小記憶體的清理,可以手動清理)

  • 非同步操作。我們知道Unix下的檔案操作是有block和non-block的方式的,像有些系統呼叫也是block式的,如:Socket下的select,Windows下的WaitforObject之類的,如果我們的程式是同步操作,那麼會非常影響效能,我們可以改成非同步的,但是改成非同步的方式會讓你的程式變複雜。非同步方式一般要通過佇列,要注間佇列的效能問題,另外,非同步下的狀態通知通常是個問題,比如訊息事件通知方式,有callback方式,等,這些方式同樣可能會影響你的效能。但是通常來說,非同步操作會讓效能的吞吐率有很大提升(Throughput),但是會犧牲系統的響應時間(latency)。這需要業務上支援

  • 語言和程式碼庫。我們要熟悉語言以及所使用的函式庫或類庫的效能。比如:STL中的很多容器分配了記憶體後,那怕你刪除元素,記憶體也不會回收,其會造成記憶體洩露的假像,並可能造成記憶體碎片問題。再如,STL某些容器的size()==0 和 empty()是不一樣的,因為,size()是O(n)複雜度,empty()是O(1)的複雜度,這個要小心。Java中的JVM調優需要使用的這些引數:-Xms -Xmx -Xmn -XX:SurvivorRatio -XX:MaxTenuringThreshold,還需要注意JVM的GC,GC的霸氣大家都知道,尤其是full GC(還整理記憶體碎片),他就像“恐龍特級克賽號”一樣,他執行的時候,整個世界的時間都停止了。(對java不是很熟悉,看著累2333)

4.3)網路調優

這部分對於我不是很重要,暫時略過,想看可點選【1】連結

4.4)系統調優

A)I/O模型

前面說到過select/poll/epoll這三個系統呼叫,我們都知道,Unix/Linux下把所有的裝置都當成檔案來進行I/O,所以,那三個操作更應該算是I/O相關的系統呼叫。說到 I/O模型,這對於我們的I/O效能相當重要,我們知道,Unix/Linux經典的I/O方式是(關於Linux下的I/O模型,大家可以讀一下這篇文章《使用非同步I/O大大提高效能》):

  • 同步阻塞式I/O,這個不說了。
  • 同步無阻塞方式。其通過fctnl設定 O_NONBLOCK 來完成。
  • 對於select/poll/epoll這三個是I/O不阻塞,但是在事件上阻塞,算是:I/O非同步,事件同步的呼叫。
  • AIO方式。這種I/O 模型是一種處理與 I/O 並行的模型。I/O請求會立即返回,說明請求已經成功發起了。在後臺完成I/O操作時,嚮應用程式發起通知,通知有兩種方式:一種是產生一個訊號,另一種是執行一個基於執行緒的回撥函式來完成這次 I/O 處理過程。

第四種因為沒有任何的阻塞,無論是I/O上,還是事件通知上,所以,其可以讓你充分地利用CPU,比起第二種同步無阻塞好處就是,第二種要你一遍一遍地去輪詢。Nginx之所所以高效,是其使用了epoll和AIO的方式來進行I/O的。

再說一下Windows下的I/O模型,
a)一個是WriteFile系統呼叫,這個系統呼叫可以是同步阻塞的,也可以是同步無阻塞的,關於看檔案是不是以Overlapped開啟的。關於同步無阻塞,需要設定其最後一個引數Overlapped,微軟叫Overlapped I/O,你需要WaitForSingleObject才能知道有沒有寫完成。這個系統呼叫的效能可想而知。
b)另一個叫WriteFileEx的系統呼叫,其可以實現非同步I/O,並可以讓你傳入一個callback函式,等I/O結束後回撥之, 但是這個回撥的過程Windows是把callback函式放到了APC(Asynchronous Procedure Calls)的佇列中,然後,只用當應用程式當前執行緒成為可被通知狀態(Alterable)時,才會被回撥。只有當你的執行緒使用了這幾個函式時WaitForSingleObjectEx, WaitForMultipleObjectsEx, MsgWaitForMultipleObjectsEx, SignalObjectAndWait 和 SleepEx,執行緒才會成為Alterable狀態。可見,這個模型,還是有wait,所以效能也不高。
c)然後是IOCP – IO Completion Port,IOCP會把I/O的結果放在一個佇列中,但是,偵聽這個佇列的不是主執行緒,而是專門來幹這個事的一個或多個執行緒去幹(老的平臺要你自己建立執行緒,新的平臺是你可以建立一個執行緒池)。IOCP是一個執行緒池模型。這個和Linux下的AIO模型比較相似,但是實現方式和使用方式完全不一樣。

當然,真正提高I/O效能方式是把和外設的I/O的次數降到最低,最好沒有,所以,對於讀來說,記憶體cache通常可以從質上提升效能,因為記憶體比外設快太多了。對於寫來說,cache主要寫的資料,少寫幾次,但是cache帶來的問題就是實時性的問題,也就是latency會變大,我們需要在寫的次數上和相應上做權衡

B)多核CPU調優

關於CPU的多核技術,我們知道,CPU0是很關鍵的,如果0號CPU被用得過狠的話,別的CPU效能也會下降,因為CPU0是有調整功能的,所以,我們不能任由作業系統負載均衡,因為我們自己更瞭解自己的程式,所以,我們可以手動地為其分配CPU核,而不會過多地佔用CPU0,或是讓我們關鍵程序和一堆別的程序擠在一起

  • 對於Windows來說,我們可以通過“工作管理員”中的“程序”而中右鍵選單中的“設定相關性……”(Set Affinity…)來設定並限制這個程序能被執行在哪些核上。

  • 對於Linux來說,可以使用taskset命令來設定(你可以通過安裝schedutils來安裝這個命令:apt-get install schedutils)

多核CPU還有一個技術叫NUMA技術(Non-Uniform Memory Access)。傳統的多核運算是使用SMP(Symmetric Multi-Processor )模式,多個處理器共享一個集中的儲存器和I/O匯流排於是就會出現一致儲存器訪問的問題,一致性通常意味著效能問題。NUMA模式下,處理器被劃分成多個node, 每個node有自己的本地儲存器空間。關於NUMA的一些技術細節,你可以檢視一下這篇文章《Linux 的 NUMA 技術》,在Linux下,對NUMA調優的命令是:numactl 。如下面的命令:(指定命令“myprogram arg1 arg2”執行在node 0 上,其記憶體分配在node 0 和 1上)

numactl --cpubind=0 --membind=0,1 myprogram arg1 arg2

當然,上面這個命令並不好,因為記憶體跨越了兩個node,這非常不好。最好的方式是隻讓程式訪問和自己執行一樣的node,如:

$ numactl --membind 1 --cpunodebind 1 --localalloc myapplication

C)檔案系統調優

關於檔案系統,因為檔案系統也是有cache的,所以,為了讓檔案系統有最大的效能。首要的事情就是分配足夠大的記憶體,這個非常關鍵,在Linux下可以使用free命令來檢視 free/used/buffers/cached,理想來說,buffers和cached應該有40%左右。然後是一個快速的硬碟控制器,SCSI會好很多。最快的是Intel SSD 固態硬碟,速度超快,但是寫次數有限。

接下來,我們就可以調優檔案系統配置了,對於Linux的Ext3/4來說,幾乎在所有情況下都有所幫助的一個引數是關閉檔案系統訪問時間,在/etc/fstab下看看你的檔案系統 有沒有noatime引數(一般來說應該有),還有一個是dealloc,它可以讓系統在最後時刻決定寫入檔案發生時使用哪個塊,可優化這個寫入程式。還要注間一下三種日誌模式:data=journal、data=ordered和data=writeback。預設設定data=ordered提供效能和防護之間的最佳平衡

當然,對於這些來說,ext4的預設設定基本上是最佳優化了。

這裡介紹一個Linux下的檢視I/O的命令—— iotop,可以讓你看到各程序的磁碟讀寫的負載情況。

其它還有一些關於NFS、XFS的調優,大家可以上google搜尋一些相關優化的文章看看。關於各檔案系統,大家可以看一下這篇文章——《Linux日誌檔案系統及效能分析

4.5)資料庫調優

一樣略過不提

Reference

【1】效能調優攻略 https://coolshell.cn/articles/7490.html
【2】程式碼優化概要 https://coolshell.cn/articles/2967.html
【3】Linux 的 NUMA 技術 https://www.ibm.com/developerworks/cn/linux/l-numa/index.html
【4】Linux日誌檔案系統及效能分析 https://www.ibm.com/developerworks/cn/linux/l-jfs/