国产一级a片免费看高清,亚洲熟女中文字幕在线视频,黄三级高清在线播放,免费黄色视频在线看

打開APP
userphoto
未登錄

開通VIP,暢享免費(fèi)電子書等14項(xiàng)超值服

開通VIP
高性能服務(wù)器架構(gòu)

 引言

    本文檔的目的是為了同大家分享多年來我在開發(fā)一種特定類型的應(yīng)用時(shí)形成的一些觀點(diǎn),而“服務(wù)器”只是對這類應(yīng)用程序的一個(gè)不是那么恰如其分的稱謂。更準(zhǔn)確的說,我將描述的是一大類的程序,這類程序的設(shè)計(jì)使得它們能夠在每秒鐘內(nèi)處理數(shù)量十分巨大的離散消息或請求。網(wǎng)絡(luò)服務(wù)器是最為常見的同此定義吻合的軟件,但是,并非所有同此定義吻合的程序絕對可以稱作是服務(wù)器。然而,“高性能請求處理程序”這種稱謂又很難讓人接受,所以,為了行文簡單起見,我就用“服務(wù)器”這個(gè)詞了事了。

    盡管在單個(gè)程序中進(jìn)行多任務(wù)處理現(xiàn)在早已司空見慣了,但我將不會(huì)對“適度并行”的應(yīng)用程序進(jìn)行討論。就在現(xiàn)在,你閱讀本文檔所用的瀏覽器可能正在以并行的方式做著一些事情,但是如此低水平的并行真是不會(huì)帶來任何值得關(guān)注的挑戰(zhàn)。真正值得關(guān)注的挑戰(zhàn)出現(xiàn)在處理請求的架構(gòu)本身是總體性能的限制性因素的時(shí)候,此時(shí)對架構(gòu)進(jìn)行改善就能夠真正地提高性能。運(yùn)行在主頻為幾G赫茲的CPU和上G內(nèi)存的環(huán)境中,通過DSL線路同時(shí)進(jìn)行著6個(gè)下載任務(wù)的瀏覽器,往往就不屬于這種情況。這里的重點(diǎn)并不在于象是用吸管喝著飲料似的應(yīng)用程序,而是在于象是通過消防拴來喝水的應(yīng)用程序,這類程序處于馬上就要突破硬件能力的邊緣地帶,你對這類程序的設(shè)計(jì)起著至關(guān)重要的作用。

    毫無疑問有些人會(huì)反對我的意見和建議,或者認(rèn)為他們有更好的辦法。這非常好。這里我可不是想要發(fā)出什么上帝之聲;這些只是我發(fā)現(xiàn)正合我意的方法,但合意的標(biāo)準(zhǔn)并不僅是它們在性能方面的表現(xiàn)不錯(cuò),而且還包括后期對這些代碼進(jìn)行調(diào)試和擴(kuò)展的難度也不高這個(gè)標(biāo)準(zhǔn)。你的衡量標(biāo)準(zhǔn)可能有所不同。如果你發(fā)現(xiàn)有別的方法更適合你那就太棒了,但是要警告的是,我在本文中建議的作為替代方案的幾乎所有方法我都試過了,而且其結(jié)果都很令人氣惱和不可接受。你所鐘愛的觀點(diǎn)要是放到本文中作為其中的故事之一可能也會(huì)非常的合適,如果你慫恿我把這些故事寫出來,無辜的讀者可能會(huì)被我煩死的。你可不想傷害讀者,對吧?

    本文剩下的部分將圍繞著我稱之為“性能低下的四騎士”的四個(gè)方面的內(nèi)容來進(jìn)行:

  1. 數(shù)據(jù)拷貝
  2. 上下文切換
  3. 內(nèi)存分配
  4. 鎖的爭用

    本文最后還包含了一個(gè)包羅萬象的部分,但是這四個(gè)是最大的性能殺手。如果你能在不拷貝數(shù)據(jù)、無需上下文切換、不用進(jìn)行內(nèi)存分配而且不會(huì)引起對鎖的爭用的情況下處理絕大多數(shù)請求,那么你的服務(wù)器性能一定會(huì)非常好,即使有些小地方做得不對也沒有太大的關(guān)系。


