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


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

譯者序

前些天在並發編程網,看到了關於 Disruptor 的介紹。感覺此框架驚為天人,值得學習學習。在把並發編程網上面介紹逐一瀏覽之后發覺,缺少了對於 Disruptor 基礎應用的介紹。於是就有了翻譯海外基礎介紹的想法。

  • 首先

要為以后難以在工作中用到 Disruptor 而感到沮喪。因為據介紹來看,它號稱"能夠在一個線程里每秒處理6百萬訂單" 。我所在的平台撐不起這個量,同時也限於學歷跟從業背景難以去這類大公司供職。

  • 其次

追逐性能,常常來說你給老板省了多少硬件,老板是看不到的。
建議一開始還是不要設計得性能太過優秀,不然老板看不到你的價值。

  • 最后

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:

此次翻譯拖了快兩個月,糾結、消沉、迷離、回歸。
開始覺得不斷的技術探索,仿佛只是對於前途的過多焦慮,讓自己更多的沉浸於忙碌,從而更多的抬頭看路。
看到很多人接下來的路,只是混混資歷跟業務。然后慢慢的加薪拿股權,就算是人工智能其實也沒有什么明朗的技術變現路線。
技術再好,也需要自我營銷與宣傳。止步眼前,心中頗多不甘。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM