1. 程式人生 > >剖析Disruptor:為什麼會這麼快?(一)Ringbuffer的特別之處

剖析Disruptor:為什麼會這麼快?(一)Ringbuffer的特別之處

作者:Trisha    譯者寒桐  校對:方騰飛

最近,我們開源了LMAX Disruptor,它是我們的交易系統吞吐量快(LMAX是一個新型的交易平臺,號稱能夠單執行緒每秒處理數百萬的訂單)的關鍵原因。為什麼我們要將其開源?我們意識到對高效能程式設計領域的一些傳統觀點,有點不對勁。我們找到了一種更好、更快地線上程間共享資料的方法,如果不公開於業界共享的話,那未免太自私了。同時開源也讓我們覺得看起來更酷。

從這個站點,你可以下載到一篇解釋什麼是Disruptor及它為什麼如此高效能的文件。這篇文件的編寫過程,我並沒有參與太多,只是簡單地插入了一些標點符號和重組了一些我不懂的句子,但是非常高興的是,我仍然從中提升了自己的寫作水平。

我發現要把所有的事情一下子全部解釋清楚還是有點困難的,所有我準備一部分一部分地解釋它們,以適合我的NADD聽眾。

首先介紹ringbuffer。我對Disruptor的最初印象就是ringbuffer。但是後來我意識到儘管ringbuffer是整個模式(Disruptor)的核心,但是Disruptor對ringbuffer的訪問控制策略才是真正的關鍵點所在。

ringbuffer到底是什麼?

嗯,正如名字所說的一樣,它是一個環(首尾相接的環),你可以把它用做在不同上下文(執行緒)間傳遞資料的buffer。

RingBuffer.png

(好吧,這是我通過畫圖板手畫的,我試著畫草圖,希望我的強迫症不會讓我畫完美的圓和直線)

基本來說,ringbuffer擁有一個序號,這個序號指向陣列中下一個可用的元素。(校對注:如下圖右邊的圖片表示序號,這個序號指向陣列的索引4的位置。)

RingBufferInitial.png

隨著你不停地填充這個buffer(可能也會有相應的讀取),這個序號會一直增長,直到繞過這個環。

要找到陣列中當前序號指向的元素,可以通過mod操作:

               sequence mod array length = array index

以上面的ringbuffer為例(java的mod語法):12 % 10 = 2。很簡單吧。

事實上,上圖中的ringbuffer只有10個槽完全是個意外。如果槽的個數是2的N次方更有利於基於二進位制的計算機進行計算。

(校對注:2的N次方換成二進位制就是1000,100,10,1這樣的數字, sequence & (array length-1) = array index,比如一共有8槽,3&(8-1)=3,HashMap就是用這個方式來定位陣列元素的,這種方式比取模的速度更快。)

那又怎麼樣?

如果你看了維基百科裡面的關於環形buffer的詞條,你就會發現,我們的實現方式,與其最大的區別在於:沒有尾指標。我們只維護了一個指向下一個可用位置的序號。這種實現是經過深思熟慮的—我們選擇用環形buffer的最初原因就是想要提供可靠的訊息傳遞。我們需要將已經被服務傳送過的訊息儲存起來,這樣當另外一個服務通過nak (校對注:拒絕應答訊號)告訴我們沒有成功收到訊息時,我們能夠重新發送給他們。

聽起來,環形buffer非常適合這個場景。它維護了一個指向尾部的序號,當收到nak(校對注:拒絕應答訊號)請求,可以重發從那一點到當前序號之間的所有訊息:

RingBufferReplay.png

我們實現的ring buffer和大家常用的佇列之間的區別是,我們不刪除buffer中的資料,也就是說這些資料一直存放在buffer中,直到新的資料覆蓋他們。這就是和維基百科版本相比,我們不需要尾指標的原因。ringbuffer本身並不控制是否需要重疊(決定是否重疊是生產者-消費者行為模式的一部分–如果你等不急我寫blog來說明它們,那麼可以自行檢出Disruptor專案)。

它為什麼如此優秀?

之所以ringbuffer採用這種資料結構,是因為它在可靠訊息傳遞方面有很好的效能。這就夠了,不過它還有一些其他的優點。

首先,因為它是陣列,所以要比連結串列快,而且有一個容易預測的訪問模式。(譯者注:陣列內元素的記憶體地址的連續性儲存的)。這是對CPU快取友好的—也就是說,在硬體級別,陣列中的元素是會被預載入的,因此在ringbuffer當中,cpu無需時不時去主存載入陣列中的下一個元素。(校對注:因為只要一個元素被載入到快取行,其他相鄰的幾個元素也會被載入進同一個快取行)

其次,你可以為陣列預先分配記憶體,使得陣列物件一直存在(除非程式終止)。這就意味著不需要花大量的時間用於垃圾回收。此外,不像連結串列那樣,需要為每一個新增到其上面的物件創造節點物件—對應的,當刪除節點時,需要執行相應的記憶體清理操作。

缺少的部分

我並沒有在本文中介紹如何避免ringbuffer產生重疊,以及如何對ringbuffer進行讀寫操作。你可能注意到了我將ringbuffer和連結串列那樣的資料結構進行比較,因為我並認為連結串列是實際問題的標準答案。

當你將Disruptor和基於 佇列之類的實現進行比較時,事情將變得很有趣。佇列通常注重維護佇列的頭尾元素,新增和刪除元素等。所有的這些我都沒有在ringbuffer裡提到,這是因為ringbuffer不負責這些事情,我們把這些操作都移到了資料結構(ringbuffer)的外部

到這個站點閱讀文章或者檢出程式碼可以瞭解更多細節。或者觀看Mike 和Martin在去年San Francisco QCon大會上的視訊,或者再等我一些時間來思考剩下的東西,然後在接下來的blog中逐一介紹。