數(shù)據(jù)拷貝

    因?yàn)橐粋€(gè)非常簡單的原因,這一小節(jié)本來可以寫得非常簡短:絕大多數(shù)人都已經(jīng)有過這方面的教訓(xùn)了。每個(gè)人都知道,數(shù)據(jù)拷貝很不好;這很顯然,對吧?嗯,真地很對,正是因?yàn)槟阍谀愕挠?jì)算領(lǐng)域生涯的早期就有過這方面的教訓(xùn)了所以很顯然,并且之所以你有這方面教訓(xùn)是因?yàn)樵缭趲资昵熬陀腥碎_始提出數(shù)據(jù)拷貝這個(gè)詞了。我知道我的情況就是這樣的,但我有點(diǎn)跑題了?,F(xiàn)如今,在每個(gè)學(xué)校的課程和各種非正規(guī)的指南中都會(huì)對數(shù)據(jù)拷貝進(jìn)行討論。即使那些做銷售的都已經(jīng)弄明白了,“零拷貝”是個(gè)不錯(cuò)的時(shí)髦詞。

    盡管后知后覺顯然認(rèn)為數(shù)據(jù)拷貝很不好,但是,貌似人們還是沒有弄明白其中的一些細(xì)微之處。其中最重要的一點(diǎn)就是,數(shù)據(jù)拷貝往往發(fā)生地很隱蔽,形式上也有所偽裝。你真的了解你所調(diào)用的驅(qū)動(dòng)程序或者代碼庫里面到底有沒有進(jìn)行數(shù)據(jù)拷貝嗎?可能情況比你所想的要復(fù)雜一些。請你猜猜看PC中“程序控制的I/O”指的是什么。哈希函數(shù)就是一個(gè)不是隱蔽的而是經(jīng)過偽裝的數(shù)據(jù)拷貝的例子,它具有數(shù)據(jù)拷貝的所有內(nèi)存訪問開銷,而且還涉及了大量的計(jì)算過程。 只要指出來哈希實(shí)際上是個(gè)“數(shù)據(jù)拷貝再加其它操作“的過程,那么貌似有一點(diǎn)很顯然,就是要避免使用哈希函數(shù)了,但是,我知道至少有一群高人會(huì)把這個(gè)問題解決掉。如果你真想消除數(shù)據(jù)拷貝,不管是因?yàn)樗鼈冋娴貢?huì)損害性能,還是因?yàn)槟憔褪窍氚选傲憧截惒僮鳌睂懭肽阍诤诳痛髸?huì)上的幻燈片里,你將需要對很多并不沒有大張旗鼓告訴你但其實(shí)真的包含了數(shù)據(jù)拷貝的很多東西一直追查到底。

經(jīng)實(shí)踐驗(yàn)證過的避免數(shù)據(jù)拷貝的方法就是使用間接法,傳遞緩沖區(qū)描述符(或者是一個(gè)緩沖區(qū)描述符組成的鏈)而不是僅僅傳遞緩沖區(qū)指針。每個(gè)描述符一般都由以的幾個(gè)部分組成:

  • 一個(gè)指針以及整個(gè)緩沖區(qū)的長度。
  • 一個(gè)指針和長度,或者是緩沖區(qū)中真正填充了數(shù)據(jù)部分的。
  • 指向列表中其它緩沖區(qū)描述符的前向和后向指針。
  • 一個(gè)引用計(jì)數(shù)

    現(xiàn)在,不用通過拷貝一段數(shù)據(jù)來確保這些數(shù)據(jù)能夠呆在內(nèi)存中了,代碼可以很簡單地對適當(dāng)?shù)木彌_區(qū)描述符中的引用計(jì)數(shù)加一。在某些情況下這種做法會(huì)相當(dāng)?shù)爻晒?,包括在典型的網(wǎng)絡(luò)協(xié)議棧的運(yùn)作模式中也沒問題,但是,這種做法也有可能會(huì)成為一件讓你大為頭疼的事情。一般來說,要在緩沖區(qū)描述符鏈的開頭或者結(jié)尾部分添加新的緩沖區(qū)很容易,同樣為整個(gè)緩沖區(qū)增加引用以及立即撤銷為整個(gè)鏈分配的內(nèi)存也很容易。在中間部分添加新緩沖區(qū)、一點(diǎn)一點(diǎn)的撤銷已分配的內(nèi)存或者引用部分緩沖區(qū)這三種操作每一個(gè)都會(huì)讓你的日子越來越難過。要想對緩沖區(qū)進(jìn)行分割或者合并只會(huì)把你逼瘋。

    然而,實(shí)際上我并不建議在所有情況下都采用這種方法。為什么不建議呢?因?yàn)椴捎眠@種方法后,每次想查看報(bào)頭部分的時(shí)候你都不得不對描述符鏈進(jìn)行遍歷,這么做真是痛苦了。這里真的還有比數(shù)據(jù)拷貝更加糟糕的事情。我發(fā)現(xiàn),要做的最好的事情就是找出程序里的大對象,比如數(shù)據(jù)塊,確保象前文所述那樣,為這些數(shù)據(jù)塊獨(dú)立分配內(nèi)存,這樣就不需要對它們進(jìn)行拷貝了,至于剩下其它的東西就不要操那么多心了。

這就是我對數(shù)據(jù)拷貝要說一下我的最后一個(gè)觀點(diǎn):在避免數(shù)據(jù)拷貝時(shí)不要做得太過火。我看到過太多代碼,為了避免數(shù)據(jù)拷貝而它們把某些事情搞得更糟了,比如,它們會(huì)迫使系統(tǒng)進(jìn)行上下文切換或者會(huì)打斷數(shù)據(jù)規(guī)模較大的I/O請求。數(shù)據(jù)拷貝代價(jià)比較高,當(dāng)你正在尋找需要避免冗余操作的地方時(shí),其中首要的就是應(yīng)該看看有沒有出現(xiàn)數(shù)據(jù)拷貝的地方。但是,有一點(diǎn)會(huì)減少這么做對你的回報(bào)。仔細(xì)排查代碼,然后就是為了排除掉最后的幾個(gè)數(shù)據(jù)拷貝而把代碼搞到復(fù)雜了兩倍多,這通常是對時(shí)間的一種浪費(fèi),這些時(shí)間本可以更好地花在別的地方。


上下文切換

    鑒于每個(gè)人都認(rèn)為數(shù)據(jù)拷貝顯然不好,我經(jīng)常驚嘆于竟然有那么多人會(huì)完全忽略上下文切換對性能的影響。按照我的經(jīng)驗(yàn)來看,在高負(fù)載的情況下,同數(shù)據(jù)拷貝相比,實(shí)際上上下文切換實(shí)際才是更多的導(dǎo)致系統(tǒng)“完全失靈”的元兇;系統(tǒng)開始在來回從一個(gè)線程到另一個(gè)線程的切換中所花的時(shí)間比線程真正做有用的工作所花的時(shí)間還要多。令人驚奇的是,從某個(gè)角度講,引起系統(tǒng)過度進(jìn)行上下文切換的元兇十分顯而易見。上下文切換的頭號原因就是活躍線程數(shù)超過了處理器的總數(shù)。隨著活躍線程數(shù)同處理器總數(shù)比值的增大,上下文切換的數(shù)量也會(huì)增大。如果幸運(yùn),這種增加會(huì)是線性的,但通常都是成指數(shù)級增長。這個(gè)非常簡單事實(shí)可以解釋出每個(gè)連接都用一個(gè)線程來處理的多線程設(shè)計(jì)為什么伸縮性會(huì)非常之差??缮炜s系統(tǒng)唯一比較現(xiàn)實(shí)的方案就是限制活躍線程的總數(shù),讓該數(shù)(在一般情況下)小于或等于處理器的總數(shù)。這種方案有一種比較多見的經(jīng)過修改的版本就是只使用一個(gè)線程;盡管這么做的確能夠完全避免上下文胡亂切換,而且還不用再使用鎖了,但它也無法利用多CPU來提高總吞吐量了,所以除非所設(shè)計(jì)的程序是非CPU密集型的(通常是網(wǎng)絡(luò)I/O密集型的),一般大家都不采用這種方案。

    一個(gè)“適度使用線程”的程序要做的第一件事就是找出如何讓一個(gè)線程同時(shí)處理多個(gè)連接的辦法。這通常意味著要在前臺使用select/poll API、異步I/O、信號或者完成端口,而后端使用一個(gè)事件驅(qū)動(dòng)的結(jié)構(gòu)。關(guān)于到底哪種前臺API才是最好的,已經(jīng)發(fā)生過許多類似于“宗教之爭”的爭論,而且這種爭論還會(huì)持續(xù)下去。Dan Kegel所寫的C10K論文是這個(gè)領(lǐng)域中最好的參考資料。我個(gè)人認(rèn)為,各種select/poll API和信號都是些丑陋的伎倆,因此我比較偏愛AIO或者完成端口,但這實(shí)際上并沒有那么重要。所以這些方案,也許要將除select()外,用起來都相當(dāng)不錯(cuò),真地都不會(huì)做太多的事情來解決發(fā)生在你的程序前端最外層之外的任何問題。

    事件驅(qū)動(dòng)的多線程服務(wù)器最簡單的概念模型以隊(duì)列為中心;一個(gè)或多個(gè)“監(jiān)聽者”線程讀取請求并將其放入隊(duì)列之中,然后由一個(gè)或多個(gè)“工作者”線程將請求從隊(duì)列中取出并對它們進(jìn)行處理。從概念上講,這是個(gè)好模型,但太多人真的就按照這種方式來編碼了。為什么這么做不對?因?yàn)閷?dǎo)致上下文切換的第二號原因就是將工作從一個(gè)線程傳遞給另外一個(gè)線程。有些人甚至?xí)屧鹊木€程來發(fā)送對請求的響應(yīng),這樣勢必造成處理每個(gè)請求時(shí)不是發(fā)生一次而是兩次上下文切換,這可真是錯(cuò)上加錯(cuò)啊。這里很重要的一點(diǎn)就是,要采用一種“對稱”的方法,一個(gè)給定的線程可以在根本不引起上下文切換的情況下,可以在剛開始時(shí)是監(jiān)聽者的身份,隨后其身份可以變換為工作者,然后再次成為監(jiān)聽者。這種方法到底需要在線程間分配所有的連接還是需要讓所有的線程按次序排隊(duì)成為所有連接的監(jiān)聽者,似乎并不太重要了。

    通常即使對于將來的下一刻,也很難知道系統(tǒng)中到底有多少活躍的線程。畢竟請求可能會(huì)在任何時(shí)刻從任意一個(gè)連接中發(fā)過來,還有專用于處理各種維護(hù)任務(wù)的“背景”線程也可能會(huì)挑選在那個(gè)時(shí)刻醒過來。如果你不知道到底有多少個(gè)線程是活躍的,那你怎么才能做到限制系統(tǒng)中應(yīng)該有多少個(gè)活躍線程呢?從我的經(jīng)驗(yàn)來看,最簡單同時(shí)也是最有效的方法之一就是:采用一個(gè)老式的計(jì)數(shù)信號量,當(dāng)每個(gè)線程在做“真正的工作”時(shí),它必須持有該信號量。如果活躍線程數(shù)已經(jīng)達(dá)到上限,那么處于偵聽模式的每個(gè)線程在醒來的時(shí)候,可能會(huì)導(dǎo)致一次額外的線程切換,隨后就會(huì)阻塞在該信號量之上,但是一旦所有偵聽模式的線程都以這種方式進(jìn)入阻塞狀態(tài),那么直到現(xiàn)有線程之一“退出活躍狀態(tài)”之前,它們就不會(huì)再對系統(tǒng)資源進(jìn)行爭用了,因此它們對系統(tǒng)性能的影響可以忽略不計(jì)。更重要的是,這種方法還處理了維護(hù)線程,這些線程在大多數(shù)的時(shí)間中都處于休眠模式,所以不會(huì)計(jì)入活動(dòng)線程計(jì)數(shù),這種處理方式比其它的方案要更加的優(yōu)雅。

    既然將請求的處理過程分為了兩個(gè)階段(監(jiān)聽者和工作者)并由多個(gè)線程來為這兩個(gè)階段服務(wù),那么將處理過程更進(jìn)一步分為多于兩個(gè)的階段就是很自然的事情了。按照最簡單的形式,請求的處理就變成了先在一個(gè)方向完成一個(gè)階段的處理過程,然后再在另外一個(gè)方向上(為了響應(yīng)請求)進(jìn)行另外一個(gè)階段的處理。然而,死去可能會(huì)變得更加復(fù)雜;有一個(gè)階段可能會(huì)代表著在涉及不同階段的兩個(gè)處理路徑上進(jìn)行“分叉”,或者該階段可能會(huì)產(chǎn)生一個(gè)響應(yīng)(比如,該響應(yīng)是個(gè)緩存中的值)而無需進(jìn)行下一個(gè)階段的處理了。因此,每個(gè)階段都需要能夠?yàn)檎埱笾付ā跋聜€(gè)階段應(yīng)該干什么了”。這里有三種可能,由每個(gè)階段的分發(fā)函數(shù)的返回值來表示:

  • 該請求需要接著傳遞到另外一個(gè)階段(在返回值里用一個(gè)ID或指針來表示這個(gè)階段)。
  • 該請求已經(jīng)處理完畢(用一個(gè)專門的“請求處理完畢”返回值來表示)。
  • 該請求被阻塞(用一個(gè)專門的“請求被阻塞”返回值來表示)。這等價(jià)于一種情況,只是該請求仍未釋放,隨后會(huì)在另外一個(gè)線程中接著對其進(jìn)行處理。

    請注意,在本模型中,請求的隊(duì)列操作是在階段 完成的,而不是在階段間完成的。這樣就能夠避免常見的愚蠢做法:不斷將請求放入后繼階段的隊(duì)列之中,然后立即進(jìn)行該后繼階段并將該請求從隊(duì)列中取出。我認(rèn)為,類似這樣的隊(duì)列活動(dòng)以及加解鎖的動(dòng)作絕對是無事生非。

把一個(gè)復(fù)雜的認(rèn)為分割成相互通信的多個(gè)較小部分的這種做法如果感覺很熟悉的話,那是因?yàn)檫@種做法已由來已久。我的方法的根源是1978年由C.A.R. Hoar闡明的概念通信順序進(jìn)程(Communicating Sequential Process,簡稱CSP),而CSP又是基于早在1963年,也就是在我出生之前,由Per Brinch Hansen和Matthew Conway提出的一些觀點(diǎn)。然而,在當(dāng)初Hoare創(chuàng)造CSP這個(gè)名詞時(shí),他所說的“進(jìn)程”是在抽象的數(shù)學(xué)意義上講的進(jìn)程,CSP進(jìn)程跟操作系統(tǒng)中那個(gè)具有相同名字的實(shí)體并無關(guān)聯(lián)。使用運(yùn)行在單個(gè)OS線程中的、跟線程看上去很象的協(xié)程(coroutine)是實(shí)現(xiàn)CSP的最常見方法,而且依我看,就是這種實(shí)現(xiàn)方法給用戶造成了這樣的棘手的局面:使用了并發(fā)編程卻仍舊不具有并發(fā)編程的可伸縮性。

    Matt Welsh的SEDA是當(dāng)代實(shí)現(xiàn)階段化任務(wù)執(zhí)行理念的一個(gè)朝著更為理性方向發(fā)展的實(shí)例。實(shí)際上,SEDA是個(gè)非常好的例子,它"具有非常恰當(dāng)?shù)姆?wù)器體系結(jié)構(gòu)”,所以它的一些具體特性非常值得拿來說一說(特別是同我在上文中總結(jié)的不大相同的特性)。

  1. SEDA的“批處理(batching)”傾向于強(qiáng)調(diào)在一個(gè)階段中同時(shí)處理多個(gè)請求,而我的方法更傾向于強(qiáng)調(diào)同時(shí)在多個(gè)階段中處理同一個(gè)的請求。
  2. 在我看來,SEDA的一個(gè)重大缺陷是它為每個(gè)階段分配了一個(gè)單獨(dú)的線程池,只是在“后臺”根據(jù)負(fù)載情況對線程進(jìn)行重新分配。這樣一來,引起上下文切換的頭號和第二號原因仍然會(huì)不斷出現(xiàn)。
  3. 從學(xué)術(shù)研究項(xiàng)目的角度講,用Java來實(shí)現(xiàn)SEDA也許可以說得過去。但從實(shí)際應(yīng)用角度來講,我認(rèn)為這種選擇可以說是很令人遺憾的。

內(nèi)存分配

    分配和釋放內(nèi)存是許多應(yīng)用程序中最常見的操作之一。因此,人們?yōu)榱俗屚ㄓ玫膬?nèi)存分配器更加的高效而開研究出了許多巧妙的花招。然而,正是由于這些內(nèi)存分配器的通用性,使得它們不可避免地會(huì)在許多場合下其效率遠(yuǎn)低于它們的其它替代方案,而且即使再巧妙也無法避免這種情況的發(fā)生。因此,關(guān)于如何徹底避免系統(tǒng)內(nèi)存分配器,我有三個(gè)建議。

    建議一是使用一個(gè)簡單的預(yù)分配方案。我們都知道,靜態(tài)內(nèi)存分配在對程序的功能會(huì)施加人為限制的情況下最好不要誰好用,但是,預(yù)分配還有其它我們能夠從中獲益匪淺的很多種形式。通常使用預(yù)分配的原因來自于只調(diào)用一次系統(tǒng)內(nèi)存分配器比調(diào)用多次好,即使在這個(gè)過程中會(huì)有部分內(nèi)存被“浪費(fèi)掉”了。所以,如果有可能可以斷定,同時(shí)使用的數(shù)據(jù)不會(huì)多于N項(xiàng),在程序啟動(dòng)之初就先進(jìn)行內(nèi)存預(yù)分配可能會(huì)是個(gè)正確的選擇。即使無法做出這樣的斷定,為請求處理器在開始時(shí)就會(huì)需要進(jìn)行分配的內(nèi)存進(jìn)行預(yù)分配可能要比隨著需要一點(diǎn)一點(diǎn)來分配內(nèi)存強(qiáng);而且通過一次調(diào)用系統(tǒng)內(nèi)存分配器就為許多個(gè)數(shù)據(jù)項(xiàng)而分配的內(nèi)存還可能是連續(xù)的,這往往會(huì)極大的降低錯(cuò)誤恢復(fù)代碼的復(fù)雜度。在內(nèi)存非常緊張的情況下,預(yù)分配就不是一個(gè)好的選擇了,但是除了一些最極端的情況,其它情況下預(yù)分配一般都會(huì)是個(gè)絕對上算的選擇。

    建議二是采用后備列表(lookaside list)來對分配和釋放的頻率比較高的對象進(jìn)行管理。其基本的想法是要將最近要釋放的對象放入該列表而不是真正的釋放它們,希望其后不久再需要它們的時(shí)候只需從后備列表中將它們重新取回來而不是從系統(tǒng)內(nèi)存中為它們再次分配內(nèi)存。使用后備列表還會(huì)帶來一個(gè)額外的好處,就是在實(shí)現(xiàn)從后備列表中傳進(jìn)/傳出復(fù)雜對象時(shí),我們可以跳過對這些復(fù)雜對象的初始化/終止化(initialization/finalization)操作。

