[翻譯]高併發框架 LMAX Disruptor 介紹

原文地址:Concurrency with LMAX Disruptor – An Introductionhtml

譯者序

前些天在併發編程網,看到了關於 Disruptor 的介紹。感受此框架驚爲天人,值得學習學習。在把併發編程網上面介紹逐一瀏覽以後發覺,缺乏了對於 Disruptor 基礎應用的介紹。因而就有了翻譯海外基礎介紹的想法。git

  • 首先

要爲之後難以在工做中用到 Disruptor 而感到沮喪。由於據介紹來看,它號稱"可以在一個線程裏每秒處理6百萬訂單" 。我所在的平臺撐不起這個量,同時也限於學歷跟從業背景難以去這類大公司供職。github

  • 其次

追逐性能,經常來講你給老闆省了多少硬件,老闆是看不到的。
建議一開始仍是不要設計得性能太過優秀,否則老闆看不到你的價值。編程

  • 最後

Disruptor 是一個在併發編程中避免資源競爭的容器,用於協調生產者與消費者之間的關係,同時有着領域驅動模型 CQRS框架那種基於命令的影子。
應用這個框架編寫代碼將會較爲繁複,模塊與模塊以前的通訊全由一個又一個Event類來協調。
相對於大多數喜歡一個方法到底的開發同窗來講會比較麻煩,畢竟須要定義更多類。數組

1. 概覽

本篇文章目的在於介紹 LMAX Disruptor,探討它是如何幫助咱們實現軟件低延遲、高併發特性。
咱們還將介紹 Disruptor 庫的基本用法。緩存

2. Disruptor 是什麼?

Disruptor 是由 LMAX 編寫的開源Java庫。它是個併發編程框架,用於處理大量事務,並且低延遲(然而並不會像常規併發代碼那樣複雜)。
如此高效的性能優化,是經過更高效的利用底層硬件的設計實現。性能優化

2.1. 機械情懷

讓咱們從機械情懷的核心概念開始 - 這就是了解底層硬件如何以最屌的方式運行。數據結構

舉個栗子,架構

到CPU的延遲 CPU時鐘 耗時
主內存 不少(Multiple) ~60-80 ns
L3 緩存 ~40-45 週期 ~15 ns
L2 緩存 ~10 週期 ~3 ns
L1 緩存 ~3-4 週期 ~1 ns
寄存器 1 週期 ~15 ns

2.2. 爲何不用隊列

生產者和消費者之間經常速率不一致,隊列一般老是爲"空"或"滿"。所以隊列頭(head)、隊列尾(tail)和隊列大小(size)有着資源競爭(write contention)。生產和消費不多達到和諧的狀態。併發

一般採用鎖來解決資源競爭(write contention)問題,但與此同時又會陷入內核級別的上下文切換。當這種狀況發生時,處理器所緩存的數據可能丟失。(譯者注:當線程A、B分別在CPU上不一樣的兩個內核上運行時,線程A正要更新變量Y。不幸的是,這個變量也同時正要被線程B所更新。若是核心A得到了全部權,緩存子系統將會使核心B中對應的緩存行失效。當核心B得到了全部權而後執行更新操做,核心A就要使本身對應的緩存行失效。這會來來回回的通過L3緩存,大大影響了性能。)

爲了達到更好的線程可伸縮性,就必須確保不會有兩個寫線程操做同一個變量(多個讀線程是沒有問題的,如同處理器間調用高速連接獲取緩存)。隊列,它敗在了獨立寫入原則(one-writer principle)。

若是兩個不一樣的線程寫入隊列中兩個不一樣的值,那麼每一個內核都會使另一個線程的緩存行失效(數據在主內存與高速緩存之間的傳輸是作的固定大小的塊傳輸,稱之爲緩存行。譯者注:僞共享和緩存行)。儘管兩個線程寫入兩個不一樣的變量,也一樣會引發它們間的資源競爭。這叫作僞共享,由於每次訪問隊列頭(head),隊列尾(tail)也一樣會被加載到緩存行,反之亦然。

2.3. Disruptor是如何工做的?


Disruptor 有一個基於數組的循環數據結構(環裝緩衝區)。這個循環數據結構,它是個擁有下個可用元素引用的數組。預先分配了對象內存空間。生產者與消費者經過這個循環數據結構進行讀寫操做,並不會有鎖或資源競爭。

Disruptor 中,全部事件(events)以組播的方式被髮布給全部消費者,以便下游隊列經過並行的方式進行消費。由於消費者的並行消費,須要協調消費者間的依賴關係(依賴關係圖)。

生產者和消費者中有個序列計數器,指示緩衝區中當前正在被它所處理的元素。全部生產者或消費者都只能夠修改它本身的序列計數器,但同時能夠讀取其餘的序列計數器

3. 使用Disruptor

3.1. Maven 依賴

讓咱們把Disruptor 庫的依賴關係添加到 pom.xml中。

<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.3.6</version>
</dependency>

最新版本的依賴關係能夠在這裏找到。

3.2. 定義 Event

讓咱們來定義一個攜帶數據的 Event:

public static class ValueEvent {
    private int value;
    public final static EventFactory EVENT_FACTORY 
      = () -> new ValueEvent();
 
    // standard getters and setters
}

這個 EventFactory 會讓 Disruptor分配事件。

3.3. 消費者(Consumer)

消費者從環裝緩衝區讀取數據。讓咱們來定義個處理事件的消費者:

public class SingleEventPrintConsumer {
    ...
 
    public EventHandler<ValueEvent>[] getEventHandler() {
        EventHandler<ValueEvent> eventHandler 
          = (event, sequence, endOfBatch) 
            -> print(event.getValue(), sequence);
        return new EventHandler[] { eventHandler };
    }
  
    private void print(int id, long sequenceId) {
        logger.info("Id is " + id 
          + " sequence id that was used is " + sequenceId);
    }
}

在咱們的示例中,消費者只是打印打印日誌。

3.4. 構造 Disruptor

構造 Disruptor:

ThreadFactory threadFactory = DaemonThreadFactory.INSTANCE;
 
WaitStrategy waitStrategy = new BusySpinWaitStrategy();
Disruptor<ValueEvent> disruptor 
  = new Disruptor<>(
    ValueEvent.EVENT_FACTORY, 
    16, 
    threadFactory, 
    ProducerType.SINGLE, 
    waitStrategy);

在這個 Disruptor 的構造方法中,依次定義瞭如下參數:

  • Event Factory – 負責生成用於填充環裝緩衝區的事件對象;
  • The size of Ring Buffer – 定義環裝緩衝區的大小。它必須是2的冪,不然會在初始化時拋出異常。由於重點在於使用邏輯二進制運算符有着更好的性能;(例如:mod運算)
  • Thread Factory – 事件處理線程建立工廠;
  • Producer Type – 指定是否有單個或者多個生產者;
  • Waiting strategy – 定義如何處理沒法跟上生產者步伐的慢消費者;

鏈接消費者處理程序:

disruptor.handleEventsWith(getEventHandler());

Disruptor能夠提供多個消費者來處理生產者生成的數據。在上面的例子中,咱們只使用了一個消費者處理事件。

3.5. 啓動 Disruptor

RingBuffer<ValueEvent> ringBuffer = disruptor.start();

3.6 構造和發佈事件(Event)

生產者將參數按順序放置到環形緩衝區中。(譯者注:3.4所述Event Factory已經做爲參數,構造Disruptor對象)生產者必須獲取到到下個可用元素,以免覆蓋還沒有消耗的元素。

利用 RingBuffer 發佈事件:

for (int eventCount = 0; eventCount < 32; eventCount++) {
    long sequenceId = ringBuffer.next();
    ValueEvent valueEvent = ringBuffer.get(sequenceId);
    valueEvent.setValue(eventCount);
    ringBuffer.publish(sequenceId);
}

在此,生產者依次生產、發佈事件。值得注意的是 Disruptor 與2階段提交協議相似。它先獲取一個新序列號(sequenceId),再經過(sequenceId)獲取事件,而後製做事件,最後發佈。下次得到sequenceId + 1。

4. 總結

在本教程中,咱們已經闡述了 Disruptor是什麼,它是如何實現低延遲的併發處理。回顧了機械情懷的理念,以及如何利用它實現低延遲。最後展現了一個使用 Disruptor 庫的例子。

示例代碼能夠在GitHub項目中找到。這是一個基於Maven的項目,因此它很容易導入和運行。

引用:

DDD CQRS架構和傳統架構的優缺點比較
僞共享(False Sharing)
僞共享和緩存行

ps:

這次翻譯拖了快兩個月,糾結、消沉、迷離、迴歸。 開始以爲不斷的技術探索,彷彿只是對於前途的過多焦慮,讓本身更多的沉浸於忙碌,從而更多的擡頭看路。 看到不少人接下來的路,只是混混資歷跟業務。而後慢慢的加薪拿股權,就算是人工智能其實也沒有什麼明朗的技術變現路線。 技術再好,也須要自我營銷與宣傳。止步眼前,心中頗多不甘。

相關文章
相關標籤/搜索