高效能伺服器架構 第三篇

來源:互聯網
上載者:User

轉自:http://www.doserv.com/article/2012/0831/5299117.shtml

 

記憶體配置(Memory Allocator)

申請和釋放記憶體是應用程式中最常見的操作, 因此發明了許多聰明的技巧使得記憶體的申請效率更高. 然而再聰明的方法也不能彌補這種事實: 在很多場合中,一般的記憶體配置方法非常沒有效率。所以為了減少向系統申請記憶體,我有三個建議.

建議一是使用預分配. 我們都知道由於使用靜態分配而對程式的功能加上人為限制是一種糟糕的設計. 但是還是有許多其它很不錯的預分配方案. 通常認為,通過系統一次性分配記憶體要比分開幾次分配要好,即使這樣做在程式中浪費了某些記憶體. 如果能夠確定在程式中會有幾項記憶體使用量,在程式啟動時預分配就是一個合理的選擇. 即使不能確定,在開始時為請求控制代碼預分配可能需要的所有記憶體也比在每次需要一點的時候才分配要好. 通過系統一次性連續分配多項記憶體還能極大減少錯誤處理代碼. 在記憶體比較緊張時,預分配可能不是一個好的選擇,但是除非面對最極端的系統內容,否則預分配都是一個穩賺不賠的選擇.

建議二是使用一個記憶體釋放分配的lookaside list(監視列表或者後備列表). 基本的概念是把最近釋放的對象放到鏈表裡而不是真的釋放它,當不久再次需要該對象時,直接從鏈表上取下來用,不用通過系統來分配. 使用lookaside list的一個額外好處是可以避免複雜物件的初始化和清理.

通常,讓lookaside list不受限制的增長,即使在程式空閑時也不釋放佔用的對象是個糟糕的想法. 在避免引入複雜的鎖或競爭情況下,不週期性“清掃"非活躍對象是很有必要的. 一個比較妥當的辦法是,讓lookaside list由兩個可以獨立鎖定的鏈表組成: 一個"新鏈"和一個"舊鏈".使用時優先從"新"鏈分配,然後最後才依靠"舊"鏈. 對象總是被釋放的"新"鏈上。清除線程則按如下規則運行:

1. 鎖住兩個鏈

2. 儲存舊鏈的頭結點

3. 把前一個新鏈掛到舊鏈的前頭

4. 解鎖

5. 在空閑時通過第二步儲存的頭結點開始釋放舊鏈的所有對象

使用了這種方式的系統中,對象只有在真的沒用時才會釋放,釋放至少延時一個清除間隔期(指清除線程的運行間隔),但同常不會超過兩個間隔期. 清除線程不會和普通線程發生鎖競爭. 理論上來說,同樣的方法也可以應用到請求的多個階段,但目前我還沒有發現有這麼用的.

使用lookaside lists有一個問題是,保持指派至需要一個鏈表指標(鏈表結點),這可能會增加記憶體的使用. 但是即使有這種情況,使用它帶來的好處也能夠遠遠彌補這些額外記憶體的花銷.

第三條建議與我們還沒有討論的鎖有關係. 先拋開它不說. 即使使用lookaside list,記憶體配置時的鎖競爭也常常是最大的開銷. 解決方案是使用線程私人的lookasid list, 這樣就可以避免多個線程之間的競爭. 更進一步,每個處理器一個鏈會更好,但這樣只有在非搶先式線程環境下才有用. 基於極端考慮,私人lookaside list甚至可以和一個共用的鏈工作結合起來使用.

鎖競爭(Lock Contention)

高效率的鎖是非常難規劃的, 以至於我把它稱作卡律布狄斯和斯庫拉(參見附錄). 一方面, 鎖的簡單化(廣泛鎖定)會導致平行處理的序列化, 因而降低了並發的效率和系統延展性; 另一方面, 鎖的複雜化(細部鎖定)在空間佔用上和操作時的時間消耗上都可能產生對效能的侵蝕. 偏向於廣泛鎖定會有死結發生,而偏向於細部鎖定則會產生競爭. 在這兩者之間,有一個狹小的路徑通向正確性和高效率,但是路在哪裡?