即使程序在空閑狀態(tài)時(shí)也永不讓真正釋放所有對象,就會(huì)讓后備列表無限制地增長下去,通常情況下這種做法是不可取的。因此,一般都很有必要設(shè)立一個(gè)周期性的“清掃者”任務(wù)來釋放非活躍對象,但是如果因引入清掃者而增加了鎖的復(fù)雜度以及出現(xiàn)鎖爭用的幾率,那么這也同樣是不可取的。這里有一個(gè)比較好的折中的辦法,把后備列表分為兩個(gè)獨(dú)立鎖定的“舊”列表和“新”列表。使用時(shí)首選從新列表中分配對象,然后是從舊列表中分配,萬不得已時(shí)才從系統(tǒng)中分配對象;對象總是釋放到新列表中。清掃者線程要按照下來步驟進(jìn)行操作:

  1. 鎖定這兩個(gè)列表。
  2. 將舊列表的頭指針保存起來。
  3. 通過列表的頭指針賦值將(先前的)新列表轉(zhuǎn)變?yōu)榕f列表。
  4. 解鎖。
  5. 在空閑時(shí)將在第二步保存起來的舊列表中的所有對象都釋放掉。

    在這種系統(tǒng)中,對象只有在至少一個(gè)完整的清掃周期且最多絕對不會(huì)超過兩個(gè)清掃周期的時(shí)間內(nèi)沒有被使用到后,才會(huì)被真正的釋放掉。更重要的是,清掃者線程在做的大部分工作時(shí)都不會(huì)和普通線程發(fā)生鎖爭用。從理論上講,同樣的方法也可以推廣到多于兩個(gè)處理階段的系統(tǒng)中,但我還沒有看出來這種推廣有多大的實(shí)用價(jià)值。

    使用后備列表有一個(gè)讓人擔(dān)心的問題是列表指針可能會(huì)增加對象的大小。從我的經(jīng)驗(yàn)來看,我用后備列表來管理的絕大多數(shù)對象反正都已經(jīng)包含了列表指針,所以這個(gè)問題沒有什么太大的意義。但是,即使指針只是用于后備列表的,因?yàn)楹髠淞斜肀苊饬硕啻握{(diào)用系統(tǒng)內(nèi)存分配器(而且還避免了對象初始化操作的多次執(zhí)行),用由此而節(jié)省下來的系統(tǒng)開銷來彌補(bǔ)列表指針?biāo)加玫哪屈c(diǎn)額外的內(nèi)存還是綽綽有余的。

    建議三實(shí)際上同我們還尚未討論的鎖有關(guān)系,但無論如何我在這里要先說幾句。通常在分配內(nèi)存時(shí),最大的開銷化在了鎖爭用上,即使使用后備列表情況也是這樣的。有個(gè)解決辦法就是維護(hù)多個(gè)私有的后備列表,如此一來每個(gè)列表都絕不可能再發(fā)生鎖爭用的情況。例如,你可以為每個(gè)線程創(chuàng)建一個(gè)單獨(dú)的后備列表?;诰彺鏌岫龋╟ache-warmth)方面的考慮,為每個(gè)處理器創(chuàng)建一個(gè)列表可能會(huì)更好,但這只有在非搶占式線程環(huán)境下才可行。為了創(chuàng)建內(nèi)存分配開銷極低的系統(tǒng),如有必要,私有的后備列表甚至還可以同共享的后備列表結(jié)合起來使用。


鎖的爭用

    眾所周知,要設(shè)計(jì)出高效的鎖定機(jī)制是極其困難的, 我將造成這個(gè)困難的原因稱為斯庫拉和卡律布狄斯(譯者注:這兩個(gè)名字放到一起在英語中的意思一般是讓人進(jìn)退兩難、腹背受敵的意思),她倆是古希臘史詩《奧德賽》中的兩個(gè)女妖。斯庫拉代表非常簡化和/或粗粒度的鎖,她會(huì)把本可以或本應(yīng)該并行進(jìn)行的活動(dòng)轉(zhuǎn)化為必須按順序執(zhí)行的活動(dòng),因而會(huì)對性能和可伸縮性造成損失;卡律布狄斯代表超復(fù)雜或細(xì)粒度的鎖,但她需要鎖的地方太多再加上鎖操作占用的時(shí)間同樣也會(huì)造成性能損失。 靠近斯庫拉的陷阱代表著發(fā)生死鎖和活鎖(deadlock and livelock)的情況;靠近卡律布狄斯的陷阱代表著競態(tài)條件(race condition)。在這二者之間有一條狹窄的通道,它代表著即高效又正確的鎖。。。但是這樣的通道在哪里呢?因?yàn)殒i一般會(huì)和程序邏輯緊密的聯(lián)系在一起,所以在不深刻改變程序運(yùn)行基礎(chǔ)的情況下,想要設(shè)計(jì)出很好的鎖定方案往往都是不太可能的。這就是人們?yōu)槭裁丛骱捩i,并努力為他們采用不具伸縮性的單線程方案正名的原因。

    幾乎每一個(gè)鎖定方案開頭都會(huì)設(shè)計(jì)成“一個(gè)可以鎖住所有東西的大鎖”并且心存僥幸,希望這種設(shè)計(jì)性能不會(huì)糟到哪里去。這種僥幸心理往往不能得逞,當(dāng)希望破滅后,大鎖就會(huì)被分解成許多個(gè)相對較小的鎖并繼續(xù)心存僥幸,隨后在重復(fù)一遍這個(gè)過程,大概在性能基本說的過去時(shí)整個(gè)設(shè)計(jì)過程才會(huì)結(jié)束。 通常每個(gè)迭代過程都會(huì)將程序的復(fù)雜度和鎖操作的開銷提高20-50%,但只能減少5-10%的鎖爭用。幸運(yùn)的話,最終還是會(huì)在性能方面有適度的提高的,但性能沒有提升卻反而出現(xiàn)下降也并不罕見。設(shè)計(jì)者會(huì)因此而感到一頭霧水,心里想:“我是按照所有的教科書教我的辦法將鎖的粒度調(diào)整到更小的,可為什么性能卻更糟了呢?”

    依我看,情況變得更糟了的原因在于,上文中所說的那個(gè)方法完全是被誤導(dǎo)了。請把這個(gè)設(shè)計(jì)問題的“解空間”想象為一個(gè)山脈,山脈中的高地代表著優(yōu)秀的設(shè)計(jì)方案,而山脈中的低處代表著糟糕的方案。上文中的問題就在于,開始時(shí)的那個(gè)“大鎖”同山脈中的高峰間橫亙著各種各樣的山谷、山鞍、小山峰和絕路。這是個(gè)經(jīng)典的爬山問題;從這樣的一個(gè)起點(diǎn)開始,試圖通過一小步一小步的方式爬到更高的山峰還不想走下坡路基本上是件不可能實(shí)現(xiàn)的事情。設(shè)計(jì)者所需要的是應(yīng)該采用一種完全不同方式來爬向頂峰。

    你要做的第一件事就是在你的頭腦中要對你的程序的鎖操作有一個(gè)示意圖,該圖具有兩個(gè)軸:

  • 縱軸表示的是代碼。如果你采用的是無分支階段的階段化架構(gòu),那很可能你已經(jīng)有了一張類似大家在學(xué)習(xí)OSI模型中的網(wǎng)絡(luò)協(xié)議棧時(shí)所使用的那種圖。
  • 橫軸表示的是數(shù)據(jù)。在每個(gè)階段中,每個(gè)請求都應(yīng)該分配到一個(gè)單獨(dú)的數(shù)據(jù)集之中,該數(shù)據(jù)集包含著屬于請求本身的資源。

