使用Chronicle Queue建立低延遲的TB級別的佇列 - DZone

banq發表於2022-01-17

本文介紹如何使用開源 Chronicle Queue建立巨大的持久佇列,同時保持可預測和一致的低延遲。
在本文中,目標是維護來自市場資料饋送的物件佇列(例如,在交易所交易的證券的最新價格)。也可以選擇其他業務領域,例如來自物聯網裝置的感官輸入或讀取汽車行業的碰撞記錄資訊。原理是一樣的。
首先,定義一個持有市場資料的類:

public class MarketData extends SelfDescribingMarshallable {

    int securityId;
    long time;
    float last;
    float high;
    float low;

    // Getters and setters not shown for brevity
}



注意:在實際場景中,使用float和double儲存貨幣值時必須非常小心,否則可能會導致舍入問題 [Bloch18, Item 60]。但是,在這篇介紹性文章中,我想保持簡單。
還有一個小的實用函式MarketDataUtil::create將在呼叫時建立並返回一個新的隨機MarketData物件:


static MarketData create() {

    MarketData marketData = new MarketData();
    int id = ThreadLocalRandom.current().nextInt(1000);
    marketData.setSecurityId(id);
    float nextFloat = ThreadLocalRandom.current().nextFloat();
    float last = 20 + 100 * nextFloat;

    marketData.setLast(last);
    marketData.setHigh(last * 1.1f);
    marketData.setLow(last * 0.9f);
    marketData.setTime(System.currentTimeMillis());

    return marketData;
}

現在,目標是建立一個持久、併發、低延遲、可從多個程式訪問並且可以容納數十億個物件的佇列。
 

天真的方法
有了這些類,就可以探索使用ConcurrentLinkedQueue的簡單方法:

public static void main(String[] args) {

    final Queue<MarketData> queue = new ConcurrentLinkedQueue<>();
    for (long i = 0; i < 1e9; i++) {
        queue.add(MarketDataUtil.create());
    }
  
}

導致失敗的有幾個原因:
  1. ConcurrentLinkedQueue將為新增到佇列中的每個元素建立一個包裝節點。這將有效地使建立的物件數量增加一倍。
  2. 物件放置在 Java 堆上,導致堆記憶體壓力和垃圾收集問題。在我的機器上,這導致我的整個 JVM 變得無響應,唯一的辦法是使用“kill -9”強行殺死它。
  3. 無法從其他程式(即其他 JVM)讀取佇列。
  4. 一旦 JVM 終止,佇列的內容就會丟失。因此,佇列不是持久的。

檢視其他各種標準 Java 類,可以得出結論,不支援大型持久佇列。
 

Chronicle Queue
Chronicle Queue 是一個開源庫,旨在滿足上述要求。這是設定和使用它的一種方法:

public static void main(String[] args) {

    final MarketData marketData = new MarketData();
    final ChronicleQueue q = ChronicleQueue
            .single("market-data");

    final ExcerptAppender appender = q.acquireAppender();

    for (long i = 0; i < 1e9; i++) {
        try (final DocumentContext document =
                     appender.acquireWritingDocument(false)) {

             document
                    .wire()
                    .bytes()
                    .writeObject(MarketData.class, 
                            MarketDataUtil.recycle(marketData));
        }
    }
}


使用配備 2.3 GHz 8 核英特爾酷睿 i9 的 MacBook Pro 2019 時,僅使用一個執行緒即可插入每秒超過 3,000,000 條訊息。佇列透過給定目錄“market-data”中的記憶體對映檔案持久化。人們會期望MarketData物件至少佔用 4 (int securityId) + 8 (long time) + 4*3 (float last, high 和 low) = 24 位元組。
在上面的示例中,附加了 10 億個物件,導致對映檔案佔用 30,148,657,152 個位元組,這意味著每條訊息大約 30 個位元組。在我看來,這確實非常有效。
 
從編年史佇列中讀取很簡單。繼續上面的示例,下面顯示瞭如何從佇列中讀取前兩個MarketData物件:

public static void main(String[] args) {

    final ChronicleQueue q = ChronicleQueue
            .single("market-data");
  
    final ExcerptTailer tailer = q.createTailer();

    for (long i = 0; i < 2; i++) {

        try (final DocumentContext document =
                     tailer.readingDocument()) {

            MarketData marketData = document
                    .wire()
                    .bytes()
                    .readObject(MarketData.class);

            System.out.println(marketData);
        }
    }
}


還有許多其他功能超出了本文的範圍。例如,可以將佇列檔案設定為以特定間隔(例如每天、每小時或每分鐘)滾動,從而有效地建立資訊分解,以便隨著時間的推移清理舊資料。還有一些規定能夠隔離 CPU 並將 Java 執行緒鎖定到這些隔離的 CPU,從而顯著減少應用程式抖動。
 

相關文章