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