高性能服務(wù)器架構(gòu)里的隱藏秘密
在提到服務(wù)器架構(gòu)時,一般只有非常資深的運(yùn)維工程師才能完完全全把這件事請給你講明白。服務(wù)器里有各種細(xì)節(jié)互相制約,如果不能很好協(xié)調(diào)這些細(xì)節(jié),幾乎沒有辦法談到服務(wù)器調(diào)優(yōu)、或者其他的任何話題。今天這篇文章,很難得的將一些細(xì)節(jié)知識講解的及其通透,因此,小編把文章放上來給大家做個參考。
引言
本文將與你分享我多年來在服務(wù)器開發(fā)方面的一些經(jīng)驗(yàn)。對于這里所說的服務(wù)器,更精確的定義應(yīng)該是每秒處理大量離散消息或者請求的服務(wù)程序,網(wǎng)絡(luò)服務(wù)器更符合這種情況,但并非所有的網(wǎng)絡(luò)程序都是嚴(yán)格意義上的服務(wù)器。使用“高性能請求處理程序”是一個很糟糕的標(biāo)題,為了敘述起來簡單,下面將簡稱為“服務(wù)器”。
本文不會涉及到多任務(wù)應(yīng)用程序,在單個程序里同時處理多個任務(wù)現(xiàn)在已經(jīng)很常見。比如你的瀏覽器可能就在做一些并行處理,但是這類并行程序設(shè)計沒有多大挑戰(zhàn)性。真正的挑戰(zhàn)出現(xiàn)在服務(wù)器的架構(gòu)設(shè)計對性能產(chǎn)生制約時,如何通過改善架構(gòu)來提升系統(tǒng)性能。對于在擁有上G內(nèi)存和G赫茲CPU上運(yùn)行的瀏覽器來說,通過DSL進(jìn)行多個并發(fā)下載任務(wù)不會有如此的挑戰(zhàn)性。這里,應(yīng)用的焦點(diǎn)不在于通過吸管小口吮吸,而是如何通過水龍頭大口暢飲,這里麻煩是如何解決在硬件性能的制約.(作者的意思應(yīng)該是怎么通過網(wǎng)絡(luò)硬件的改善來增大流量)
一些人可能會對我的某些觀點(diǎn)和建議發(fā)出置疑,或者自認(rèn)為有更好的方法,?這是無法避免的。在本文中我不想扮演上帝的角色;這里所談?wù)摰氖俏易约旱囊恍┙?jīng)驗(yàn),這些經(jīng)驗(yàn)對我來說,?不僅在提高服務(wù)器性能上有效,而且在降低調(diào)試?yán)щy度和增加系統(tǒng)的可擴(kuò)展性上也有作用。但是對某些人的系統(tǒng)可能會有所不同。如果有其它更適合于你的方法,那實(shí)在是很不錯.?但是值得注意的是,對本文中所提出的每一條建議的其它一些可替代方案,我經(jīng)過實(shí)驗(yàn)得出的結(jié)論都是悲觀的。你自己的小聰明在這些實(shí)驗(yàn)中或許有更好的表現(xiàn),但是如果因此慫恿我在這里建議讀者這么做,可能會引起無辜讀者的反感。你并不想惹怒讀者,對吧?
本文的其余部分將主要說明影響服務(wù)器性能的四大殺手:
- 數(shù)據(jù)拷貝(Data Copies)
- 環(huán)境切換(Context Switches)
- 內(nèi)存分配(Memory allocation)
- 鎖競爭(Lock contention)
在文章結(jié)尾部分還會提出其它一些比較重要的因素,但是上面的四點(diǎn)是主要因素。如果服務(wù)器在處理大部分請求時能夠做到?jīng)]有數(shù)據(jù)拷貝,沒有環(huán)境切換,沒有內(nèi)存分配,沒有鎖競爭,那么我敢保證你的服務(wù)器的性能一定很出色。
數(shù)據(jù)拷貝(Data Copies)
本節(jié)會有點(diǎn)短,因?yàn)榇蠖鄶?shù)人在數(shù)據(jù)拷貝上吸取過教訓(xùn)。幾乎每個人都知道產(chǎn)生數(shù)據(jù)拷貝是不對的,這點(diǎn)是顯而易見的,在你的職業(yè)生涯中,?你很早就會見識過它;而且遇到過這個問題,因?yàn)?0年前就有人開始說這個詞。對我來說確實(shí)如此。現(xiàn)今,幾乎每個大學(xué)課程和幾乎所有how-to文檔中都提到了它。甚至在某些商業(yè)宣傳冊中,"零拷貝"?都是個流行用語。
盡管數(shù)據(jù)拷貝的壞處顯而易見,但是還是會有人忽視它。因?yàn)楫a(chǎn)生數(shù)據(jù)拷貝的代碼常常隱藏很深且?guī)в袀窝b,你知道你所調(diào)用的庫或驅(qū)動的代碼會進(jìn)行數(shù)據(jù)拷貝嗎?答案往往超出想象。猜猜“程序I/O”在計算機(jī)上到底指什么?哈希函數(shù)是偽裝的數(shù)據(jù)拷貝的例子,它有帶拷貝的內(nèi)存訪問消耗和更多的計算。曾經(jīng)指出哈希算法是一種有效的“拷貝+”似乎能夠被避免,但據(jù)我所知,有一些非常聰明的人說過要做到這一點(diǎn)是相當(dāng)困難的。如果想真正去除數(shù)據(jù)拷貝,不管是因?yàn)橛绊懥朔?wù)器性能,還是想在黑客大會上展示"零復(fù)制”技術(shù),你必須自己跟蹤可能發(fā)生數(shù)據(jù)拷貝的所有地方,而不是輕信宣傳。
有一種可以避免數(shù)據(jù)拷貝的方法是使用buffer的描述符(或者buffer chains的描述符)來取代直接使用buffer指針,每個buffer描述符應(yīng)該由以下元素組成:
- 一個指向buffer的指針和整個buffer的長度
- 一個指向buffer中真實(shí)數(shù)據(jù)的指針和真實(shí)數(shù)據(jù)的長度,或者長度的偏移
- 以雙向鏈表的形式提供指向其它buffer的指針
- 一個引用計數(shù)
現(xiàn)在,代碼可以簡單的在相應(yīng)的描述符上增加引用計數(shù)來代替內(nèi)存中數(shù)據(jù)的拷貝。這種做法在某些條件下表現(xiàn)的相當(dāng)好,包括在典型的網(wǎng)絡(luò)協(xié)議棧的操作上,但有些情況下這做法也令人很頭大。一般來說,在buffer chains的開頭和結(jié)尾增加buffer很容易,對整個buffer增加引用計數(shù),以及對buffer chains的即刻釋放也很容易。在chains的中間增加buffer,一塊一塊的釋放buffer,或者對部分buffer增加引用技術(shù)則比較困難。而分割,組合chains會讓人立馬崩潰。
我不建議在任何情況下都使用這種技術(shù),因?yàn)楫?dāng)你想在鏈上搜索你想要的一個塊時,就不得不遍歷一遍描述符鏈,這甚至比數(shù)據(jù)拷貝更糟糕。最適用這種技術(shù)地方是在程序中大的數(shù)據(jù)塊上,這些大數(shù)據(jù)塊應(yīng)該按照上面所說的那樣獨(dú)立的分配描述符,以避免發(fā)生拷貝,也能避免影響服務(wù)器其它部分的工作.(大數(shù)據(jù)塊拷貝很消耗CPU,會影響其它并發(fā)線程的運(yùn)行)。
關(guān)于數(shù)據(jù)拷貝最后要指出的是:在避免數(shù)據(jù)拷貝時不要走極端。我看到過太多的代碼為了避免數(shù)據(jù)拷貝,最后結(jié)果反而比拷貝數(shù)據(jù)更糟糕,比如產(chǎn)生環(huán)境切換或者一個大的I/O請求被分解了。數(shù)據(jù)拷貝是昂貴的,但是在避免它時,是收益遞減的(意思是做過頭了,效果反而不好)。為了除去最后少量的數(shù)據(jù)拷貝而改變代碼,繼而讓代碼復(fù)雜度翻番,不如把時間花在其它方面。
上下文切換(Context Switches)
相對于數(shù)據(jù)拷貝影響的明顯,非常多的人會忽視了上下文切換對性能的影響。在我的經(jīng)驗(yàn)里,比起數(shù)據(jù)拷貝,上下文切換是讓高負(fù)載應(yīng)用徹底完蛋的真正殺手。系統(tǒng)更多的時間都花費(fèi)在線程切換上,而不是花在真正做有用工作的線程上。令人驚奇的是,(和數(shù)據(jù)拷貝相比)在同一個水平上,導(dǎo)致上下文切換原因總是更常見。引起環(huán)境切換的第一個原因往往是活躍線程數(shù)比CPU個數(shù)多。隨著活躍線程數(shù)相對于CPU個數(shù)的增加,上下文切換的次數(shù)也在增加,如果你夠幸運(yùn),這種增長是線性的,但更常見是指數(shù)增長。這個簡單的事實(shí)解釋了為什么每個連接一個線程的多線程設(shè)計的可伸縮性更差。對于一個可伸縮性的系統(tǒng)來說,限制活躍線程數(shù)少于或等于CPU個數(shù)是更有實(shí)際意義的方案。曾經(jīng)這種方案的一個變種是只使用一個活躍線程,雖然這種方案避免了環(huán)境爭用,同時也避免了鎖,但它不能有效利用多CPU在增加總吞吐量上的價值,因此除非程序無CPU限制(non-CPU-bound),(通常是網(wǎng)絡(luò)I/O限制?network-I/O-bound),應(yīng)該繼續(xù)使用更實(shí)際的方案。
一個有適量線程的程序首先要考慮的事情是規(guī)劃出如何創(chuàng)建一個線程去管理多連接。這通常意味著前置一個select/poll,?異步I/O,信號或者完成端口,而后臺使用一個事件驅(qū)動的程序框架。關(guān)于哪種前置API是最好的有很多爭論。?Dan Kegel的C10K在這個領(lǐng)域是一篇不錯的論文。個人認(rèn)為,select/poll和信號通常是一種丑陋的方案,因此我更傾向于使用AIO或者完成端口,但是實(shí)際上它并不會好太多。也許除了select(),它們都還不錯。所以不要花太多精力去探索前置系統(tǒng)最外層內(nèi)部到底發(fā)生了什么。
對于最簡單的多線程事件驅(qū)動服務(wù)器的概念模型,?其內(nèi)部有一個請求緩存隊(duì)列,客戶端請求被一個或者多個監(jiān)聽線程獲取后放到隊(duì)列里,然后一個或者多個工作線程從隊(duì)列里面取出請求并處理。從概念上來說,這是一個很好的模型,有很多用這種方式來實(shí)現(xiàn)他們的代碼。這會產(chǎn)生什么問題嗎?引起環(huán)境切換的第二個原因是把對請求的處理從一個線程轉(zhuǎn)移到另一個線程。有些人甚至把對請求的回應(yīng)又切換回最初的線程去做,這真是雪上加霜,因?yàn)槊恳粋€請求至少引起了2次環(huán)境切換。把一個請求從監(jiān)聽線程轉(zhuǎn)換到成工作線程,又轉(zhuǎn)換回監(jiān)聽線程的過程中,使用一種“平滑”的方法來避免環(huán)境切換是非常重要的。此時,是否把連接請求分配到多個線程,或者讓所有線程依次作為監(jiān)聽線程來服務(wù)每個連接請求,反而不重要了。
即使在將來,也不可能有辦法知道在服務(wù)器中同一時刻會有多少激活線程.畢竟,每時每刻都可能有請求從任意連接發(fā)送過來,一些進(jìn)行特殊任務(wù)的“后臺”線程也會在任意時刻被喚醒。那么如果你不知道當(dāng)前有多少線程是激活的,又怎么能夠限制激活線程的數(shù)量呢?根據(jù)我的經(jīng)驗(yàn),最簡單同時也是最有效的方法之一是:用一個老式的帶計數(shù)的信號量,每一個線程執(zhí)行的時候就先持有信號量。如果信號量已經(jīng)到了最大值,那些處于監(jiān)聽模式的線程被喚醒的時候可能會有一次額外的環(huán)境切換,(監(jiān)聽線程被喚醒是因?yàn)橛羞B接請求到來,?此時監(jiān)聽線程持有信號量時發(fā)現(xiàn)信號量已滿,所以即刻休眠),?接著它就會被阻塞在這個信號量上,一旦所有監(jiān)聽模式的線程都這樣阻塞住了,那么它們就不會再競爭資源了,直到其中一個線程釋放信號量,這樣環(huán)境切換對系統(tǒng)的影響就可以忽略不計。更主要的是,這種方法使大部分時間處于休眠狀態(tài)的線程避免在激活線程數(shù)中占用一個位置,這種方式比其它的替代方案更優(yōu)雅。
一旦處理請求的過程被分成兩個階段(監(jiān)聽和工作),那么更進(jìn)一步,這些處理過程在將來被分成更多的階段(更多的線程)就是很自然的事了。最簡單的情況是一個完整的請求先完成第一步,然后是第二步(比如回應(yīng))。然而實(shí)際會更復(fù)雜:一個階段可能產(chǎn)生出兩個不同執(zhí)行路徑,也可能只是簡單的生成一個應(yīng)答(例如返回一個緩存的值)。由此每個階段都需要知道下一步該如何做,根據(jù)階段分發(fā)函數(shù)的返回值有三種可能的做法:
- 請求需要被傳遞到另外一個階段(返回一個描述符或者指針)
- 請求已經(jīng)完成(返回ok)
- 請求被阻塞(返回"請求阻塞")。這和前面的情況一樣,阻塞到直到別的線程釋放資源
應(yīng)該注意到在這種模式下,對階段的排隊(duì)是在一個線程內(nèi)完成的,而不是經(jīng)由兩個線程中完成。這樣避免不斷把請求放在下一階段的隊(duì)列里,緊接著又從該隊(duì)列取出這個請求來執(zhí)行。這種經(jīng)由很多活動隊(duì)列和鎖的階段很沒必要。
這種把一個復(fù)雜的任務(wù)分解成多個較小的互相協(xié)作的部分的方式,看起來很熟悉,這是因?yàn)檫@種做法確實(shí)很老了。我的方法,源于CAR在1978年發(fā)明的"通信序列化進(jìn)程"(Communicating Sequential Processes?CSP),它的基礎(chǔ)可以上溯到1963時的Per Brinch Hansen and Matthew Conway--在我出生之前!然而,當(dāng)Hoare創(chuàng)造出CSP這個術(shù)語的時候,“進(jìn)程”是從抽象的數(shù)學(xué)角度而言的,而且,這個CSP術(shù)語中的進(jìn)程和操作系統(tǒng)中同名的那個進(jìn)程并沒有關(guān)系。依我看來,這種在操作系統(tǒng)提供的單個線程之內(nèi),實(shí)現(xiàn)類似多線程一樣協(xié)同并發(fā)工作的CSP的方法,在可擴(kuò)展性方面讓很多人頭疼。
一個實(shí)際的例子是,Matt Welsh的SEDA,這個例子表明分段執(zhí)行的(stage-execution)思想朝著一個比較合理的方向發(fā)展。SEDA是一個很好的“server Aarchitecture done right”的例子,值得把它的特性評論一下:
1.SEDA的批處理傾向于強(qiáng)調(diào)一個階段處理多個請求,而我的方式傾向于強(qiáng)調(diào)一個請求分成多個階段處理。
2.在我看來SEDA的一個重大缺陷是給每個階段申請一個獨(dú)立的在加載響應(yīng)階段中線程“后臺”重分配的線程池。結(jié)果,原因1和原因2引起的環(huán)境切換仍然很多。
3.在純技術(shù)的研究項(xiàng)目中,在Java中使用SEDA是有用的,然而在實(shí)際應(yīng)用場合,我覺得這種方法很少被選擇。
?
內(nèi)存分配(Memory Allocator)
申請和釋放內(nèi)存是應(yīng)用程序中最常見的操作,?因此發(fā)明了許多聰明的技巧使得內(nèi)存的申請效率更高。然而再聰明的方法也不能彌補(bǔ)這種事實(shí):在很多場合中,一般的內(nèi)存分配方法非常沒有效率。所以為了減少向系統(tǒng)申請內(nèi)存,我有三個建議。
建議一是使用預(yù)分配。我們都知道由于使用靜態(tài)分配而對程序的功能加上人為限制是一種糟糕的設(shè)計。但是還是有許多其它很不錯的預(yù)分配方案。通常認(rèn)為,通過系統(tǒng)一次性分配內(nèi)存要比分開幾次分配要好,即使這樣做在程序中浪費(fèi)了某些內(nèi)存。如果能夠確定在程序中會有幾項(xiàng)內(nèi)存使用,在程序啟動時預(yù)分配就是一個合理的選擇。即使不能確定,在開始時為請求句柄預(yù)分配可能需要的所有內(nèi)存也比在每次需要一點(diǎn)的時候才分配要好。通過系統(tǒng)一次性連續(xù)分配多項(xiàng)內(nèi)存還能極大減少錯誤處理代碼。在內(nèi)存比較緊張時,預(yù)分配可能不是一個好的選擇,但是除非面對最極端的系統(tǒng)環(huán)境,否則預(yù)分配都是一個穩(wěn)賺不賠的選擇。
建議二是使用一個內(nèi)存釋放分配的lookaside list(監(jiān)視列表或者后備列表)。基本的概念是把最近釋放的對象放到鏈表里而不是真的釋放它,當(dāng)不久再次需要該對象時,直接從鏈表上取下來用,不用通過系統(tǒng)來分配。使用lookaside list的一個額外好處是可以避免復(fù)雜對象的初始化和清理.
通常,讓lookaside list不受限制的增長,即使在程序空閑時也不釋放占用的對象是個糟糕的想法。在避免引入復(fù)雜的鎖或競爭情況下,不定期的“清掃"非活躍對象是很有必要的。一個比較妥當(dāng)?shù)霓k法是,讓lookaside list由兩個可以獨(dú)立鎖定的鏈表組成:一個"新鏈"和一個"舊鏈".使用時優(yōu)先從"新"鏈分配,然后最后才依靠"舊"鏈。對象總是被釋放的"新"鏈上。清除線程則按如下規(guī)則運(yùn)行:
- 鎖住兩個鏈
- 保存舊鏈的頭結(jié)點(diǎn)
- 把前一個新鏈掛到舊鏈的前頭
- 解鎖
- 在空閑時通過第二步保存的頭結(jié)點(diǎn)開始釋放舊鏈的所有對象。
使用了這種方式的系統(tǒng)中,對象只有在真的沒用時才會釋放,釋放至少延時一個清除間隔期(指清除線程的運(yùn)行間隔),但同常不會超過兩個間隔期。清除線程不會和普通線程發(fā)生鎖競爭。理論上來說,同樣的方法也可以應(yīng)用到請求的多個階段,但目前我還沒有發(fā)現(xiàn)有這么用的。
使用lookaside lists有一個問題是,保持分配對象需要一個鏈表指針(鏈表結(jié)點(diǎn)),這可能會增加內(nèi)存的使用。但是即使有這種情況,使用它帶來的好處也能夠遠(yuǎn)遠(yuǎn)彌補(bǔ)這些額外內(nèi)存的花銷。
第三條建議與我們還沒有討論的鎖有關(guān)系。先拋開它不說。即使使用lookaside list,內(nèi)存分配時的鎖競爭也常常是最大的開銷。解決方法是使用線程私有的lookasid list,?這樣就可以避免多個線程之間的競爭。更進(jìn)一步,每個處理器一個鏈會更好,但這樣只有在非搶先式線程環(huán)境下才有用。基于極端考慮,私有l(wèi)ookaside list甚至可以和一個共用的鏈工作結(jié)合起來使用。
鎖競爭(Lock?Contention)
高效率的鎖是非常難規(guī)劃的,?以至于我把它稱作卡律布狄斯和斯庫拉(參見附錄)。一方面,?鎖的簡單化(粗粒度鎖)會導(dǎo)致并行處理的串行化,因而降低了并發(fā)的效率和系統(tǒng)可伸縮性;?另一方面,?鎖的復(fù)雜化(細(xì)粒度鎖)在空間占用上和操作時的時間消耗上都可能產(chǎn)生對性能的侵蝕。偏向于粗粒度鎖會有死鎖發(fā)生,而偏向于細(xì)粒度鎖則會產(chǎn)生競爭。在這兩者之間,有一個狹小的路徑通向正確性和高效率,但是路在哪里?
由于鎖傾向于對程序邏輯產(chǎn)生束縛,所以如果要在不影響程序正常工作的基礎(chǔ)上規(guī)劃出鎖方案基本是不可能的。這也就是人們?yōu)槭裁丛骱捩i,并且為自己設(shè)計的不可擴(kuò)展的單線程方案找借口了。
幾乎我們每個系統(tǒng)中鎖的設(shè)計都始于一個"鎖住一切的超級大鎖",并寄希望于它不會影響性能,當(dāng)希望落空時(幾乎是必然),?大鎖被分成多個小鎖,然后我們繼續(xù)禱告(性能不會受影響),接著,是重復(fù)上面的整個過程(許多小鎖被分成更小的鎖),?直到性能達(dá)到可接受的程度。通常,上面過程的每次重復(fù)都回增加大于20%-50%的復(fù)雜性和鎖負(fù)荷,并減少5%-10%的鎖競爭。最終結(jié)果是取得了適中的效率,但是實(shí)際效率的降低是不可避免的。設(shè)計者開始抓狂:"我已經(jīng)按照書上的指導(dǎo)設(shè)計了細(xì)粒度鎖,為什么系統(tǒng)性能還是很糟糕?"
在我的經(jīng)驗(yàn)里,上面的方法從基礎(chǔ)上來說就不正確。設(shè)想把解決方案當(dāng)成一座山,優(yōu)秀的方案表示山頂,糟糕的方案表示山谷。上面始于"超級鎖"的解決方案就好像被形形色色的山谷,凹溝,小山頭和死胡同擋在了山峰之外的登山者一樣,是一個典型的糟糕爬山法;從這樣一個地方開始登頂,還不如下山更容易一些。那么登頂正確的方法是什么?
首要的事情是為你程序中的鎖形成一張圖表,有兩個軸:
- 圖表的縱軸表示代碼。如果你正在應(yīng)用剔出了分支的階段架構(gòu)(指前面說的為請求劃分階段),你可能已經(jīng)有這樣一張劃分圖了,就像很多人見過的OSI七層網(wǎng)絡(luò)協(xié)議架構(gòu)圖一樣。
- 圖表的水平軸表示數(shù)據(jù)集。在請求的每個階段都應(yīng)該有屬于該階段需要的數(shù)據(jù)集。
現(xiàn)在,你有了一張網(wǎng)格圖,圖上每個單元格表示一個特定階段需要的特定數(shù)據(jù)集。下面是應(yīng)該遵守的最重要的規(guī)則:兩個請求不應(yīng)該產(chǎn)生競爭,除非它們在同一個階段需要同樣的數(shù)據(jù)集。如果你嚴(yán)格遵守這個規(guī)則,那么你已經(jīng)成功了一半。
一旦你定義出了上面那個網(wǎng)格圖,在你的系統(tǒng)中的每種類型的鎖就都可以被標(biāo)識出來了。你的下一個目標(biāo)是確保這些標(biāo)識出來的鎖盡可能在兩個軸之間均勻的分布,?這部分工作是和具體應(yīng)用相關(guān)的。你得像個鉆石切割工一樣,根據(jù)你對程序的了解,找出請求階段和數(shù)據(jù)集之間的自然“紋理線”。有時候它們很容易發(fā)現(xiàn),有時候又很難找出來,此時需要不斷回顧來發(fā)現(xiàn)它。在程序設(shè)計時,把代碼分隔成不同階段是很復(fù)雜的事情,我也沒有好的建議,但是對于數(shù)據(jù)集的定義,有一些建議給你:
- 如果你能對請求按順序編號,或者能對請求進(jìn)行哈希,或者能把請求和事物ID關(guān)聯(lián)起來,那么根據(jù)這些編號或者ID就能對數(shù)據(jù)更好的進(jìn)行分隔。
- 有時,基于數(shù)據(jù)集的資源最大化利用,把請求動態(tài)的分配給數(shù)據(jù),相對于依據(jù)請求的固有屬性來分配會更有優(yōu)勢。就好像現(xiàn)代CPU的多個整數(shù)運(yùn)算單元知道把請求分離一樣。
- 確定每個階段指定的數(shù)據(jù)集是不一樣的是非常有用的,以便保證一個階段爭奪的數(shù)據(jù)在另外階段不會爭奪。
如果你在縱向和橫向上把“鎖空間(這里實(shí)際指鎖的分布)"?分隔了,并且確保了鎖均勻分布在網(wǎng)格上,那么恭喜你獲得了一個好方案。現(xiàn)在你處在了一個好的登山點(diǎn),打個比喻,你面有了一條通向頂峰的緩坡,但你還沒有到山頂。現(xiàn)在是時候?qū)︽i競爭進(jìn)行統(tǒng)計,看看該如何改進(jìn)了。以不同的方式分隔階段和數(shù)據(jù)集,然后統(tǒng)計鎖競爭,直到獲得一個滿意的分隔。當(dāng)你做到這個程度的時候,那么無限風(fēng)景將呈現(xiàn)在你腳下。
其他方面
我已經(jīng)闡述完了影響性能的四個主要方面。然而還有一些比較重要的方面需要說一說,大所屬都可歸結(jié)于你的平臺或系統(tǒng)環(huán)境:
- 你的存儲子系統(tǒng)在大數(shù)據(jù)讀寫和小數(shù)據(jù)讀寫,隨即讀寫和順序讀寫方面是如何進(jìn)行?在預(yù)讀和延遲寫入方面做得怎樣?
- 你使用的網(wǎng)絡(luò)協(xié)議效率如何?是否可以通過修改參數(shù)改善性能?是否有類似于TCP_CORK, MSG_PUSH,Nagle-toggling算法的手段來避免小消息產(chǎn)生?
- 你的系統(tǒng)是否支持Scatter-Gather I/O(例如readv/writev)??使用這些能夠改善性能,也能避免使用緩沖鏈(見第一節(jié)數(shù)據(jù)拷貝的相關(guān)敘述)帶來的麻煩。(說明:在dma傳輸數(shù)據(jù)的過程中,要求源物理地址和目標(biāo)物理地址必須是連續(xù)的。但在有的計算機(jī)體系中,如IA,連續(xù)的存儲器地址在物理上不一定是連續(xù)的,則dma傳輸要分成多次完成。如果傳輸完一塊物理連續(xù)的數(shù)據(jù)后發(fā)起一次中斷,同時主機(jī)進(jìn)行下一塊物理連續(xù)的傳輸,則這種方式即為block dma方式。scatter/gather方式則不同,它是用一個鏈表描述物理不連續(xù)的存儲器,然后把鏈表首地址告訴dma master。dma master傳輸完一塊物理連續(xù)的數(shù)據(jù)后,就不用再發(fā)中斷了,而是根據(jù)鏈表傳輸下一塊物理連續(xù)的數(shù)據(jù),最后發(fā)起一次中斷。很顯然?scatter/gather方式比block dma方式效率高)
- 你的系統(tǒng)的頁大小是多少?高速緩存大小是多少?向這些大小邊界進(jìn)行對起是否有用?系統(tǒng)調(diào)用和上下文切換花的代價是多少?
- 你是否知道鎖原語的饑餓現(xiàn)象?你的事件機(jī)制有沒有"驚群"問題?你的喚醒/睡眠機(jī)制是否有這樣糟糕的行為:?當(dāng)X喚醒了Y,?環(huán)境立刻切換到了Y,但是X還有沒完成的工作?
我在這里考慮的了很多方面,相信你也考慮過。在特定情況下,應(yīng)用這里提到的某些方面可能沒有價值,但能考慮這些因素的影響還是有用的。如果在系統(tǒng)手冊中,你沒有找到這些方面的說明,那么就去努力找出答案。寫一個測試程序來找出答案;不管怎樣,寫這樣的測試代碼都是很好的技巧鍛煉。如果你寫的代碼在多個平臺上都運(yùn)行過,那么把這些相關(guān)的代碼抽象為一個平臺相關(guān)的庫,將來在某個支持這里提到的某些功能的平臺上,你就贏得了先機(jī)。
對你的代碼,“知其所以然”,?弄明白其中高級的操作,?以及在不同條件下的花銷.這不同于傳統(tǒng)的性能分析,?不是關(guān)于具體的實(shí)現(xiàn),而是關(guān)乎設(shè)計.?低級別的優(yōu)化永遠(yuǎn)是蹩腳設(shè)計的最后救命稻草.
————五一期間全線課程特惠————
Linux面授班,優(yōu)惠四重享,4000元大禮包等你拿
Linux網(wǎng)絡(luò)班,雙重優(yōu)惠出擊,最高2000元降價+666元學(xué)習(xí)資料贈送
Python全棧+Python運(yùn)維,最高直降2000元+八重大禮
課程詳情,請咨詢學(xué)習(xí)顧問: