性能調優攻略

關於性能優化這是一個比較大的話題,在《由 12306.cn 談談網站性能技術》中我從業務和設計上說過一些可用的技術以及那些技術的優缺點,今天,想從一些技術細節上談談性能優化,主要是一些代碼級別的技術和方法。本文的東西是我的一些經驗和知識,並不一定全對,希望大家指正和補充

在開始這篇文章之前,大家可以移步去看一下《代碼優化概要》,這篇文章基本上告訴你——要進行優化,先得找到性能瓶頸! 但是在講如何定位系統性能瓶勁之前,請讓我講一下系統性能的定義和測試,因爲沒有這兩件事,後面的定位和優化無從談起。

一、系統性能定義

讓我們先來說說如何什麼是系統性能。這個定義非常關鍵,如果我們不清楚什麼是系統性能,那麼我們將無法定位之。我見過很多朋友會覺得這很容易,但是仔細一問,其實他們並沒有一個比較系統的方法,所以,在這裏我想告訴大家如何系統地來定位性能。 總體來說,系統性能就是兩個事:

  1. Throughput ,吞吐量。也就是每秒鐘可以處理的請求數,任務數。
  2. 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 下也有很多相關的命令和工具,比如:SystemTapLatencyTOP,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 的結果

四、常見的系統瓶頸

下面這些東西是我所經歷過的一些問題,也許並不全,也許並不對,大家可以補充指正,我純屬拋磚引玉。關於系統架構方面的性能調優,大家可移步看一下《由 12306.cn 談談網站性能技術》,關於 Web 方面的一些性能調優的東西,大家可以看看《Web 開發中需要了解的東西》一文中的性能一章。我在這裏就不再說設計和架構上的東西了。

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

  • 用空間換時間。各種 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(還整理內存碎片),他就像“恐龍特級克賽號”一樣,他運行的時候,整個世界的時間都停止了。

4. 3)網絡調優

關於網絡調優,尤其是 TCP Tuning(你可以以這兩個關鍵詞在網上找到很多文章),這裏面有很多很多東西可以說。看看 Linux 下 TCP/IP 的那麼多參數就知道了(順便說一下,你也許不喜歡 Linux,但是你不能否認 Linux 給我們了很多可以進行內核調優的權力)。強烈建議大家看看《TCP/IP 詳解卷1:協議》這本書。我在這裏只講一些概念上的東西。

A) TCP 調優

我們知道 TCP 鏈接是有很多開銷的,一個是會佔用文件描述符,另一個是會開緩存,一般來說一個系統可以支持的 TCP 鏈接數是有限的,我們需要清楚地認識到 TCP 鏈接對系統的開銷是很大的。正是因爲 TCP 是耗資源的,所以,很多***都是讓你係統上出現大量的 TCP 鏈接,把你的系統資源耗盡。比如著名的 SYNC Flood ***。

所以,我們要注意配置 KeepAlive 參數,這個參數的意思是定義一個時間,如果鏈接上沒有數據傳輸,系統會在這個時間發一個包,如果沒有收到迴應,那麼 TCP 就認爲鏈接斷了,然後就會把鏈接關閉,這樣可以回收系統資源開銷。(注:HTTP 層上也有 KeepAlive 參數)對於像 HTTP 這樣的短鏈接,設置一個1-2分鐘的 keepalive 非常重要。這可以在一定程度上防止 DoS ***。有下面幾個參數(下面這些參數的值僅供參考):

  net.ipv4.tcp_keepalive_probes = 5

  net.ipv4.tcp_keepalive_intvl = 20

  net.ipv4.tcp_fin_timeout = 30

對於 TCP 的 TIME_WAIT 這個狀態,主動關閉的一方進入 TIME_WAIT 狀態,TIME_WAIT 狀態將持續 2 個 MSL (Max Segment Lifetime),默認爲 4 分鐘,TIME_WAIT 狀態下的資源不能回收。有大量的 TIME_WAIT 鏈接的情況一般是在 HTTP 服務器上。對此,有兩個參數需要注意,

  net.ipv4.tcp_tw_reuse=1

  net.ipv4.tcp_tw_recycle=1

前者表示重用 TIME_WAIT,後者表示回收 TIME_WAIT 的資源。

TCP 還有一個重要的概念叫 RWIN(TCP Receive Window Size),這個東西的意思是,我一個 TCP 鏈接在沒有向 Sender 發出 ack 時可以接收到的最大的數據包。爲什麼這個很重要?因爲如果 Sender 沒有收到 Receiver 發過來 ack,Sender 就會停止發送數據並會等一段時間,如果超時,那麼就會重傳。這就是爲什麼 TCP 鏈接是可靠鏈接的原因。重傳還不是最嚴重的,如果有丟包發生的話,TCP 的帶寬使用率會馬上受到影響(會盲目減半),再丟包,再減半,然後如果不丟包了,就逐步恢復。相關參數如下:

  net.core.wmem_default = 8388608

  net.core.rmem_default = 8388608

  net.core.rmem_max = 16777216

  net.core.wmem_max = 16777216

一般來說,理論上的 RWIN 應該設置成:吞吐量 * 迴路時間。Sender 端的 buffer 應該和 RWIN 有一樣的大小,因爲 Sender 端發送完數據後要等 Receiver 端確認,如果網絡延時很大,buffer 過小了,確認的次數就會多,於是性能就不高,對網絡的利用率也就不高了。也就是說,對於延遲大的網絡,我們需要大的 buffer,這樣可以少一點 ack,多一些數據,對於響應快一點的網絡,可以少一些 buffer。因爲,如果有丟包(沒有收到 ack),buffer 過大可能會有問題,因爲這會讓 TCP 重傳所有的數據,反而影響網絡性能。(當然,網絡差的情況下,就別玩什麼高性能了) 所以,高性能的網絡重要的是要讓網絡丟包率非常非常地小(基本上是用在 LAN 裏),如果網絡基本是可信的,這樣用大一點的 buffer 會有更好的網絡傳輸性能(來來回回太多太影響性能了)。

另外,我們想一想,如果網絡質量非常好,基本不丟包,而業務上我們不怕偶爾丟幾個包,如果是這樣的話,那麼,我們爲什麼不用速度更快的 UDP 呢?你想過這個問題了嗎?

B)UDP 調優

說到 UDP 的調優,有一些事我想重點說一樣,那就是 MTU——最大傳輸單元(其實這對 TCP 也一樣,因爲這是鏈路層上的東西)。所謂最大傳輸單元,你可以想像成是公路上的公交車,假設一個公交車可以最多坐 70 人,帶寬就像是公路的車道數一樣,如果一條路上最多可以容下 100 輛公交車,那意味着我最多可以運送 7000 人,但是如果公交車坐不滿,比如平均每輛車只有 20 人,那麼我只運送了 2000 人,於是我公路資源(帶寬資源)就被浪費了。 所以,我們對於一個 UDP 的包,我們要儘量地讓他大到 MTU 的最大尺寸再往網絡上傳,這樣可以最大化帶寬利用率。對於這個 MTU,以太網是 1500 字節,光纖是 4352 字節,802.11無線網是 7981。但是,當我們用 TCP/UDP 發包的時候,我們的有效負載 Payload 要低於這個值,因爲 IP 協議會加上 20 個字節,UDP 會加上 8 個字節(TCP 加的更多),所以,一般來說,你的一個 UDP 包的最大應該是 1500-8-20=1472,這是你的數據的大小。當然,如果你用光纖的話, 這個值就可以更大一些。(順便說一下,對於某些 NB 的千光以態網網卡來說,在網卡上,網卡硬件如果發現你的包的大小超過了 MTU,其會幫你做 fragment,到了目標端又會幫你做重組,這就不需要你在程序中處理了)

再多說一下,使用 Socket 編程的時候,你可以使用 setsockopt () 設置 SO_SNDBUF/SO_RCVBUF 的大小,TTL 和 KeepAlive 這些關鍵的設置,當然,還有很多,具體你可以查看一下 Socket 的手冊。

最後說一點,UDP 還有一個最大的好處是 multi-cast 多播,這個技術對於你需要在內網裏通知多臺結點時非常方便和高效。而且,多播這種技術對於機會的水平擴展(需要增加機器來偵聽多播信息)也很有利。

C)網卡調優

對於網卡,我們也是可以調優的,這對於千兆以及網網卡非常必要,在 Linux 下,我們可以用 ifconfig 查看網上的統計信息,如果我們看到 overrun 上有數據,我們就可能需要調整一下 txqueuelen 的尺寸(一般默認爲 1000),我們可以調大一些,如:ifconfig eth0 txqueuelen 5000。Linux 下還有一個命令叫:ethtool 可以用於設置網卡的緩衝區大小。在 Windows 下,我們可以在網卡適配器中的高級選項卡中調整相關的參數(如:Receive Buffers, Transmit Buffer 等,不同的網卡有不同的參數)。把 Buffer 調大對於需要大數據量的網絡傳輸非常有效。

D)其它網絡性能

關於多路複用技術,也就是用一個線程來管理所有的 TCP 鏈接,有三個系統調用要重點注意:一個是 select,這個系統調用只支持上限 1024 個鏈接,第二個是 poll,其可以突破 1024 的限制,但是 select 和 poll 本質上是使用的輪詢機制,輪詢機制在鏈接多的時候性能很差,因主是O(n)的算法,所以,epoll 出現了,epoll 是操作系統內核支持的,僅當在鏈接活躍時,操作系統纔會 callback,這是由操作系統通知觸發的,但其只有 Linux Kernel 2.6 以後才支持(準確說是2.5.44中引入的),當然,如果所有的鏈接都是活躍的,過多的使用 epoll_ctl 可能會比輪詢的方式還影響性能,不過影響的不大。

另外,關於一些和 DNS Lookup 的系統調用要小心,比如:gethostbyaddr/gethostbyname,這個函數可能會相當的費時,因爲其要到網絡上去找域名,因爲 DNS 的遞歸查詢,會導致嚴重超時,而又不能通過設置什麼參數來設置 time out,對此你可以通過配置 hosts 文件來加快速度,或是自己在內存中管理對應表,在程序啓動時查好,而不要在運行時每次都查。另外,在多線程下面,gethostbyname 會一個更嚴重的問題,就是如果有一個線程的 gethostbyname 發生阻塞,其它線程都會在 gethostbyname 處發生阻塞,這個比較變態,要小心。(你可以試試 GNU 的 gethostbyname_r(),這個的性能要好一些) 這種到網上找信息的東西很多,比如,如果你的 Linux 使用了 NIS,或是 NFS,某些用戶或文件相關的系統調用就很慢,所以要小心。

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, SignalObjectAndWaitSleepEx,線程纔會成爲 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)數據庫調優

數據庫調優並不是我的強項,我就僅用我非常有限的知識說上一些吧。注意,下面的這些東西並不一定正確,因爲在不同的業務場景,不同的數據庫設計下可能會得到完全相反的結論,所以,我僅在這裏做一些一般性的說明,具體問題還要具體分析。

A)數據庫引擎調優

我對數據庫引擎不是熟,但是有幾個事情我覺得是一定要去了解的。

  • 數據庫的鎖的方式。這個非常非常地重要。併發情況下,鎖是非常非常影響性能的。各種隔離級別,行鎖,表鎖,頁鎖,讀寫鎖,事務鎖,以及各種寫優先還是讀優先機制。性能最高的是不要鎖,所以,分庫分表,冗餘數據,減少一致性事務處理,可以有效地提高性能。NoSQL 就是犧牲了一致性和事務處理,並冗餘數據,從而達到了分佈式和高性能。
  • 數據庫的存儲機制。不但要搞清楚各種類型字段是怎麼存儲的,更重要的是數據庫的數據存儲方式,是怎麼分區的,是怎麼管理的,比如 Oracle 的數據文件,表空間,段,等等。瞭解清楚這個機制可以減輕很多的I/O負載。比如:MySQL 下使用 show engines;可以看到各種存儲引擎的支持。不同的存儲引擎有不同的側重點,針對不同的業務或數據庫設計會讓你有不同的性能。
  • 數據庫的分佈式策略。最簡單的就是複製或鏡像,需要了解分佈式的一致性算法,或是主主同步,主從同步。通過了解這種技術的機理可以做到數據庫級別的水平擴展。

B)SQL 語句優化

關於 SQL 語句的優化,首先也是要使用工具,比如:MySQL SQL Query AnalyzerOracle SQL Performance Analyzer,或是微軟 SQL Query Analyzer,基本上來說,所有的 RMDB 都會有這樣的工具,來讓你查看你的應用中的 SQL 的性能問題。 還可以使用 explain 來看看 SQL 語句最終 Execution Plan 會是什麼樣的。

還有一點很重要,數據庫的各種操作需要大量的內存,所以服務器的內存要夠,優其應對那些多表查詢的 SQL 語句,那是相當的耗內存。