由於鎖傾向於對程式邏輯產生束縛,所以如果要在不影響程式正常工作的基礎上規划出鎖方案基本是不可能的. 這也就是人們為什麼憎恨鎖,並且為自己設計的不可擴充的單線程方案找借口了.

幾乎我們每個系統中鎖的設計都始於一個"鎖住一切的超級大鎖",並寄希望於它不會影響效能,當希望落空時(幾乎是必然), 大鎖被分成多個小鎖,然後我們繼續禱告(效能不會受影響),接著,是重複上面的整個過程(許多小鎖被分成更小的鎖), 直到效能達到可接受的程度. 通常,上面過程的每次重複都回增加大於20%-50%的複雜性和鎖負荷,並減少5%-10%的鎖競爭. 最終結果是取得了適中的效率,但是實際效率的降低是不可避免的. 設計者開始抓狂:"我已經按照書上的指導設計了細部鎖定,為什麼系統效能還是很糟糕?"

在我的經驗裡,上面的方法從基礎上來說就不正確. 設想把解決方案當成一座山,優秀的方案表示山頂,糟糕的方案表示山穀. 上面始於"超級鎖"的解決方案就好像被形形色色的山穀,凹溝,小山頭和死胡同擋在了山峰之外的登山者一樣,是一個典型的糟糕爬山法;從這樣一個地方開始登頂,還不如下山更容易一些。那麼登頂正確的方法是什麼?

首要的事情是為你程式中的鎖形成一張圖表,有兩個軸:

圖表的縱軸表示代碼. 如果你正在應用剔出了分支的階段架構(指前面說的為請求劃分階段),你可能已經有這樣一張劃分圖了,就像很多人見過的OSI七層網路通訊協定架構圖一樣.

圖表的水平軸表示資料集. 在請求的每個階段都應該有屬於該階段需要的資料集.

現在,你有了一張網格圖,圖上每個儲存格表示一個特定階段需要的特定資料集. 下面是應該遵守的最重要的規則:兩個請求不應該產生競爭,除非它們在同一個階段需要同樣的資料集. 如果你嚴格遵守這個規則,那麼你已經成功了一半.

一旦你定義出了上面那個網格圖,在你的系統中的每種類型的鎖就都可以被標識出來了. 你的下一個目標是確保這些標識出來的鎖儘可能在兩個軸之間均勻的分布, 這部分工作是和具體應用相關的. 你得像個鑽石切割工一樣,根據你對程式的瞭解,找出要求階段和資料集之間的自然"紋理線". 有時候它們很容易發現,有時候又很難找出來,此時需要不斷回顧來發現它. 在程式設計時,把代碼分隔成不同階段是很複雜的事情,我也沒有好的建議,但是對於資料集的定義,有一些建議給你:

如果你能對請求按順序編號,或者能對請求進行雜湊,或者能把請求和事物ID關聯起來,那麼根據這些編號或者ID就能對資料更好的進行分隔.

有時,基於資料集的資源極大化利用,把請求動態分配給資料,相對於依據請求的固有屬性來分配會更有優勢. 就好像現代CPU的多個整數運算單元知道把請求分離一樣.

確定每個階段指定的資料集是不一樣的是非常有用的,以便保證一個階段爭奪的資料在另外階段不會爭奪.

如果你在縱向和橫向上把"鎖空間(這裡實際指鎖的分布)" 分隔了,並且確保了鎖均勻分布在網格上,那麼恭喜你獲得了一個好方案. 現在你處在了一個好的登山點,打個比喻,你面有了一條通向頂峰的緩坡,但你還沒有到山頂. 現在是時候對鎖競爭進行統計,看看該如何改進了. 以不同的方式分隔階段和資料集,然後統計鎖競爭,直到獲得一個滿意的分隔. 當你做到這個程度的時候,那麼無限風景將呈現在你腳下.

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.