這樣你就會(huì)得到一個(gè)網(wǎng)格,網(wǎng)格中的每個(gè)單元格表示的是某特定處理階段中的某特定數(shù)據(jù)集。其中最重要的是這個(gè)規(guī)則:兩個(gè)請求間不應(yīng)該發(fā)生爭用,除非這兩個(gè)請求處于同一個(gè)數(shù)據(jù)集并且處于同一個(gè)處理階段。如果你能做到嚴(yán)格遵守這個(gè)規(guī)則,那么你就已經(jīng)成功一半了。

    上面的網(wǎng)格各項(xiàng)內(nèi)容都弄明確之后,你就能夠畫出你的程序中所有類型的鎖操作了,你的下一個(gè)目標(biāo)是確保最終的畫出來點(diǎn)在兩個(gè)軸的方向上的分布要越均勻越好。很不幸,這部分工作同具體應(yīng)用的相關(guān)性很大,你得象鉆石切割師那樣,根據(jù)你對程序要達(dá)到什么目的的了解,找出階段和數(shù)據(jù)集間最自然的“切割線”。有時(shí)開頭就能非常容易的找出來,有時(shí)就比較困難了,但貌似通過反復(fù)琢磨才能更容易的找到這些切割系。將代碼分割到若個(gè)個(gè)階段之中是程序設(shè)計(jì)中比較復(fù)雜的一件事,所以這塊我也沒有什么太多要說的,關(guān)于如何定義數(shù)據(jù)集我倒是有幾個(gè)建議:

  • 如果請求有與之相關(guān)聯(lián)的某種形式的批號、哈?;蛘呤聞?wù)ID,那除了將這個(gè)值除以數(shù)據(jù)集的總數(shù)之外,很少有其它更好的做法。
  • 有時(shí),最好是基于哪個(gè)數(shù)據(jù)集具有最大可用資源而不是請求內(nèi)存的屬性將請求動(dòng)態(tài)地分配給數(shù)據(jù)集。我們可以把數(shù)據(jù)集看作現(xiàn)代CPU中的多個(gè)整數(shù)單元;這些整數(shù)單元還知道點(diǎn)如何在系統(tǒng)中發(fā)出離散請求流。
  • 要確保每個(gè)階段的數(shù)據(jù)集分配方案互不相同,這樣一來,在一個(gè)階段中會(huì)發(fā)生爭用的請求就能夠保證不會(huì)在另一個(gè)階段中也發(fā)生爭用了。這個(gè)建議通常會(huì)對你很有幫助作用。
    如果你從縱橫兩個(gè)方向?qū)δ愕摹版i空間”進(jìn)行了分割,并且的確所有的鎖活動(dòng)都最終的單元格中呈均勻分布,你就可以相對自信的說,你設(shè)計(jì)的鎖定方案一定錯(cuò)不了。但這里還有一個(gè)步驟要走。你還記得我在本文中的前若個(gè)段中嘲笑過的那個(gè)“一小步一小步往前走”的方法嗎?它仍然沒有消失,但是(譯者注:原文此處用的不是“但是”而是“因?yàn)椤?,這貌似同上下文有所違和。)現(xiàn)在你站在了一個(gè)好的起點(diǎn)之上,這同前幾段中的那個(gè)方法不同。以比喻的方式來說,你可能已經(jīng)站在了整個(gè)山脈中最高峰的一個(gè)山坡之上了,但很可能你還不是在峰頂之上。接下來就到時(shí)候去做下面這些事情了:收集爭用統(tǒng)計(jì)數(shù)據(jù)從而找出你需要在哪些方面做出改善、以不同的方式來劃分階段和數(shù)據(jù)集并收集更多的統(tǒng)計(jì)數(shù)據(jù)直到你滿意為止。在你完成這些任務(wù)之后,你就一定能夠欣賞到山頂之上無限美好的景色了。


其他說明

    正如我所承諾的那樣,我的講解涵蓋了服務(wù)器設(shè)計(jì)中與性能相關(guān)的四個(gè)關(guān)鍵問題。不過,我們還是需要根據(jù)每個(gè)服務(wù)器各自的情況進(jìn)行分別對待。一般來說,下面的列表可以更好地幫助你了解你所使用的平臺(或環(huán)境):

  • 你是如何完成存儲子系統(tǒng)的部署?根據(jù)請求量?有序的還是隨機(jī)的?預(yù)讀和后寫工作進(jìn)展的如何?
  • 你所使用的網(wǎng)絡(luò)協(xié)議效率如何?你在傳遞中使用的參數(shù)后標(biāo)識可以改進(jìn)的更好嗎?像 TCP_CORK、MSG_PUSH 或者 Nagle-toggling trick 這些工具可以幫助你消除極小的信息嗎?
  • 你的系統(tǒng)支持分散或聚集 I/O (例如讀寫操作)嗎?使用這些方法可以改進(jìn)服務(wù)器的性能,并且可以免除你在使用緩沖區(qū)鏈時(shí)所遭受的巨大痛苦。
  • 你的頁面體積有多大?你的緩存線體積?是否值得將這些內(nèi)容排列好放在邊界?系統(tǒng)調(diào)用和環(huán)境切換的代碼有多大,是否關(guān)聯(lián)到其它事物?
  • 你的讀寫者是否因?yàn)轭l繁加鎖基本對象而無畏地消耗能源?這些對象又是什么呢?你的事件是否存在“雷鳴猛獸”的問題?你的睡眠和喚醒是否存在嵌套行為(盡管這很常見),如 X 喚醒 Y 后 Y 便立即發(fā)生無論此時(shí) X 是否完成所有任務(wù)?

    毫不夸張地講,沿著這個(gè)思路我還能想出更多的問題。我相信你也能。 在有些情況下,可能這些問題還不值得你真正花時(shí)間為它們做點(diǎn)什么,但通常它們至少還屬于值得你去思考的問題。大部分問題的答案你從系統(tǒng)文檔中是找不到的,如果你不知道這些答案,那么 去找吧!寫個(gè)測試程序或者是小型基準(zhǔn)測試程序來在實(shí)踐中找出答案吧;不管怎樣,編寫這些代碼本身就是一種很有用的技能。如果你寫的代碼要運(yùn)行在多個(gè)平臺之上,那這些問題中的大部分問題可能都需要你將功能抽象到各平臺字節(jié)的代碼庫中,這樣就能夠根據(jù)平臺支持的特性不同,實(shí)現(xiàn)在某個(gè)平臺上獲得單獨(dú)的性能提高。

有個(gè)“知道所有答案”的理論同樣適用于你自己的代碼。弄清你的代碼中比較重要的高層操作在哪里并在不同的情況下對它們執(zhí)行所花的時(shí)間進(jìn)行統(tǒng)計(jì)。這和傳統(tǒng)的性能分析并不完全相同;這是在衡量設(shè)計(jì)元素,而不是真正的實(shí)現(xiàn)。底層優(yōu)化一般是那些把設(shè)計(jì)搞砸了的人最后的救命稻草。

原文出自:http://www.oschina.net/translate/high-performance-server-architecture


本站僅提供存儲服務(wù),所有內(nèi)容均由用戶發(fā)布,如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊舉報(bào)。
打開APP,閱讀全文并永久保存 查看更多類似文章
猜你喜歡
類似文章
高性能IO背后原理-零拷貝(zero copy)技術(shù)概述
阿里P7二面:聊聊零拷貝的原理
超硬核,基于mmap和零拷貝實(shí)現(xiàn)高效的內(nèi)存共享
操作系統(tǒng)IO之零拷貝技術(shù)
阿里二面:什么是mmap?
通過零拷貝實(shí)現(xiàn)有效數(shù)據(jù)傳輸
更多類似文章 >>
生活服務(wù)
分享 收藏 導(dǎo)長圖 關(guān)注 下載文章
綁定賬號成功
后續(xù)可登錄賬號暢享VIP特權(quán)!
如果VIP功能使用有故障,
可點(diǎn)擊這里聯(lián)系客服!

聯(lián)系客服