下面我根據我有限的數據庫 SQL 的知識說幾個會有性能問題的 SQL:

  • 全表檢索。比如:select * from user where lastname = “xxxx”,這樣的 SQL 語句基本上是全表查找,線性複雜度O(n),記錄數越多,性能也越差(如:100條記錄的查找要 50ms,一百萬條記錄需要 5 分鐘)。對於這種情況,我們可以有兩種方法提高性能:一種方法是分表,把記錄數降下來,另一種方法是建索引(爲 lastname 建索引)。索引就像是 key-value 的數據結構一樣,key 就是 where 後面的字段,value 就是物理行號,對索引的搜索複雜度是基本上是O(log (n)) ——用B-Tree 實現索引(如:100條記錄的查找要 50ms,一百萬條記錄需要 100ms)。
  • 索引。對於索引字段,最好不要在字段上做計算、類型轉換、函數、空值判斷、字段連接操作,這些操作都會破壞索引原本的性能。當然,索引一般都出現在 Where 或是 Order by 字句中,所以對 Where 和 Order by 子句中的子段最好不要進行計算操作,或是加上什麼 NOT 之類的,或是使用什麼函數。
  • 多表查詢。關係型數據庫最多的操作就是多表查詢,多表查詢主要有三個關鍵字,EXISTS,IN 和 JOIN(關於各種 join,可以參看圖解 SQL 的 Join 一文)。基本來說,現代的數據引擎對 SQL 語句優化得都挺好的,JOIN 和 IN/EXISTS 在結果上有些不同,但性能基本上都差不多。有人說,EXISTS 的性能要好於 IN,IN 的性能要好於 JOIN,我各人覺得,這個還要看你的數據、schema 和 SQL 語句的複雜度,對於一般的簡單的情況來說,都差不多,所以千萬不要使用過多的嵌套,千萬不要讓你的 SQL 太複雜,寧可使用幾個簡單的 SQL 也不要使用一個巨大無比的嵌套N級的 SQL。還有人說,如果兩個表的數據量差不多,Exists 的性能可能會高於 In,In 可能會高於 Join,如果這兩個表一大一小,那麼子查詢中,Exists 用大表,In 則用小表。這個,我沒有驗證過,放在這裏讓大家討論吧。另,有一篇關於 SQL Server 的文章大家可以看看《IN vs JOIN vs EXISTS
  • JOIN 操作。有人說,Join 表的順序會影響性能,只要 Join 的結果集是一樣,性能和 join 的次序無關。因爲後臺的數據庫引擎會幫我們優化的。Join 有三種實現算法,嵌套循環,排序歸併,和 Hash 式的 Join。(MySQL 只支持第一種)
    • 嵌套循環,就好像是我們常見的多重嵌套循環。注意,前面的索引說過,數據庫的索引查找算法用的是B-Tree,這是O(log (n))的算法,所以,整個算法復法度應該是O(log (n)) * O(log (m)) 這樣的。
    • Hash 式的 Join,主要解決嵌套循環的O(log (n))的複雜,使用一個臨時的 hash 表來標記。
    • 排序歸併,意思是兩個表按照查詢字段排好序,然後再合併。當然,索引字段一般是排好序的。

還是那句話,具體要看什麼樣的數據,什麼樣的 SQL 語句,你才知道用哪種方法是最好的。

  • 部分結果集。我們知道 MySQL 裏的 Limit 關鍵字,Oracle 裏的 rownum,SQL Server 裏的 Top 都是在限制前幾條的返回結果。這給了我們數據庫引擎很多可以調優的空間。一般來說,返回 top n 的記錄數據需要我們使用 order by,注意在這裏我們需要爲 order by 的字段建立索引。有了被建索引的 order by 後,會讓我們的 select 語句的性能不會被記錄數的所影響。使用這個技術,一般來說我們前臺會以分頁方式來顯現數據,Mysql 用的是 OFFSET,SQL Server 用的是 FETCH NEXT,這種 Fetch 的方式其實並不好是線性複雜度,所以,如果我們能夠知道 order by 字段的第二頁的起始值,我們就可以在 where 語句裏直接使用>=的表達式來 select,這種技術叫 seek,而不是 fetch,seek 的性能比 fetch 要高很多。
  • 字符串。正如我前面所說的,字符串操作對性能上有非常大的惡夢,所以,能用數據的情況就用數字,比如:時間,工號,等。
  • 全文檢索。千萬不要用 Like 之類的東西來做全文檢索,如果要玩全文檢索,可以嘗試使用 Sphinx
  • 其它
    • 不要 select *,而是明確指出各個字段,如果有多個表,一定要在字段名前加上表名,不要讓引擎去算。
    • 不要用 Having,因爲其要遍歷所有的記錄。性能差得不能再差。
    • 儘可能地使用 UNION ALL 取代 UNION。
    • 索引過多,insert 和 delete 就會越慢。而 update 如果 update 多數索引,也會慢,但是如果只 update 一個,則只會影響一個索引表。
    • 等等。

關於 SQL 語句的優化,網上有很多文章, 不同的數據庫引擎有不同的優化技巧,《MySQL 性能優化的最佳 20+ 條經驗

先寫這麼多吧,歡迎大家指正補充。

12

0

來自: coolshell.cn

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章