從代碼級別的技術(shù)細(xì)節(jié)入手,看性能優(yōu)化怎么做
系統(tǒng)性能定義
讓我們先來說說如何什么是系統(tǒng)性能。這個(gè)定義非常關(guān)鍵,如果我們不清楚什么是系統(tǒng)性能,那么我們將無法定位之。我見過很多朋友會(huì)覺得這很容易,但是仔細(xì)一問,其實(shí)他們并沒有一個(gè)比較系統(tǒng)的方法,所以,在這里我想告訴大家如何系統(tǒng)地來定位性能。 總體來說,系統(tǒng)性能就是兩個(gè)事:
Throughput,吞吐量。也就是每秒鐘可以處理的請求數(shù),任務(wù)數(shù)。
Latency, 系統(tǒng)延遲。也就是系統(tǒng)在處理一個(gè)請求或一個(gè)任務(wù)時(shí)的延遲。
一般來說,一個(gè)系統(tǒng)的性能受到這兩個(gè)條件的約束,缺一不可。比如,我的系統(tǒng)可以頂?shù)米∫话偃f的并發(fā),但是系統(tǒng)的延遲是 2 分鐘以上,那么,這個(gè)一百萬的負(fù)載毫無意義。系統(tǒng)延遲很短,但是吞吐量很低,同樣沒有意義。所以,一個(gè)好的系統(tǒng)的性能測試必然受到這兩個(gè)條件的同時(shí)作用。 有經(jīng)驗(yàn)的朋友一定知道,這兩個(gè)東西的一些關(guān)系:
Throughput 越大,Latency 會(huì)越差。因?yàn)檎埱罅窟^大,系統(tǒng)太繁忙,所以響應(yīng)速度自然會(huì)低。
Latency 越好,能支持的 Throughput 就會(huì)越高。因?yàn)?Latency 短說明處理速度快,于是就可以處理更多的請求。
系統(tǒng)性能測試
經(jīng)過上述的說明,我們知道要測試系統(tǒng)的性能,需要我們收集系統(tǒng)的 Throughput 和 Latency 這兩個(gè)值。
首先,需要定義 Latency 這個(gè)值,比如說,對于網(wǎng)站系統(tǒng)響應(yīng)時(shí)間必需是 5 秒以內(nèi)(對于某些實(shí)時(shí)系統(tǒng)可能需要定義的更短,比如 5ms 以內(nèi),這個(gè)更根據(jù)不同的業(yè)務(wù)來定義)
其次,開發(fā)性能測試工具,一個(gè)工具用來制造高強(qiáng)度的 Throughput,另一個(gè)工具用來測量 Latency。對于第一個(gè)工具,你可以參考一下“十個(gè)免費(fèi)的 Web 壓力測試工具”,關(guān)于如何測量 Latency,你可以在代碼中測量,但是這樣會(huì)影響程序的執(zhí)行,而且只能測試到程序內(nèi)部的 Latency,真正的 Latency 是整個(gè)系統(tǒng)都算上,包括操作系統(tǒng)和網(wǎng)絡(luò)的延時(shí),你可以使用 Wireshark 來抓網(wǎng)絡(luò)包來測量。這兩個(gè)工具具體怎么做,這個(gè)還請大家自己思考去了。
最后,開始性能測試。你需要不斷地提升測試的 Throughput,然后觀察系統(tǒng)的負(fù)載情況,如果系統(tǒng)頂?shù)米。蔷陀^察 Latency 的值。這樣,你就可以找到系統(tǒng)的最大負(fù)載,并且你可以知道系統(tǒng)的響應(yīng)延時(shí)是多少。
再多說一些,
關(guān)于 Latency,如果吞吐量很少,這個(gè)值估計(jì)會(huì)非常穩(wěn)定,當(dāng)吞吐量越來越大時(shí),系統(tǒng)的 Latency 會(huì)出現(xiàn)非常劇烈的抖動(dòng),所以,我們在測量 Latency 的時(shí)候,我們需要注意到 Latency 的分布,也就是說,有百分之幾的在我們允許的范圍,有百分之幾的超出了,有百分之幾的完全不可接受。也許,平均下來的 Latency 達(dá)標(biāo)了,但是其中僅有 50% 的達(dá)到了我們可接受的范圍。那也沒有意義。
關(guān)于性能測試,我們還需要定義一個(gè)時(shí)間段。比如:在某個(gè)吞吐量上持續(xù) 15 分鐘。因?yàn)楫?dāng)負(fù)載到達(dá)的時(shí)候,系統(tǒng)會(huì)變得不穩(wěn)定,當(dāng)過了一兩分鐘后,系統(tǒng)才會(huì)穩(wěn)定。另外,也有可能是,你的系統(tǒng)在這個(gè)負(fù)載下前幾分鐘還表現(xiàn)正常,然后就不穩(wěn)定了,甚至垮了。所以,需要這么一段時(shí)間。這個(gè)值,我們叫做峰值極限。
性能測試還需要做 Soak Test,也就是在某個(gè)吞吐量下,系統(tǒng)可以持續(xù)跑一周甚至更長。這個(gè)值,我們叫做系統(tǒng)的正常運(yùn)行的負(fù)載極限。
性能測試有很多很復(fù)要的東西,比如:burst test 等。 這里不能一一詳述,這里只說了一些和性能調(diào)優(yōu)相關(guān)的東西??傊?,性能測試是一細(xì)活和累活。
定位性能瓶頸
有了上面的鋪墊,我們就可以測試到到系統(tǒng)的性能了,再調(diào)優(yōu)之前,我們先來說說如何找到性能的瓶頸。我見過很多朋友會(huì)覺得這很容易,但是仔細(xì)一問,其實(shí)他們并沒有一個(gè)比較系統(tǒng)的方法。
查看操作系統(tǒng)負(fù)載
首先,當(dāng)我們系統(tǒng)有問題的時(shí)候,我們不要急于去調(diào)查我們代碼,這個(gè)毫無意義。我們首要需要看的是操作系統(tǒng)的報(bào)告。看看操作系統(tǒng)的 CPU 利用率,看看內(nèi)存使用率,看看操作系統(tǒng)的 IO,還有網(wǎng)絡(luò)的 IO,網(wǎng)絡(luò)鏈接數(shù),等等。Windows 下的 perfmon 是一個(gè)很不錯(cuò)的工具,Linux 下也有很多相關(guān)的命令和工具,比如:SystemTap,LatencyTOP,vmstat, sar, iostat, top, tcpdump 等等 。通過觀察這些數(shù)據(jù),我們就可以知道我們的軟件的性能基本上出在哪里。比如:
1、先看 CPU 利用率,如果 CPU 利用率不高,但是系統(tǒng)的 Throughput 和 Latency 上不去了,這說明我們的程序并沒有忙于計(jì)算,而是忙于別的一些事,比如 IO。(另外,CPU 的利用率還要看內(nèi)核態(tài)的和用戶態(tài)的,內(nèi)核態(tài)的一上去了,整個(gè)系統(tǒng)的性能就下來了。而對于多核 CPU 來說,CPU 0 是相當(dāng)關(guān)鍵的,如果 CPU 0 的負(fù)載高,那么會(huì)影響其它核的性能,因?yàn)?CPU 各核間是需要有調(diào)度的,這靠 CPU0 完成)
2、然后,我們可以看一下 IO 大不大,IO 和 CPU 一般是反著來的,CPU 利用率高則 IO 不大,IO 大則 CPU 就小。關(guān)于 IO,我們要看三個(gè)事,一個(gè)是磁盤文件 IO,一個(gè)是驅(qū)動(dòng)程序的 IO(如:網(wǎng)卡),一個(gè)是內(nèi)存換頁率。這三個(gè)事都會(huì)影響系統(tǒng)性能。
3、然后,查看一下網(wǎng)絡(luò)帶寬使用情況,在 Linux 下,你可以使用 iftop, iptraf, ntop, tcpdump 這些命令來查看?;蚴怯? Wireshark 來查看。
4、如果 CPU 不高,IO 不高,內(nèi)存使用不高,網(wǎng)絡(luò)帶寬使用不高。但是系統(tǒng)的性能上不去。這說明你的程序有問題,比如,你的程序被阻塞了??赡苁且?yàn)榈饶莻€(gè)鎖,可能是因?yàn)榈饶硞€(gè)資源,或者是在切換上下文。
通過了解操作系統(tǒng)的性能,我們才知道性能的問題,比如:帶寬不夠,內(nèi)存不夠,TCP 緩沖區(qū)不夠,等等,很多時(shí)候,不需要調(diào)整程序的,只需要調(diào)整一下硬件或操作系統(tǒng)的配置就可以了。
使用 Profiler 測試
接下來,我們需要使用性能檢測工具,也就是使用某個(gè) Profiler 來差看一下我們程序的運(yùn)行性能。如:Java 的 JProfiler/TPTP/CodePro Profiler,GNU 的 gprof,IBM 的 PurifyPlus,Intel 的 VTune,AMD 的 CodeAnalyst,還有 Linux 下的 OProfile/perf,后面兩個(gè)可以讓你對你的代碼優(yōu)化到 CPU 的微指令級別,如果你關(guān)心 CPU 的 L1/L2 的緩存調(diào)優(yōu),那么你需要考慮一下使用 VTune。 使用這些 Profiler 工具,可以讓你程序中各個(gè)模塊函數(shù)甚至指令的很多東西,如:運(yùn)行的時(shí)間 ,調(diào)用的次數(shù),CPU 的利用率,等等。這些東西對我們來說非常有用。
我們重點(diǎn)觀察運(yùn)行時(shí)間最多,調(diào)用次數(shù)最多的那些函數(shù)和指令。這里注意一下,對于調(diào)用次數(shù)多但是時(shí)間很短的函數(shù),你可能只需要輕微優(yōu)化一下,你的性能就上去了(比如:某函數(shù)一秒種被調(diào)用 100 萬次,你想想如果你讓這個(gè)函數(shù)提高 0.01 毫秒的時(shí)間 ,這會(huì)給你帶來多大的性能)
使用 Profiler 有個(gè)問題我們需要注意一下,因?yàn)?Profiler 會(huì)讓你的程序運(yùn)行的性能變低,像 PurifyPlus 這樣的工具會(huì)在你的代碼中插入很多代碼,會(huì)導(dǎo)致你的程序運(yùn)行效率變低,從而沒發(fā)測試出在高吞吐量下的系統(tǒng)的性能,對此,一般有兩個(gè)方法來定位系統(tǒng)瓶頸:
1、在你的代碼中自己做統(tǒng)計(jì),使用微秒級的計(jì)時(shí)器和函數(shù)調(diào)用計(jì)算器,每隔 10 秒把統(tǒng)計(jì) log 到文件中。
2、分段注釋你的代碼塊,讓一些函數(shù)空轉(zhuǎn),做 Hard Code 的 Mock,然后再測試一下系統(tǒng)的 Throughput 和 Latency 是否有質(zhì)的變化,如果有,那么被注釋的函數(shù)就是性能瓶頸,再在這個(gè)函數(shù)體內(nèi)注釋代碼,直到找到最耗性能的語句。
最后再說一點(diǎn),對于性能測試,不同的 Throughput 會(huì)出現(xiàn)不同的測試結(jié)果,不同的測試數(shù)據(jù)也會(huì)有不同的測試結(jié)果。所以,用于性能測試的數(shù)據(jù)非常重要,性能測試中,我們需要觀測試不同 Throughput 的結(jié)果。
常見的系統(tǒng)瓶頸
下面這些東西是我所經(jīng)歷過的一些問題,也許并不全,也許并不對,大家可以補(bǔ)充指正,我純屬拋磚引玉。
一般來說,性能優(yōu)化也就是下面的幾個(gè)策略:
用空間換時(shí)間。各種 cache 如 CPU L1/L2/RAM 到硬盤,都是用空間來換時(shí)間的策略。這樣策略基本上是把計(jì)算的過程一步一步的保存或緩存下來,這樣就不用每次用的時(shí)候都要再計(jì)算一遍,比如數(shù)據(jù)緩沖,CDN,等。這樣的策略還表現(xiàn)為冗余數(shù)據(jù),比如數(shù)據(jù)鏡象,負(fù)載均衡什么的。
用時(shí)間換空間。有時(shí)候,少量的空間可能性能會(huì)更好,比如網(wǎng)絡(luò)傳輸,如果有一些壓縮數(shù)據(jù)的算法(如前些天說的“Huffman 編碼壓縮算法” 和 “rsync 的核心算法”),這樣的算法其實(shí)很耗時(shí),但是因?yàn)槠款i在網(wǎng)絡(luò)傳輸,所以用時(shí)間來換空間反而能省時(shí)間。
簡化代碼。最高效的程序就是不執(zhí)行任何代碼的程序,所以,代碼越少性能就越高。關(guān)于代碼級優(yōu)化的技術(shù)大學(xué)里的教科書有很多示例了。如:減少循環(huán)的層數(shù),減少遞歸,在循環(huán)中少聲明變量,少做分配和釋放內(nèi)存的操作,盡量把循環(huán)體內(nèi)的表達(dá)式抽到循環(huán)外,條件表達(dá)的中的多個(gè)條件判斷的次序,盡量在程序啟動(dòng)時(shí)把一些東西準(zhǔn)備好,注意函數(shù)調(diào)用的開銷(棧上開銷),注意面向?qū)ο笳Z言中臨時(shí)對象的開銷,小心使用異常(不要用異常來檢查一些可接受可忽略并經(jīng)常發(fā)生的錯(cuò)誤),…… 等等,等等,這連東西需要我們非常了解編程語言和常用的庫。
并行處理。如果 CPU 只有一個(gè)核,你要玩多進(jìn)程,多線程,對于計(jì)算密集型的軟件會(huì)反而更慢(因?yàn)椴僮飨到y(tǒng)調(diào)度和切換開銷很大),CPU 的核多了才能真正體現(xiàn)出多進(jìn)程多線程的優(yōu)勢。并行處理需要我們的程序有 Scalability,不能水平或垂直擴(kuò)展的程序無法進(jìn)行并行處理。從架構(gòu)上來說,這表再為——是否可以做到不改代碼只是加加機(jī)器就可以完成性能提升?
總之,根據(jù) 2:8 原則來說,20% 的代碼耗了你 80% 的性能,找到那 20% 的代碼,你就可以優(yōu)化那 80% 的性能。下面的一些東西都是我的一些經(jīng)驗(yàn),我只例舉了一些最有價(jià)值的性能調(diào)優(yōu)的的方法,供你參考,也歡迎補(bǔ)充。
算法調(diào)優(yōu)
算法非常重要,好的算法會(huì)有更好的性能。舉幾個(gè)我經(jīng)歷過的項(xiàng)目的例子,大家可以感覺一下。
一個(gè)是 過濾算法,系統(tǒng)需要對收到的請求做過濾,我們把可以被 filter in/out 的東西配置在了一個(gè)文件中,原有的過濾算法是遍歷過濾配置,后來,我們找到了一種方法可以對這個(gè)過濾配置進(jìn)行排序,這樣就可以用二分折半的方法來過濾,系統(tǒng)性能增加了 50%。
一個(gè)是 哈希算法。計(jì)算哈希算法的函數(shù)并不高效,一方面是計(jì)算太費(fèi)時(shí),另一方面是碰撞太高,碰撞高了就跟單向鏈表一個(gè)性能(可參看 Hash Collision DoS 問題)。我們知道,算法都是和需要處理的數(shù)據(jù)很有關(guān)系的,就算是被大家所嘲笑的“冒泡排序”在某些情況下(大多數(shù)數(shù)據(jù)是排好序的)其效率會(huì)高于所有的排序算法。哈希算法也一樣,廣為人知的哈希算法都是用英文字典做測試,但是我們的業(yè)務(wù)在數(shù)據(jù)有其特殊性,所以,對于還需要根據(jù)自己的數(shù)據(jù)來挑選適合的哈希算法。對于我以前的一個(gè)項(xiàng)目,公司內(nèi)某牛人給我發(fā)來了一個(gè)哈希算法,結(jié)果讓我們的系統(tǒng)性能上升了 150%。(關(guān)于各種哈希算法,你一定要看看 StackExchange 上的這篇關(guān)于各種 hash 算法的文章 )
分而治之和預(yù)處理。以前有一個(gè)程序?yàn)榱松稍聢?bào)表,每次都需要計(jì)算很長的時(shí)間,有時(shí)候需要花將近一整天的時(shí)間。于是我們把我們找到了一種方法可以把這個(gè)算法發(fā)成增量式的,也就是說我每天都把當(dāng)天的數(shù)據(jù)計(jì)算好了后和前一天的報(bào)表合并,這樣可以大大的節(jié)省計(jì)算時(shí)間,每天的數(shù)據(jù)計(jì)算量只需要 20 分鐘,但是如果我要算整個(gè)月的,系統(tǒng)則需要 10 個(gè)小時(shí)以上(SQL 語句在大數(shù)據(jù)量面前性能成級數(shù)性下降)。這種分而治之的思路在大數(shù)據(jù)面前對性能有很幫助,就像 merge 排序一樣。SQL 語句和數(shù)據(jù)庫的性能優(yōu)化也是這一策略,如:使用嵌套式的 Select 而不是笛卡爾積的 Select,使用視圖,等等。
代碼調(diào)優(yōu)
從我的經(jīng)驗(yàn)上來說,代碼上的調(diào)優(yōu)有下面這幾點(diǎn):
字符串操作。這是最費(fèi)系統(tǒng)性能的事了,無論是 strcpy, strcat 還是 strlen,最需要注意的是字符串子串匹配。所以,能用整型最好用整型。舉幾個(gè)例子,第一個(gè)例子是 N 年前做銀行的時(shí)候,我的同事喜歡把日期存成字符串(如:2012-05-29 08:30:02),我勒個(gè)去,一個(gè) select where between 語句相當(dāng)耗時(shí)。
另一個(gè)例子是,我以前有個(gè)同事把一些狀態(tài)碼用字符串來處理,他的理由是,這樣可以在界面上直接顯示,后來性能調(diào)優(yōu)的時(shí)候,我把這些狀態(tài)碼全改成整型,然后用位操作查狀態(tài),因?yàn)橛幸粋€(gè)每秒鐘被調(diào)用了 150K 次的函數(shù)里面有三處需要檢查狀態(tài),經(jīng)過改善以后,整個(gè)系統(tǒng)的性能上升了 30% 左右。還有一個(gè)例子是,我以前從事的某個(gè)產(chǎn)品編程規(guī)范中有一條是要在每個(gè)函數(shù)中把函數(shù)名定義出來,如:const char fname[]=”functionName()”, 這是為了好打日志,但是為什么不聲明成 static 類型的呢?
多線程調(diào)優(yōu)。有人說,thread is evil,這個(gè)對于系統(tǒng)性能在某些時(shí)候是個(gè)問題。因?yàn)槎嗑€程瓶頸就在于互斥和同步的鎖上,以及線程上下文切換的成本,怎么樣的少用鎖或不用鎖是根本(比如:多版本并發(fā)控制 (MVCC) 在分布式系統(tǒng)中的應(yīng)用 中說的樂觀鎖可以解決性能問題),此外,還有讀寫鎖也可以解決大多數(shù)是讀操作的并發(fā)的性能問題。
這里多說一點(diǎn)在 C++ 中,我們可能會(huì)使用線程安全的智能指針 AutoPtr 或是別的一些容器,只要是線程安全的,其不管三七二十一都要上鎖,上鎖是個(gè)成本很高的操作,使用 AutoPtr 會(huì)讓我們的系統(tǒng)性能下降得很快,如果你可以保證不會(huì)有線程并發(fā)問題,那么你應(yīng)該不要用 AutoPtr。我記得我上次我們同事去掉智能指針的引用計(jì)數(shù),讓系統(tǒng)性能提升了 50% 以上。對于 Java 對象的引用計(jì)數(shù),如果我猜的沒錯(cuò)的話,到處都是鎖,所以,Java 的性能問題一直是個(gè)問題。另外,線程不是越多越好,線程間的調(diào)度和上下文切換也是很夸張的事,盡可能的在一個(gè)線程里干,盡可能的不要同步線程。這會(huì)讓你有很多的性能。
內(nèi)存分配。不要小看程序的內(nèi)存分配。malloc/realloc/calloc 這樣的系統(tǒng)調(diào)非常耗時(shí),尤其是當(dāng)內(nèi)存出現(xiàn)碎片的時(shí)候。我以前的公司出過這樣一個(gè)問題——在用戶的站點(diǎn)上,我們的程序有一天不響應(yīng)了,用 GDB 跟進(jìn)去一看,系統(tǒng) hang 在了 malloc 操作上,20 秒都沒有返回,重啟一些系統(tǒng)就好了。這就是內(nèi)存碎片的問題。這就是為什么很多人抱怨 STL 有嚴(yán)重的內(nèi)存碎片的問題,因?yàn)樘嗟男?nèi)存的分配釋放了。有很多人會(huì)以為用內(nèi)存池可以解決這個(gè)問題,但是實(shí)際上他們只是重新發(fā)明了 Runtime-C 或操作系統(tǒng)的內(nèi)存管理機(jī)制,完全于事無補(bǔ)。
當(dāng)然解決內(nèi)存碎片的問題還是通過內(nèi)存池,具體來說是一系列不同尺寸的內(nèi)存池(這個(gè)留給大家自己去思考)。當(dāng)然,少進(jìn)行動(dòng)態(tài)內(nèi)存分配是最好的。說到內(nèi)存池就需要說一下池化技術(shù)。比如線程池,連接池等。池化技術(shù)對于一些短作業(yè)來說(如 http 服務(wù)) 相當(dāng)相當(dāng)?shù)挠行?。這項(xiàng)技術(shù)可以減少鏈接建立,線程創(chuàng)建的開銷,從而提高性能。
異步操作。我們知道 Unix 下的文件操作是有 block 和 non-block 的方式的,像有些系統(tǒng)調(diào)用也是 block 式的,如:Socket 下的 select,Windows 下的 WaitforObject 之類的,如果我們的程序是同步操作,那么會(huì)非常影響性能,我們可以改成異步的,但是改成異步的方式會(huì)讓你的程序變復(fù)雜。異步方式一般要通過隊(duì)列,要注間隊(duì)列的性能問題,另外,異步下的狀態(tài)通知通常是個(gè)問題,比如消息事件通知方式,有 callback 方式,等,這些方式同樣可能會(huì)影響你的性能。但是通常來說,異步操作會(huì)讓性能的吞吐率有很大提升(Throughput),但是會(huì)犧牲系統(tǒng)的響應(yīng)時(shí)間(latency)。這需要業(yè)務(wù)上支持。
語言和代碼庫。我們要熟悉語言以及所使用的函數(shù)庫或類庫的性能。比如:STL 中的很多容器分配了內(nèi)存后,那怕你刪除元素,內(nèi)存也不會(huì)回收,其會(huì)造成內(nèi)存泄露的假像,并可能造成內(nèi)存碎片問題。再如,STL 某些容器的 size()==0 和 empty() 是不一樣的,因?yàn)?,size() 是 O(n) 復(fù)雜度,empty() 是 O(1) 的復(fù)雜度,這個(gè)要小心。Java 中的 JVM 調(diào)優(yōu)需要使用的這些參數(shù):-Xms -Xmx -Xmn -XX:SurvivorRatio -XX:MaxTenuringThreshold,還需要注意 JVM 的 GC,GC 的霸氣大家都知道,尤其是 full GC(還整理內(nèi)存碎片),他就像“恐龍?zhí)丶壙速愄?hào)”一樣,他運(yùn)行的時(shí)候,整個(gè)世界的時(shí)間都停止了。
網(wǎng)絡(luò)調(diào)優(yōu)
關(guān)于網(wǎng)絡(luò)調(diào)優(yōu),尤其是 TCP Tuning(你可以以這兩個(gè)關(guān)鍵詞在網(wǎng)上找到很多文章),這里面有很多很多東西可以說。看看 Linux 下 TCP/IP 的那么多參數(shù)就知道了(順便說一下,你也許不喜歡 Linux,但是你不能否認(rèn) Linux 給我們了很多可以進(jìn)行內(nèi)核調(diào)優(yōu)的權(quán)力)。強(qiáng)烈建議大家看看《TCP/IP 詳解 卷 1: 協(xié)議》這本書。我在這里只講一些概念上的東西。
TCP 調(diào)優(yōu)
我們知道 TCP 鏈接是有很多開銷的,一個(gè)是會(huì)占用文件描述符,另一個(gè)是會(huì)開緩存,一般來說一個(gè)系統(tǒng)可以支持的 TCP 鏈接數(shù)是有限的,我們需要清楚地認(rèn)識(shí)到 TCP 鏈接對系統(tǒng)的開銷是很大的。正是因?yàn)?TCP 是耗資源的,所以,很多攻擊都是讓你系統(tǒng)上出現(xiàn)大量的 TCP 鏈接,把你的系統(tǒng)資源耗盡。比如著名的 SYNC Flood 攻擊。
所以,我們要注意配置 KeepAlive 參數(shù),這個(gè)參數(shù)的意思是定義一個(gè)時(shí)間,如果鏈接上沒有數(shù)據(jù)傳輸,系統(tǒng)會(huì)在這個(gè)時(shí)間發(fā)一個(gè)包,如果沒有收到回應(yīng),那么 TCP 就認(rèn)為鏈接斷了,然后就會(huì)把鏈接關(guān)閉,這樣可以回收系統(tǒng)資源開銷。(注:HTTP 層上也有 KeepAlive 參數(shù))對于像 HTTP 這樣的短鏈接,設(shè)置一個(gè) 1-2 分鐘的 keepalive 非常重要。這可以在一定程度上防止 DoS 攻擊。有下面幾個(gè)參數(shù)(下面這些參數(shù)的值僅供參考):
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_intvl = 20
net.ipv4.tcp_fin_timeout = 30
對于 TCP 的 TIME_WAIT 這個(gè)狀態(tài),主動(dòng)關(guān)閉的一方進(jìn)入 TIME_WAIT 狀態(tài),TIME_WAIT 狀態(tài)將持續(xù) 2 個(gè) MSL(Max Segment Lifetime),默認(rèn)為 4 分鐘,TIME_WAIT 狀態(tài)下的資源不能回收。有大量的 TIME_WAIT 鏈接的情況一般是在 HTTP 服務(wù)器上。對此,有兩個(gè)參數(shù)需要注意,
net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_tw_recycle=1
前者表示重用 TIME_WAIT,后者表示回收 TIME_WAIT 的資源。
TCP 還有一個(gè)重要的概念叫 RWIN(TCP Receive Window Size),這個(gè)東西的意思是,我一個(gè) TCP 鏈接在沒有向 Sender 發(fā)出 ack 時(shí)可以接收到的最大的數(shù)據(jù)包。為什么這個(gè)很重要?因?yàn)槿绻?Sender 沒有收到 Receiver 發(fā)過來 ack,Sender 就會(huì)停止發(fā)送數(shù)據(jù)并會(huì)等一段時(shí)間,如果超時(shí),那么就會(huì)重傳。這就是為什么 TCP 鏈接是可靠鏈接的原因。重傳還不是最嚴(yán)重的,如果有丟包發(fā)生的話,TCP 的帶寬使用率會(huì)馬上受到影響(會(huì)盲目減半),再丟包,再減半,然后如果不丟包了,就逐步恢復(fù)。相關(guān)參數(shù)如下:
net.core.wmem_default = 8388608
net.core.rmem_default = 8388608
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
一般來說,理論上的 RWIN 應(yīng)該設(shè)置成:吞吐量 * 回路時(shí)間。Sender 端的 buffer 應(yīng)該和 RWIN 有一樣的大小,因?yàn)?Sender 端發(fā)送完數(shù)據(jù)后要等 Receiver 端確認(rèn),如果網(wǎng)絡(luò)延時(shí)很大,buffer 過小了,確認(rèn)的次數(shù)就會(huì)多,于是性能就不高,對網(wǎng)絡(luò)的利用率也就不高了。也就是說,對于延遲大的網(wǎng)絡(luò),我們需要大的 buffer,這樣可以少一點(diǎn) ack,多一些數(shù)據(jù),對于響應(yīng)快一點(diǎn)的網(wǎng)絡(luò),可以少一些 buffer。
因?yàn)椋绻衼G包(沒有收到 ack),buffer 過大可能會(huì)有問題,因?yàn)檫@會(huì)讓 TCP 重傳所有的數(shù)據(jù),反而影響網(wǎng)絡(luò)性能。(當(dāng)然,網(wǎng)絡(luò)差的情況下,就別玩什么高性能了) 所以,高性能的網(wǎng)絡(luò)重要的是要讓網(wǎng)絡(luò)丟包率非常非常地小(基本上是用在 LAN 里),如果網(wǎng)絡(luò)基本是可信的,這樣用大一點(diǎn)的 buffer 會(huì)有更好的網(wǎng)絡(luò)傳輸性能(來來回回太多太影響性能了)。
另外,我們想一想,如果網(wǎng)絡(luò)質(zhì)量非常好,基本不丟包,而業(yè)務(wù)上我們不怕偶爾丟幾個(gè)包,如果是這樣的話,那么,我們?yōu)槭裁床挥盟俣雀斓?UDP 呢?你想過這個(gè)問題了嗎?
UDP 調(diào)優(yōu)
說到 UDP 的調(diào)優(yōu),有一些事我想重點(diǎn)說一樣,那就是 MTU——最大傳輸單元(其實(shí)這對 TCP 也一樣,因?yàn)檫@是鏈路層上的東西)。所謂最大傳輸單元,你可以想像成是公路上的公交車,假設(shè)一個(gè)公交車可以最多坐 70 人,帶寬就像是公路的車道數(shù)一樣,如果一條路上最多可以容下 100 輛公交車,那意味著我最多可以運(yùn)送 7000 人,但是如果公交車坐不滿,比如平均每輛車只有 20 人,那么我只運(yùn)送了 2000 人,于是我公路資源(帶寬資源)就被浪費(fèi)了。 所以,我們對于一個(gè) UDP 的包,我們要盡量地讓他大到 MTU 的最大尺寸再往網(wǎng)絡(luò)上傳,這樣可以最大化帶寬利用率。
對于這個(gè) MTU,以太網(wǎng)是 1500 字節(jié),光纖是 4352 字節(jié),802.11 無線網(wǎng)是 7981。但是,當(dāng)我們用 TCP/UDP 發(fā)包的時(shí)候,我們的有效負(fù)載 Payload 要低于這個(gè)值,因?yàn)?IP 協(xié)議會(huì)加上 20 個(gè)字節(jié),UDP 會(huì)加上 8 個(gè)字節(jié)(TCP 加的更多),所以,一般來說,你的一個(gè) UDP 包的最大應(yīng)該是 1500-8-20=1472,這是你的數(shù)據(jù)的大小。當(dāng)然,如果你用光纖的話, 這個(gè)值就可以更大一些。(順便說一下,對于某些 NB 的千光以態(tài)網(wǎng)網(wǎng)卡來說,在網(wǎng)卡上,網(wǎng)卡硬件如果發(fā)現(xiàn)你的包的大小超過了 MTU,其會(huì)幫你做 fragment,到了目標(biāo)端又會(huì)幫你做重組,這就不需要你在程序中處理了)
再多說一下,使用 Socket 編程的時(shí)候,你可以使用 setsockopt() 設(shè)置 SO_SNDBUF/SO_RCVBUF 的大小,TTL 和 KeepAlive 這些關(guān)鍵的設(shè)置,當(dāng)然,還有很多,具體你可以查看一下 Socket 的手冊。
最后說一點(diǎn),UDP 還有一個(gè)最大的好處是 multi-cast 多播,這個(gè)技術(shù)對于你需要在內(nèi)網(wǎng)里通知多臺(tái)結(jié)點(diǎn)時(shí)非常方便和高效。而且,多播這種技術(shù)對于機(jī)會(huì)的水平擴(kuò)展(需要增加機(jī)器來偵聽多播信息)也很有利。
網(wǎng)卡調(diào)優(yōu)
對于網(wǎng)卡,我們也是可以調(diào)優(yōu)的,這對于千兆以及網(wǎng)網(wǎng)卡非常必要,在 Linux 下,我們可以用 ifconfig 查看網(wǎng)上的統(tǒng)計(jì)信息,如果我們看到 overrun 上有數(shù)據(jù),我們就可能需要調(diào)整一下 txqueuelen 的尺寸(一般默認(rèn)為 1000),我們可以調(diào)大一些,如:ifconfig eth0 txqueuelen 5000。Linux 下還有一個(gè)命令叫:ethtool 可以用于設(shè)置網(wǎng)卡的緩沖區(qū)大小。在 Windows 下,我們可以在網(wǎng)卡適配器中的高級選項(xiàng)卡中調(diào)整相關(guān)的參數(shù)(如:Receive Buffers, Transmit Buffer 等,不同的網(wǎng)卡有不同的參數(shù))。把 Buffer 調(diào)大對于需要大數(shù)據(jù)量的網(wǎng)絡(luò)傳輸非常有效。
其它網(wǎng)絡(luò)性能
關(guān)于多路復(fù)用技術(shù),也就是用一個(gè)線程來管理所有的 TCP 鏈接,有三個(gè)系統(tǒng)調(diào)用要重點(diǎn)注意:一個(gè)是 select,這個(gè)系統(tǒng)調(diào)用只支持上限 1024 個(gè)鏈接,第二個(gè)是 poll,其可以突破 1024 的限制,但是 select 和 poll 本質(zhì)上是使用的輪詢機(jī)制,輪詢機(jī)制在鏈接多的時(shí)候性能很差,因主是 O(n) 的算法,所以,epoll 出現(xiàn)了,epoll 是操作系統(tǒng)內(nèi)核支持的,僅當(dāng)在鏈接活躍時(shí),操作系統(tǒng)才會(huì) callback,這是由操作系統(tǒng)通知觸發(fā)的,但其只有 Linux Kernel 2.6 以后才支持(準(zhǔn)確說是 2.5.44 中引入的),當(dāng)然,如果所有的鏈接都是活躍的,過多的使用 epoll_ctl 可能會(huì)比輪詢的方式還影響性能,不過影響的不大。
另外,關(guān)于一些和 DNS Lookup 的系統(tǒng)調(diào)用要小心,比如:gethostbyaddr/gethostbyname,這個(gè)函數(shù)可能會(huì)相當(dāng)?shù)馁M(fèi)時(shí),因?yàn)槠湟骄W(wǎng)絡(luò)上去找域名,因?yàn)?DNS 的遞歸查詢,會(huì)導(dǎo)致嚴(yán)重超時(shí),而又不能通過設(shè)置什么參數(shù)來設(shè)置 time out,對此你可以通過配置 hosts 文件來加快速度,或是自己在內(nèi)存中管理對應(yīng)表,在程序啟動(dòng)時(shí)查好,而不要在運(yùn)行時(shí)每次都查。
另外,在多線程下面,gethostbyname 會(huì)一個(gè)更嚴(yán)重的問題,就是如果有一個(gè)線程的 gethostbyname 發(fā)生阻塞,其它線程都會(huì)在 gethostbyname 處發(fā)生阻塞,這個(gè)比較變態(tài),要小心。(你可以試試 GNU 的 gethostbyname_r(),這個(gè)的性能要好一些) 這種到網(wǎng)上找信息的東西很多,比如,如果你的 Linux 使用了 NIS,或是 NFS,某些用戶或文件相關(guān)的系統(tǒng)調(diào)用就很慢,所以要小心。
系統(tǒng)調(diào)優(yōu)
I/O 模型
前面說到過 select/poll/epoll 這三個(gè)系統(tǒng)調(diào)用,我們都知道,Unix/Linux 下把所有的設(shè)備都當(dāng)成文件來進(jìn)行 I/O,所以,那三個(gè)操作更應(yīng)該算是 I/O 相關(guān)的系統(tǒng)調(diào)用。說到 I/O 模型,這對于我們的 I/O 性能相當(dāng)重要,我們知道,Unix/Linux 經(jīng)典的 I/O 方式是:
第一種,同步阻塞式 I/O,這個(gè)不說了。
第二種,同步無阻塞方式。其通過 fctnl 設(shè)置 O_NONBLOCK 來完成。
第三種,對于 select/poll/epoll 這三個(gè)是 I/O 不阻塞,但是在事件上阻塞,算是:I/O 異步,事件同步的調(diào)用。
第四種,AIO 方式。這種 I/O 模型是一種處理與 I/O 并行的模型。I/O 請求會(huì)立即返回,說明請求已經(jīng)成功發(fā)起了。在后臺(tái)完成 I/O 操作時(shí),向應(yīng)用程序發(fā)起通知,通知有兩種方式:一種是產(chǎn)生一個(gè)信號(hào),另一種是執(zhí)行一個(gè)基于線程的回調(diào)函數(shù)來完成這次 I/O 處理過程。
第四種因?yàn)闆]有任何的阻塞,無論是 I/O 上,還是事件通知上,所以,其可以讓你充分地利用 CPU,比起第二種同步無阻塞好處就是,第二種要你一遍一遍地去輪詢。Nginx 之所所以高效,是其使用了 epoll 和 AIO 的方式來進(jìn)行 I/O 的。
再說一下 Windows 下的 I/O 模型,
a)一個(gè)是 WriteFile 系統(tǒng)調(diào)用,這個(gè)系統(tǒng)調(diào)用可以是同步阻塞的,也可以是同步無阻塞的,關(guān)于看文件是不是以 Overlapped 打開的。關(guān)于同步無阻塞,需要設(shè)置其最后一個(gè)參數(shù) Overlapped,微軟叫 Overlapped I/O,你需要 WaitForSingleObject 才能知道有沒有寫完成。這個(gè)系統(tǒng)調(diào)用的性能可想而知。
b)另一個(gè)叫 WriteFileEx 的系統(tǒng)調(diào)用,其可以實(shí)現(xiàn)異步 I/O,并可以讓你傳入一個(gè) callback 函數(shù),等 I/O 結(jié)束后回調(diào)之, 但是這個(gè)回調(diào)的過程 Windows 是把 callback 函數(shù)放到了 APC(Asynchronous Procedure Calls)的隊(duì)列中,然后,只用當(dāng)應(yīng)用程序當(dāng)前線程成為可被通知狀態(tài)(Alterable)時(shí),才會(huì)被回調(diào)。只有當(dāng)你的線程使用了這幾個(gè)函數(shù)時(shí) WaitForSingleObjectEx, WaitForMultipleObjectsEx, MsgWaitForMultipleObjectsEx, SignalObjectAndWait 和 SleepEx,線程才會(huì)成為 Alterable 狀態(tài)??梢姡@個(gè)模型,還是有 wait,所以性能也不高。
c)然后是 IOCP – IO Completion Port,IOCP 會(huì)把 I/O 的結(jié)果放在一個(gè)隊(duì)列中,但是,偵聽這個(gè)隊(duì)列的不是主線程,而是專門來干這個(gè)事的一個(gè)或多個(gè)線程去干(老的平臺(tái)要你自己創(chuàng)建線程,新的平臺(tái)是你可以創(chuàng)建一個(gè)線程池)。IOCP 是一個(gè)線程池模型。這個(gè)和 Linux 下的 AIO 模型比較相似,但是實(shí)現(xiàn)方式和使用方式完全不一樣。
當(dāng)然,真正提高 I/O 性能方式是把和外設(shè)的 I/O 的次數(shù)降到最低,最好沒有,所以,對于讀來說,內(nèi)存 cache 通常可以從質(zhì)上提升性能,因?yàn)閮?nèi)存比外設(shè)快太多了。對于寫來說,cache 住要寫的數(shù)據(jù),少寫幾次,但是 cache 帶來的問題就是實(shí)時(shí)性的問題,也就是 latency 會(huì)變大,我們需要在寫的次數(shù)上和相應(yīng)上做權(quán)衡。
多核 CPU 調(diào)優(yōu)
關(guān)于 CPU 的多核技術(shù),我們知道,CPU0 是很關(guān)鍵的,如果 0 號(hào) CPU 被用得過狠的話,別的 CPU 性能也會(huì)下降,因?yàn)?CPU0 是有調(diào)整功能的,所以,我們不能任由操作系統(tǒng)負(fù)載均衡,因?yàn)槲覀冏约焊私庾约旱某绦颍?,我們可以手?dòng)地為其分配 CPU 核,而不會(huì)過多地占用 CPU0,或是讓我們關(guān)鍵進(jìn)程和一堆別的進(jìn)程擠在一起。
對于 Windows 來說,我們可以通過“任務(wù)管理器”中的“進(jìn)程”而中右鍵菜單中的“設(shè)置相關(guān)性……”(Set Affinity…)來設(shè)置并限制這個(gè)進(jìn)程能被運(yùn)行在哪些核上。
對于 Linux 來說,可以使用 taskset 命令來設(shè)置(你可以通過安裝 schedutils 來安裝這個(gè)命令:apt-get install schedutils)
多核 CPU 還有一個(gè)技術(shù)叫 NUMA 技術(shù)(Non-Uniform Memory Access)。傳統(tǒng)的多核運(yùn)算是使用 SMP(Symmetric Multi-Processor ) 模式,多個(gè)處理器共享一個(gè)集中的存儲(chǔ)器和 I/O 總線。于是就會(huì)出現(xiàn)一致存儲(chǔ)器訪問的問題,一致性通常意味著性能問題。NUMA 模式下,處理器被劃分成多個(gè) node, 每個(gè) node 有自己的本地存儲(chǔ)器空間。關(guān)于 NUMA 的一些技術(shù)細(xì)節(jié),你可以查看一下這篇文章《Linux 的 NUMA 技術(shù)》,在 Linux 下,對 NUMA 調(diào)優(yōu)的命令是:numactl 。如下面的命令:(指定命令“myprogram arg1 arg2”運(yùn)行在 node 0 上,其內(nèi)存分配在 node 0 和 1 上)
numactl --cpubind=0 --membind=0,1 myprogram arg1 arg2
當(dāng)然,上面這個(gè)命令并不好,因?yàn)閮?nèi)存跨越了兩個(gè) node,這非常不好。最好的方式是只讓程序訪問和自己運(yùn)行一樣的 node,如:
$ numactl --membind 1 --cpunodebind 1 --localalloc myapplication
文件系統(tǒng)調(diào)優(yōu)關(guān)于文件系統(tǒng),因?yàn)槲募到y(tǒng)也是有 cache 的,所以,為了讓文件系統(tǒng)有最大的性能。首要的事情就是分配足夠大的內(nèi)存,這個(gè)非常關(guān)鍵,在 Linux 下可以使用 free 命令來查看 free/used/buffers/cached,理想來說,buffers 和 cached 應(yīng)該有 40% 左右。然后是一個(gè)快速的硬盤控制器,SCSI 會(huì)好很多。最快的是 Intel SSD 固態(tài)硬盤,速度超快,但是寫次數(shù)有限。
接下來,我們就可以調(diào)優(yōu)文件系統(tǒng)配置了,對于 Linux 的 Ext3/4 來說,幾乎在所有情況下都有所幫助的一個(gè)參數(shù)是關(guān)閉文件系統(tǒng)訪問時(shí)間,在 /etc/fstab 下看看你的文件系統(tǒng) 有沒有 noatime 參數(shù)(一般來說應(yīng)該有),還有一個(gè)是 dealloc,它可以讓系統(tǒng)在最后時(shí)刻決定寫入文件發(fā)生時(shí)使用哪個(gè)塊,可優(yōu)化這個(gè)寫入程序。還要注間一下三種日志模式:data=journal、data=ordered 和 data=writeback。默認(rèn)設(shè)置 data=ordered 提供性能和防護(hù)之間的最佳平衡。
當(dāng)然,對于這些來說,ext4 的默認(rèn)設(shè)置基本上是最佳優(yōu)化了。
這里介紹一個(gè) Linux 下的查看 I/O 的命令—— iotop,可以讓你看到各進(jìn)程的磁盤讀寫的負(fù)載情況。
其它還有一些關(guān)于 NFS、XFS 的調(diào)優(yōu),大家可以上 google 搜索一些相關(guān)優(yōu)化的文章看看。
數(shù)據(jù)庫調(diào)優(yōu)數(shù)據(jù)庫調(diào)優(yōu)并不是我的強(qiáng)項(xiàng),我就僅用我非常有限的知識(shí)說上一些吧。注意,下面的這些東西并不一定正確,因?yàn)樵诓煌臉I(yè)務(wù)場景,不同的數(shù)據(jù)庫設(shè)計(jì)下可能會(huì)得到完全相反的結(jié)論,所以,我僅在這里做一些一般性的說明,具體問題還要具體分析。
數(shù)據(jù)庫引擎調(diào)優(yōu)
我對數(shù)據(jù)庫引擎不是熟,但是有幾個(gè)事情我覺得是一定要去了解的。
數(shù)據(jù)庫的鎖的方式。這個(gè)非常非常地重要。并發(fā)情況下,鎖是非常非常影響性能的。各種隔離級別,行鎖,表鎖,頁鎖,讀寫鎖,事務(wù)鎖,以及各種寫優(yōu)先還是讀優(yōu)先機(jī)制。性能最高的是不要鎖,所以,分庫分表,冗余數(shù)據(jù),減少一致性事務(wù)處理,可以有效地提高性能。NoSQL 就是犧牲了一致性和事務(wù)處理,并冗余數(shù)據(jù),從而達(dá)到了分布式和高性能。
數(shù)據(jù)庫的存儲(chǔ)機(jī)制。不但要搞清楚各種類型字段是怎么存儲(chǔ)的,更重要的是數(shù)據(jù)庫的數(shù)據(jù)存儲(chǔ)方式,是怎么分區(qū)的,是怎么管理的,比如 Oracle 的數(shù)據(jù)文件,表空間,段,等等。了解清楚這個(gè)機(jī)制可以減輕很多的 I/O 負(fù)載。比如:MySQL 下使用 show engines; 可以看到各種存儲(chǔ)引擎的支持。不同的存儲(chǔ)引擎有不同的側(cè)重點(diǎn),針對不同的業(yè)務(wù)或數(shù)據(jù)庫設(shè)計(jì)會(huì)讓你有不同的性能。
數(shù)據(jù)庫的分布式策略。最簡單的就是復(fù)制或鏡像,需要了解分布式的一致性算法,或是主主同步,主從同步。通過了解這種技術(shù)的機(jī)理可以做到數(shù)據(jù)庫級別的水平擴(kuò)展。
SQL 語句優(yōu)化
關(guān)于 SQL 語句的優(yōu)化,首先也是要使用工具,比如:MySQL SQL Query Analyzer,Oracle SQL Performance Analyzer,或是微軟 SQL Query Analyzer,基本上來說,所有的 RMDB 都會(huì)有這樣的工具,來讓你查看你的應(yīng)用中的 SQL 的性能問題。 還可以使用 explain 來看看 SQL 語句最終 Execution Plan 會(huì)是什么樣的。
還有一點(diǎn)很重要,數(shù)據(jù)庫的各種操作需要大量的內(nèi)存,所以服務(wù)器的內(nèi)存要夠,優(yōu)其應(yīng)對那些多表查詢的 SQL 語句,那是相當(dāng)?shù)暮膬?nèi)存。
下面我根據(jù)我有限的數(shù)據(jù)庫 SQL 的知識(shí)說幾個(gè)會(huì)有性能問題的 SQL:
全表檢索。比如:select * from user where lastname = “xxxx”,這樣的 SQL 語句基本上是全表查找,線性復(fù)雜度 O(n),記錄數(shù)越多,性能也越差(如:100 條記錄的查找要 50ms,一百萬條記錄需要 5 分鐘)。對于這種情況,我們可以有兩種方法提高性能:一種方法是分表,把記錄數(shù)降下來,另一種方法是建索引(為 lastname 建索引)。索引就像是 key-value 的數(shù)據(jù)結(jié)構(gòu)一樣,key 就是 where 后面的字段,value 就是物理行號(hào),對索引的搜索復(fù)雜度是基本上是 O(log(n)) ——用 B-Tree 實(shí)現(xiàn)索引(如:100 條記錄的查找要 50ms,一百萬條記錄需要 100ms)。
索引。對于索引字段,最好不要在字段上做計(jì)算、類型轉(zhuǎn)換、函數(shù)、空值判斷、字段連接操作,這些操作都會(huì)破壞索引原本的性能。當(dāng)然,索引一般都出現(xiàn)在 Where 或是 Order by 字句中,所以對 Where 和 Order by 子句中的子段最好不要進(jìn)行計(jì)算操作,或是加上什么 NOT 之類的,或是使用什么函數(shù)。
多表查詢。關(guān)系型數(shù)據(jù)庫最多的操作就是多表查詢,多表查詢主要有三個(gè)關(guān)鍵字,EXISTS,IN 和 JOIN(關(guān)于各種 join,可以參看圖解 SQL 的 Join 一文)?;緛碚f,現(xiàn)代的數(shù)據(jù)引擎對 SQL 語句優(yōu)化得都挺好的,JOIN 和 IN/EXISTS 在結(jié)果上有些不同,但性能基本上都差不多。有人說,EXISTS 的性能要好于 IN,IN 的性能要好于 JOIN,我各人覺得,這個(gè)還要看你的數(shù)據(jù)、schema 和 SQL 語句的復(fù)雜度,對于一般的簡單的情況來說,都差不多,所以千萬不要使用過多的嵌套,千萬不要讓你的 SQL 太復(fù)雜,寧可使用幾個(gè)簡單的 SQL 也不要使用一個(gè)巨大無比的嵌套 N 級的 SQL。
還有人說,如果兩個(gè)表的數(shù)據(jù)量差不多,Exists 的性能可能會(huì)高于 In,In 可能會(huì)高于 Join,如果這兩個(gè)表一大一小,那么子查詢中,Exists 用大表,In 則用小表。這個(gè),我沒有驗(yàn)證過,放在這里讓大家討論吧。另,有一篇關(guān)于 SQL Server 的文章大家可以看看《IN vs JOIN vs EXISTS》
JOIN 操作。有人說,Join 表的順序會(huì)影響性能,只要 Join 的結(jié)果集是一樣,性能和 join 的次序無關(guān)。因?yàn)楹笈_(tái)的數(shù)據(jù)庫引擎會(huì)幫我們優(yōu)化的。Join 有三種實(shí)現(xiàn)算法,嵌套循環(huán),排序歸并,和 Hash 式的 Join。(MySQL 只支持第一種)
嵌套循環(huán),就好像是我們常見的多重嵌套循環(huán)。注意,前面的索引說過,數(shù)據(jù)庫的索引查找算法用的是 B-Tree,這是 O(log(n)) 的算法,所以,整個(gè)算法復(fù)法度應(yīng)該是 O(log(n)) * O(log(m)) 這樣的。
Hash 式的 Join,主要解決嵌套循環(huán)的 O(log(n)) 的復(fù)雜,使用一個(gè)臨時(shí)的 hash 表來標(biāo)記。
排序歸并,意思是兩個(gè)表按照查詢字段排好序,然后再合并。當(dāng)然,索引字段一般是排好序的。
還是那句話,具體要看什么樣的數(shù)據(jù),什么樣的 SQL 語句,你才知道用哪種方法是最好的。
部分結(jié)果集。我們知道 MySQL 里的 Limit 關(guān)鍵字,Oracle 里的 rownum,SQL Server 里的 Top 都是在限制前幾條的返回結(jié)果。這給了我們數(shù)據(jù)庫引擎很多可以調(diào)優(yōu)的空間。一般來說,返回 top n 的記錄數(shù)據(jù)需要我們使用 order by,注意在這里我們需要為 order by 的字段建立索引。有了被建索引的 order by 后,會(huì)讓我們的 select 語句的性能不會(huì)被記錄數(shù)的所影響。使用這個(gè)技術(shù),一般來說我們前臺(tái)會(huì)以分頁方式來顯現(xiàn)數(shù)據(jù),Mysql 用的是 OFFSET,SQL Server 用的是 FETCH NEXT,這種 Fetch 的方式其實(shí)并不好是線性復(fù)雜度,所以,如果我們能夠知道 order by 字段的第二頁的起始值,我們就可以在 where 語句里直接使用>= 的表達(dá)式來 select,這種技術(shù)叫 seek,而不是 fetch,seek 的性能比 fetch 要高很多。
字符串。正如我前面所說的,字符串操作對性能上有非常大的惡夢,所以,能用數(shù)據(jù)的情況就用數(shù)字,比如:時(shí)間,工號(hào),等。
全文檢索。千萬不要用 Like 之類的東西來做全文檢索,如果要玩全文檢索,可以嘗試使用 Sphinx。
其它。
不要 select *,而是明確指出各個(gè)字段,如果有多個(gè)表,一定要在字段名前加上表名,不要讓引擎去算。
不要用 Having,因?yàn)槠湟闅v所有的記錄。性能差得不能再差。
盡可能地使用 UNION ALL 取代 UNION。
索引過多,insert 和 delete 就會(huì)越慢。而 update 如果 update 多數(shù)索引,也會(huì)慢,但是如果只 update 一個(gè),則只會(huì)影響一個(gè)索引表。
等等。