[從原始碼學設計] Flume 之 memory channel

羅西的思考發表於2021-01-29

[從原始碼學設計] Flume 之 memory channel

0x00 摘要

在使用Flume時,有時遇到如下錯誤資訊:Space for commit to queue couldn't be acquired

究其原因,是在memory channel的使用中出現了問題。

本文就以此為切入點,帶大家一起剖析下 Flume 中 MemoryChannel 的實現

0x01 業務範疇

1.1 用途和特點

Flume的用途:高可用的,高可靠的,分散式的海量日誌採集、聚合和傳輸的系統。

這裡我們介紹與本文相關的特點:

  • Flume的管道是基於事務,保證了資料在傳送和接收時的一致性.
  • Flume是可靠的,容錯性高的,可升級的,易管理的,並且可定製的
  • 當收集資料的速度超過將寫入資料的時候,也就是當收集資訊遇到峰值時,這時候收集的資訊非常大,甚至超過了系統的寫入資料能力,這時候,Flume會在資料生產者和資料收容器間做出調整,保證其能夠在兩者之間提供平穩的資料.

1.2 Channel

這裡就要介紹channel的概念。channel是一種短暫的儲存容器,它將從source處接收到的event格式的資料快取起來,直到它們被sinks消費掉,它在source和sink間起著橋樑的作用,channel是一個完整的事務,這一點保證了資料在收發的時候的一致性。並且它可以和任意數量的source和sink連結。

支援的型別主要有: JDBC channel , File System channel , Memory channel等,大致區別如下:

  • Memory Channel:events儲存在Java Heap,即記憶體佇列中(記憶體的大小是可以指定的)。對於流量較高和由於agent故障而準備丟失資料的流程來說,這是一個理想的選擇;
  • File Channel:event儲存在本地檔案中,可靠性高,但吞吐量低於Memory Channel;
  • JDBC Channel :event儲存在持久化儲存庫中(其背後是一個資料庫),JDBC channel目前支援嵌入式Derby。這是一個持續的channel,對於可恢復性非常重要的流程來說是理想的選擇;
  • Kafka Channel:events儲存在Kafka叢集中。Kafka提供高可用性和高可靠性,所以當agent或者kafka broker 崩潰時,events能馬上被其他sinks可用。

本文主要涉及Memory Channel,所以看看其特性。

  • 好處:速度快,吞吐量大;
  • 壞處:根據計算機工作的原理就可以得知,凡是在記憶體中計算的資料,只要電腦出現故障導致停機,那麼記憶體中資料是不會進行儲存的;
  • 所適用的場景:高吞吐量,允許資料丟失的業務中;

1.3 研究重點

由此,我們可以總結出來 Flume 的一些重點功能:

  • 可靠的,容錯性高的;
  • 實現事務;
  • 速度快,吞吐量大;
  • 可以調節收集的速度以解決生產者消費者不一致;
  • 可升級的,易管理,可定製的;

因為MemoryChannel屬於Flume的重要模組,所以,我們本文就看看是MemoryChannel是如何確保Flume以上特點的,這也是本文的學習思路。

1.4 實際能夠學到什麼

如何回滾,使用鎖,訊號量 ,動態擴容,如何解決生產者消費者不一致問題。

1.5 總述

MemoryChannel還是比較簡單的,主要是通過MemoryTransaction中的putList、takeList與MemoryChannel中的queue進行資料流轉和事務控制,這裡的queue相當於持久化層,只不過放到了記憶體中,如果是FileChannel的話,會把這個queue放到本地檔案中。

MemoryChannel受記憶體空間的影響,如果資料產生的過快,同時獲取訊號量超時容易造成資料的丟失。而且Flume程式掛掉,資料也會丟失。

具體是:

  • 維持一個佇列,佇列的兩端分別是source和sink。
  • source使用doPut方法往putList插入Event
  • sink使用doTake方法從queue中獲取event放入takeList,並且提供rollback方法,用於回滾。
  • commit方法作用是把putList中的event一次性寫到queue;

下面表示了Event在一個使用了MemoryChannel的agent中資料流向:

source ---> putList ---> queue ---> takeList ---> sink

為了大家更好的理解,我們提前把最終圖例發到這裡。

具體如下圖:

+----------+                                                                          +-------+
|  Source  |    +----------------------------------------------------------------+    | Sink  |
+-----+----+    | [MemoryChannel]                                                |    +---+---+
      |         |   +--------------------------------------------------------+   |        ^
      |         |   | [MemoryTransaction]                                    |   |        |
      |         |   |                                                        |   |        |
      |         |   |                                                        |   |        |
      |         |   |    channelCounter                                      |   |        |
      |         |   |                                                        |   |        |
      |         |   |    putByteCounter                     takeByteCounter  |   |        |
      |         |   |                                                        |   |        |
      |         |   |    +-----------+                      +------------+   |   |doTake  |
      +----------------> |  putList  |                      |  takeList  +----------------+
      doPut     |   |    +----+--+---+                      +----+---+---+   |   |
                |   |         |  ^                               |   ^       |   |
                |   |         |  |                               |   |       |   |
                |   +--------------------------------------------------------+   |
                |             |  |                               |   | poll      |
                |             |  |                               |   |           |
                |             |  |  rollback         rollback    |   |           |
                |             |  +--------------+  +-------------+   |           |
                |             |                 |  |                 |           |
                |             |                 |  v                 |           |
                |             |  doCommit    +--+--+---+  doCommit   |           |
                |             +------------> |  queue  | +-----------+           |
                |                            +---------+                         |
                +----------------------------------------------------------------+

手機上如圖:

img

0x02 定義

我們要看看MemoryChannel重要變數的定義,這裡我們沒有按照程式碼順序來,而是重新整理。

2.1 介面

MemorChannel中最重要的部分主要是Channel、Transaction 和Configurable三個介面。

Channel介面 主要宣告瞭Channel中的三個方法,就是佇列基本功能

public void put(Event event) throws ChannelException; //從指定的Source中獲得Event放入指定的Channel中
public Event take() throws ChannelException;     //從Channel中取出event放入Sink中
public Transaction getTransaction();             //獲得當前Channel的事務例項

Transaction介面 主要宣告瞭flume中事務機制的四個方法,就是事務功能

enum TransactionState { Started, Committed, RolledBack, Closed }    //列舉型別,指定了事務的四種狀態,事務開始、提交、失敗回滾、關閉
void begin(); 
void commit();
void rollback();
void close();

Configurable介面 主要是和flume配置元件相關的,需要從flume配置系統獲取配置資訊的任何元件,都必須實現該介面。該介面中只宣告瞭一個context方法,用於獲取配置資訊。

大體邏輯如下:

+-----------+          +--------------+        +---------------+
|           |          |              |        |               |
|  Channel  |          |  Transaction |        | Configurable  |
|           |          |              |        |               |
+-----------+          +--------------+        +---------------+

     ^                        ^                        ^
     |                        |                        |
     |                        |                        |
     |                        |                        |
     |          +-------------+--------------+         |
     |          |                            |         |
     |          |        MemorChannel        +---------+
     +-------+  |                            |
                |                            |
                |                            |
                |                            |
                |                            |
                |                            |
                |                            |
                +----------------------------+

下面我們具體講講成員變數。

2.2 配置引數

首先是一系列業務配置引數。

  //定義佇列中一次允許的事件總數
  private static final Integer defaultCapacity = 100;
  
  //定義一個事務中允許的事件總數
  private static final Integer defaultTransCapacity = 100;

  //將實體記憶體轉換成槽(slot)數,預設是100
  private static final double byteCapacitySlotSize = 100;
  
  //定義佇列中事件所使用空間的最大位元組數(預設是JVM最大可用記憶體的0.8)
  private static final Long defaultByteCapacity = (long)(Runtime.getRuntime().maxMemory() * .80);

  //定義byteCapacity和預估Event大小之間的緩衝區百分比:
  private static final Integer defaultByteCapacityBufferPercentage = 20;
  
  //新增或者刪除一個event的超時時間,單位秒:
  private static final Integer defaultKeepAlive = 3;

  // maximum items in a transaction queue
  private volatile Integer transCapacity;
  private volatile int keepAlive;
  private volatile int byteCapacity;
  private volatile int lastByteCapacity;
  private volatile int byteCapacityBufferPercentage;
  private ChannelCounter channelCounter;

這些引數基本都在configure(Context context)中設定,基本邏輯如下:

  • 設定 capacity:MemroyChannel的容量,預設是100。

  • 設定 transCapacity:每個事務最大的容量,也就是每個事務能夠獲取的最大Event數量。預設也是100。事務容量必須小於等於Channel Queue容量。

  • 設定 byteCapacityBufferPercentage:用來確定byteCapacity的一個百分比引數,即我們定義的位元組容量和實際事件容量的百分比,因為我們定義的位元組容量主要考慮Event body,而忽略Event header,因此需要減去Event header部分的記憶體佔用,可以認為該引數定義了Event header佔了實際位元組容量的百分比,預設20%;

  • 設定 byteCapacity:byteCapacity等於設定的byteCapacity值或堆的80%乘以1減去byteCapacityBufferPercentage的百分比,然後除以100。具體是首先讀取配置檔案定義的byteCapacity,如果沒有定義,則使用預設defaultByteCapacity,而defaultByteCapacity預設是JVM實體記憶體的80%(Runtime.getRuntime().maxMemory() * .80);那麼實際byteCapacity=定義的byteCapacity * (1- Event header百分比)/ byteCapacitySlotSize;byteCapacitySlotSize預設100,即計算百分比的一個係數。

  • 設定 keep-alive:增加和刪除一個Event的超時時間(單位:秒)。

  • 設定初始化 LinkedBlockingDeque物件,大小為capacity。以及各種訊號量物件。

  • 最後初始化計數器。

配置程式碼摘要如下:

public void configure(Context context) {
    capacity = context.getInteger("capacity", defaultCapacity);
    transCapacity = context.getInteger("transactionCapacity", defaultTransCapacity);
    byteCapacityBufferPercentage = context.getInteger("byteCapacityBufferPercentage",
                                                       defaultByteCapacityBufferPercentage);
    byteCapacity = (int) ((context.getLong("byteCapacity", defaultByteCapacity).longValue() *(1 - byteCapacityBufferPercentage * .01)) / byteCapacitySlotSize);
    if (byteCapacity < 1) {
        byteCapacity = Integer.MAX_VALUE;
    }
    keepAlive = context.getInteger("keep-alive", defaultKeepAlive);
    resizeQueue(capacity);
    if (channelCounter == null) {
      channelCounter = new ChannelCounter(getName());
    }
}

2.2.1 channel屬性

ChannelCounter 需要單獨說一下。其就是把channel的一些屬性封裝了一下,初始化了一個ChannelCounter,是一個計數器,記錄如當前佇列放入Event數、取出Event數、成功數等。

private ChannelCounter channelCounter;

定義如下:

public class ChannelCounter extends MonitoredCounterGroup implements
    ChannelCounterMBean {
  private static final String COUNTER_CHANNEL_SIZE = "channel.current.size";
  private static final String COUNTER_EVENT_PUT_ATTEMPT ="channel.event.put.attempt";
  private static final String COUNTER_EVENT_TAKE_ATTEMPT = "channel.event.take.attempt";
  private static final String COUNTER_EVENT_PUT_SUCCESS = "channel.event.put.success";
  private static final String COUNTER_EVENT_TAKE_SUCCESS = "channel.event.take.success";
  private static final String COUNTER_CHANNEL_CAPACITY = "channel.capacity";
}

2.4 Semaphore和Queue

其次是Semaphore和Queue。主要就是用來協助制事務。

MemoryChannel有三個訊號量用來控制事務,防止容量越界:queueStored,queueRemaining,bytesRemaining。

  • queueLock:建立一個Object當做佇列鎖,操作佇列的時候保證資料的一致性;
  • queue:使用LinkedBlockingDeque queue維持一個佇列,佇列的兩端分別是source和sink;
  • queueStored:來儲存queue中當前的儲存的event的數目,即已經儲存的容量大小,後面tryAcquire方法可以判斷是否可以take到一個event;
  • queueRemaining:來儲存queue中當前可用的容量,即空閒的容量大小,可以用來判斷當前是否有可以提交一定數量的event到queue中;
  • bytesRemaining : 表示可以使用的記憶體大小。該大小就是計算後的byteCapacity值。
  private Object queueLock = new Object();

  @GuardedBy(value = "queueLock")
  private LinkedBlockingDeque<Event> queue;

  private Semaphore queueRemaining;

  private Semaphore queueStored;

  private Semaphore bytesRemaining;// 表示可以使用的記憶體大小。該大小就是計算後的byteCapacity值。

2.5 MemoryTransaction

內部類MemoryTransaction是整個事務保證最重要的類。

MemoryTransaction用來接收資料和事務控制。該類繼承BasicTransactionSemantics類。

MemoryTransaction維護了兩個佇列,一個用於Source的put,一個用於Sink的take,容量大小為事務的容量(transCapacity)。

  • takeList:take事務用到的佇列;阻塞雙端佇列,從channel中取event先放入takeList,輸送到sink,commit成功,從channel queue中刪除;
  • putList:put事務用到的佇列;從source 會先放至putList,然後commit傳送到channel queue佇列;
  • channelCounter:channel屬性;ChannelCounter類定義了監控指標資料的一些屬性方法
  • putByteCounter:put位元組數計數器;
  • takeByteCounter:take位元組計數器;
private class MemoryTransaction extends BasicTransactionSemantics {
    private LinkedBlockingDeque<Event> takeList;
    private LinkedBlockingDeque<Event> putList;
    private final ChannelCounter channelCounter;
    private int putByteCounter = 0;
    private int takeByteCounter = 0;
}

無論是Sink,還是Source都會呼叫getTransaction()方法,獲取當前Channel的事務例項。

介面與成員變數大致邏輯可以理解如下,其中 Channel 的 API 表示這裡是 MemorChannel 的對外 API:

+-----------+                    +--------------+                  +---------------+
|           |                    |              |                  |               |
|  Channel  |                    |  Transaction |                  | Configurable  |
|           |                    |              |                  |               |
+---+-------+                    +--------------+                  +---------------+
    ^
    |                                    ^                                  ^
    |                                    |                                  |
    |                                    |                                  |
    |        +--------------------------------------------------------+     |
    |        |                           |                            |     |
    |        |  MemoryChannel            |                            |     |
    |        |                           +                            |     |
    |        |                                                        |     |
    |        |                    MemoryTransaction                   |     |
    |        |                                                        |     |
    |        |                    Semaphore / Queue                   |     |
    |        |                                                        |     |
    +--------+                                                        |     |
     API     |                                                        |     |
             |                                                        |     |
             |                               Config Parameters +------------+
             |                                                        |
             |                                                        |
             +--------------------------------------------------------+

0x03 使用

看了上面講的,估計大家還是會暈,因為成員變數和概念實在是太多了,所以我們從使用入手分析。

前面提到,memory channel內部有三個佇列,分別是putList,queue,takeList。其中putList,takeList在MemoryTransaction之中。

3.1 channel如何使用

channel之上有一把鎖,當source主動向channel放資料或者sink主動從channel取資料時,會搶鎖,誰取到鎖,誰就可以操作channel。

每次使用時會首先呼叫tx.begin()開始事務,也就是獲取鎖。然後呼叫tx.commit()提交資料或者呼叫tx.rollback()取消操作。

這裡需要注意的是:Source, Sink 都是死迴圈,搶同一個鎖。所以就會有消費者,生產者速度不一致的情況,所以就需要有 一個內部的 buffer,就是我們的Queue。

3.2 source往channel放資料

這是一個死迴圈,source一直試圖獲取channel鎖,然後從kafka獲取資料,放入channel中,那每次放入多少個資料呢?在KafkaSource.java中,程式碼是這樣的:

while (eventList.size() < batchUpperLimit &&
		System.currentTimeMillis() < maxBatchEndTime) {
}

含義就是:每次最多放batchUpperLimit或最多等待maxBatchEndTime的時間,就結束向channel放資料。

當獲取了足夠的資料,首先放入putList中,然後就會呼叫tx.commit()將putList的全部資料放入queue中。

3.3 sink從channel取資料

也是一個死迴圈,sink一直試圖獲取channel鎖,然後從channel取一批資料,放入sink和takeList(僅僅用於回滾,在呼叫rollback時takeList的資料會回滾到queue中)。每次取多少個event呢?以HDFSEventSink為例,程式碼如下:

for (txnEventCount = 0; txnEventCount < batchSize; txnEventCount++) {
    Event event = channel.take();
    if (event == null) 
    		break;
}

batchSize的大小預設是100,由hdfs.batchSize控制。

具體如下:

                                     +--------------->
                                   ^                 |
                                   |                 |   while(1)
                                   |                 v
   +-----------+                   |            +----+----+
   |  Source   |                   |  take      |  Sink   |
   |           |                   |            |         |
   +-----+-----+                   |            +---------+
         |                         |
         |           +-------------+--+
         |           | Channel        |
         |           |                |
While(1) |           |                |
         |           |       buffer   |
         |           +----------------+
         |
         |                 ^
         |                 |
         |                 |  put
         v ----------------^

0x04 實現事務

此處回答了前面提到的兩個重點:

  • 可靠的,容錯性高的;
  • 實現事務;

其實就是用事務保證整個流程的高可靠,其核心就在從source抽取資料到channel,從channel抽取到sink,當sink被消費後channel資料刪除的這三個環節。而這些環節在flume中被統一的用事務管理起來。可以說,這是flume高可靠的關鍵一點

具體涉及到的幾個點如下:

  • MemoryTransaction是實現事務的核心。每次使用時會首先呼叫tx.begin()開始事務,也就是獲取鎖。然後呼叫tx.commit()提交資料或者呼叫tx.rollback()取消操作。
  • MemoryChannel時設計時考慮了兩個容量:Channel Queue容量和事務容量,而這兩個容量涉及到了數量容量和位元組數容量。
  • MemoryChannel 會根據事務容量 transCapacity 建立兩個阻塞雙端佇列putList和takeList,這兩個佇列(相當於兩個臨時緩衝佇列)主要就是用於事務處理的。即,每個事務都有一個Take List和Put List分別用於儲存事務相關的取資料和放資料,等事務提交時才完全同步到Channel Queue,或者失敗把取資料回滾到Channel Queue。
    • 首先由一個Channel Queue用於儲存整個Channel的Event資料;
    • 當從Source往 Channel中放事件event 時,會先將event放入 putList 佇列,然後將putList佇列中的event 放入 MemoryChannel的queue中。
    • 當從 Channel 中將資料傳送給 Sink 時,則會將event先放入 takeList 佇列中,然後從takeList佇列中將event送入Sink,不論是 put 還是 take 發生異常,都會呼叫 rollback 方法回滾事務。
    • 回滾時,會先給 Channel 加鎖防止回滾時有其他執行緒訪問,若takeList 不為空, 就將寫入 takeList中的event再次放入 Channel 中,然後移除 putList 中的所有event(即就是丟棄寫入putList臨時佇列的 event)。
  • 因為多個事務要操作Channel Queue,還要考慮Channel Queue的動態擴容問題,因此MemoryChannel使用了鎖來實現;而容量問題則使用了訊號量來實現。

我們下面具體走一下這個流程。

4.1 put事務

此事務發生在在Source到Channel之間,是從指定的Source中獲得Event放入指定的Channel中,具體包括:

  • doPut:將批資料先寫入臨時緩衝區 putList;
  • doCommit:檢查 channel 記憶體佇列是否足夠合併;
  • doRollback:channel 記憶體佇列空間不足,回滾資料;

如下呼叫。

  try {
    tx.begin();
    //底層就是呼叫的doPut方法
    // Source寫事件呼叫put方法
    reqChannel.put(event);
    tx.commit();
  } catch (Throwable t) {
    // 發生異常則回滾事務
    tx.rollback();
    if (t instanceof Error) {
      throw (Error) t;
    } else if (t instanceof ChannelException) {
      throw (ChannelException) t;
    } else {
      throw new ChannelException("Unable to put event on required " +
          "channel: " + reqChannel, t);
    }
  } finally {
    if (tx != null) {
      tx.close();
    }
  }

下面分析doPut方法。

doPut邏輯如下:

  • 計算event大概佔用的slot數;
  • offer方法往putList中新增event,等事務提交時轉移到Channel Queue,如果滿了則直接拋異常回滾事務;
  • 累加這一條event所佔用的slot空間,以便之後做位元組容量限制。

具體程式碼如下:

protected void doPut(Event event) throws InterruptedException {    
      //增加放入事件計數器
      channelCounter.incrementEventPutAttemptCount();
      //estimateEventSize計算當前Event body大小
      int eventByteSize = (int) Math.ceil(estimateEventSize(event) / byteCapacitySlotSize);
    
      /*
       * offer若立即可行且不違反容量限制,則將指定的元素插入putList阻塞雙端佇列中(隊尾),
       * 並在成功時返回,如果當前沒有空間可用,則拋異常回滾事務 
       * */
      if (!putList.offer(event)) {
        throw new ChannelException(
            "Put queue for MemoryTransaction of capacity " +
            putList.size() + " full, consider committing more frequently, " +
            "increasing capacity or increasing thread count");
      }
 
      //記錄Event的byte值
      putByteCounter += eventByteSize;
}

具體如下圖,我們暫時忽略commit與rollback:

+----------+
|  Source  |    +---------------------------+
+-----+----+    | [MemoryChannel]           |
      |         |   +---------------------+ |
      |         |   | [MemoryTransaction] | |
      |         |   |                     | |
      |         |   |                     | |
      |         |   |    channelCounter   | |
      |         |   |                     | |
      |         |   |    putByteCounter   | |
      |         |   |                     | |
      |         |   |    +-----------+    | |
      +----------------> |  putList  |    | |
      doPut     |   |    +-----------+    | |
                |   +---------------------+ |
                +---------------------------+

4.2 take事務

此事務發生在Channel到Sink之間,主要是從Channel中取出event放入Sink中,具體包括。

  • doTake:將資料取到臨時緩衝區 takeList,並將資料傳送到 HDFS;
  • doCommit:如果資料全部傳送成功,則清除臨時緩衝區 takeList;
  • doRollback:資料傳送過程中如果出現異常,rollback 將臨時緩衝區 takeList 中的資料歸還給 channel 記憶體佇列;

如下呼叫:

transaction = channel.getTransaction();
transaction.begin();
 
......
  
event = channel.take();
 
......
  
transaction.commit();

邏輯如下:

  • 判斷takeList中是否還有空間,如果沒有空間則丟擲異常;
  • 判斷當前MemoryChannel中的queue中是否還有空間,這裡通過訊號量來判斷;
  • 從queue頭部彈出一條訊息,放入takeList中;
  • 估算這條Event所佔空間(slot數),累加takeList中的位元組數;
  • 將取出來的這條Event返回;

doTake具體程式碼如下:

protected Event doTake() throws InterruptedException {
        
      channelCounter.incrementEventTakeAttemptCount();//將正在從channel中取出的event計數器原子的加一,即增加取出事件計數器
  
 		 //如果takeList佇列沒有剩餘容量,即當前事務已經消費了最大容量的Event,拋異常
      if (takeList.remainingCapacity() == 0) {//takeList佇列剩餘容量為0
        throw new ChannelException("Take list for MemoryTransaction, capacity " +
            takeList.size() + " full, consider committing more frequently, " +
            "increasing capacity, or increasing thread count");
      }
      
     //嘗試獲取一個訊號量獲取許可,如果可以獲取到許可的話,證明queue佇列有空間,超時直接返回null
      if (!queueStored.tryAcquire(keepAlive, TimeUnit.SECONDS)) {
        return null;
      }
 
      Event event;
      synchronized (queueLock) {
        event = queue.poll();   //獲取並移除MemoryChannel雙端佇列表示的佇列的頭部(也就是佇列的第一個元素),佇列為空返回null,同一時間只能有一個執行緒訪問,加鎖同步
      }
      
 			//因為訊號量的保證,Channel Queue不應該返回null,出現了就不正常了
      Preconditions.checkNotNull(event, "Queue.poll returned NULL despite semaphore " +
          "signalling existence of entry");

      takeList.put(event);  //將取出的event暫存到事務的takeList佇列
 
  		//計算當前Event body大小並增加取出佇列位元組數計數器
   		/* 計算event的byte大小 */
      int eventByteSize = (int) Math.ceil(estimateEventSize(event) / byteCapacitySlotSize);
			//更新takeByteCounter大小 
      takeByteCounter += eventByteSize;

      return event;
}

於是我們把take事務加入,我們暫時忽略commit與rollback。具體如下圖,目前兩個事務是沒有聯絡的:

+----------+                                                                    +-------+
|  Source  |    +---------------------------------------------------------+     | Sink  |
+-----+----+    | [MemoryChannel]                                         |     +---+---+
      |         |   +--------------------------------------------------+  |         ^
      |         |   | [MemoryTransaction]                              |  |         |
      |         |   |                                                  |  |         |
      |         |   |                                                  |  |         |
      |         |   |    channelCounter                                |  |         |
      |         |   |                                                  |  |         |
      |         |   |    putByteCounter               takeByteCounter  |  |         |
      |         |   |                                                  |  |         |
      |         |   |    +-----------+                +------------+   |  | doTake  |
      +----------------> |  putList  |                |  takeList  +----------------+
      doPut     |   |    +-----------+                +------+-----+   |  |
                |   |                                        ^         |  |
                |   |                                        |         |  |
                |   +--------------------------------------------------+  |
                |                                            |            |
                |                                            |            |
                |                                            |            |
                |                      +---------+  poll     |            |
                |                      |  queue  | +---------+            |
                |                      +---------+                        |
                +---------------------------------------------------------+

4.3 提交事務

commit階段主要做的事情是提交事務,此程式碼繁雜在於其包括了兩個方面的操作:

  • 從putList拿資料到Queue;
  • 處理 takelist後續操作,就是根據此時具體情況調整各種數值;

commit其邏輯如下:

  • 計算takeList中Event數與putList中的Event差值;int remainingChange = takeList.size() - putList.size();
  • 差值小於0,說明takeList小,也就是向該MemoryChannel放的資料比取的資料要多,所以需要判斷該MemoryChannel是否有空間來放;
    • 首先通過訊號量來判斷是否還有剩餘空間;這一步tryAcquire方法會將bytesRemaining的值減去putByteCounter的值,如果bytesRemaining原來的值大於putByteCounter則返回true;
    • 然後判斷,在給定的keepAlive時間內,能否獲取到充足的queue空間;
  • 如果上面的兩個判斷都過了,那麼把putList中的Event放到該MemoryChannel中的queue中;
    • 將putList中的Event迴圈放入queue中;
    • 面的工作完成後,清空putList和takeList,一次事務完成;
  • 然後將兩個計數器置零;
  • 將queueStored的值加上puts的值,更新訊號量;
  • 如果takeList比putList大,說明該MemoryChannel中queue的數量應該是減少了,所以把(takeList-putList)的差值加到訊號量queueRemaining;
  • 更新channelCounter中的三個變數;

具體如下:

protected void doCommit() throws InterruptedException {    

  		//計算改變的Event數量,即取出數量-放入數量;如果放入的多,那麼改變的Event數量將是負數
    	//如果takeList更小,說明該MemoryChannel放的資料比取的資料要多,所以需要判斷該MemoryChannel是否有空間來放
      int remainingChange = takeList.size() - putList.size();  //takeList.size()可以看成source,putList.size()看成sink

 			//如果remainingChange小於0,則需要獲取Channel Queue剩餘容量的訊號量
      if (remainingChange < 0) { //sink的消費速度慢於source的產生速度
   			//利用bytesRemaining訊號量判斷是否有足夠空間接收putList中的events所佔的空間    
				//putByteCounter是需要推到channel中的資料大小,bytesRemainingchannel是容量剩餘
				//獲取putByteCounter個位元組容量訊號量,如果失敗說明超過位元組容量限制了,回滾事務
        if (!bytesRemaining.tryAcquire(putByteCounter, keepAlive, TimeUnit.SECONDS)) {
         	//channel 資料大小容量不足,事物不能提交
          throw new ChannelException("Cannot commit transaction. Byte capacity " +
              "allocated to store event body " + byteCapacity * byteCapacitySlotSize +
              "reached. Please increase heap space/byte capacity allocated to " +
              "the channel as the sinks may not be keeping up with the sources");
        }

    		//獲取Channel Queue的-remainingChange個訊號量用於放入-remainingChange個Event,如果獲取不到,則釋放putByteCounter個位元組容量訊號量,並丟擲異常回滾事務
        //因為source速度快於sink速度,需判斷queue是否還有空間接收event
        if (!queueRemaining.tryAcquire(-remainingChange, keepAlive, TimeUnit.SECONDS)) {
       	 //remainingChange如果是負數的話,說明source的生產速度,大於sink的消費速度,且這個速度大於channel所能承載的值
          bytesRemaining.release(putByteCounter);
          throw new ChannelFullException("Space for commit to queue couldn't be acquired." +
              " Sinks are likely not keeping up with sources, or the buffer size is too tight");
        }
      }
     
      int puts = putList.size(); //事務期間生產的event
      int takes = takeList.size();  //事務期間等待消費的event
  
   		//如果上述兩個訊號量都有空間的話,那麼把putList中的Event放到該MemoryChannel中的queue中。
     	//鎖住佇列開始,進行資料的流轉
      synchronized (queueLock) {//操作Channel Queue時一定要鎖定queueLock     
        if (puts > 0) {
          while (!putList.isEmpty()) { //如果有Event,則迴圈放入Channel Queue
            if (!queue.offer(putList.removeFirst())) {  
          		//如果放入Channel Queue失敗了,說明訊號量控制出問題了,這種情況不應該發生  
              throw new RuntimeException("Queue add failed, this shouldn't be able to happen");
            }
          }
        }
        //以上步驟執行成功,清空事務的putList和takeList
        putList.clear();  
        takeList.clear();
      }
  
    	//更新queue大小控制的訊號量bytesRemaining
		  //釋放takeByteCounter個位元組容量訊號量
		  bytesRemaining.release(takeByteCounter);
		  //重置位元組計數器
		  takeByteCounter = 0;
		  putByteCounter = 0;

 			//釋放puts個queueStored訊號量,這樣doTake方法就可以獲取資料了
      queueStored.release(puts);  //從queueStored釋放puts個訊號量

      //釋放remainingChange個queueRemaining訊號量    
      if (remainingChange > 0) {
        queueRemaining.release(remainingChange);
      }
      
  		//ChannelCounter一些資料計數
      if (puts > 0) { //更新成功放入Channel中的events監控指標資料
        channelCounter.addToEventPutSuccessCount(puts);
      }
      if (takes > 0) {  //更新成功從Channel中取出的events的數量
        channelCounter.addToEventTakeSuccessCount(takes);
      }

      channelCounter.setChannelSize(queue.size());
}

此處涉及到兩個訊號量:

queueStored表示Channel Queue已儲存事件容量(已儲存的事件數量),佇列取出事件時-1,放入事件成功時+N,取出失敗時-N,即Channel Queue儲存了多少事件。

  • queueStored訊號量預設為0。
  • 當doTake取出Event時減少一個queueStored訊號量。
  • 當doCommit提交事務時需要增加putList 佇列大小的queueStored訊號量。
  • 當doRollback回滾事務時需要減少takeList佇列大小的queueStored訊號量。

queueRemaining表示Channel Queue可儲存事件容量(可儲存的事件數量),取出事件成功時+N,放入事件成功時-N。

  • queueRemaining訊號量預設為Channel Queue容量。其在提交事務時首先通過remainingChange = takeList.size() - putList.size()計算獲得需要增加多少變更事件;
  • 如果小於0表示放入的事件比取出的多,表示有 remainingChange個事件放入,此時應該減少queueRemaining訊號量;
  • 而如果大於0,則表示取出的事件比放入的多,表示有queueRemaining個事件取出,此時應該增加queueRemaining訊號量;即消費事件時減少訊號量,生產事件時增加訊號量。

bytesRemaining是位元組容量訊號量,超出容量則回滾事務。

具體如下圖,現在整體業務已經走通:

+----------+                                                                          +-------+
|  Source  |    +---------------------------------------------------------------+     | Sink  |
+-----+----+    | [MemoryChannel]                                               |     +---+---+
      |         |   +--------------------------------------------------------+  |         ^
      |         |   | [MemoryTransaction]                                    |  |         |
      |         |   |                                                        |  |         |
      |         |   |                                                        |  |         |
      |         |   |    channelCounter                                      |  |         |
      |         |   |                                                        |  |         |
      |         |   |    putByteCounter                     takeByteCounter  |  |         |
      |         |   |                                                        |  |         |
      |         |   |    +-----------+                      +------------+   |  | doTake  |
      +----------------> |  putList  |                      |  takeList  +----------------+
      doPut     |   |    +----+------+                      +------+-----+   |  |
                |   |         |                                    ^         |  |
                |   |         |                                    |         |  |
                |   +--------------------------------------------------------+  |
                |             |                                    | poll       |
                |             |                                    |            |
                |             |                                    |            |
                |             |  doCommit    +---------+  doCommit |            |
                |             +------------> |  queue  | +---------+            |
                |                            +---------+                        |
                +---------------------------------------------------------------+

手機如下圖:

img

4.4 回滾事務

當一個事務失敗時,會進行回滾,即呼叫本方法。在回滾時,需要把takeList中暫存的事件回滾到Channel Queue,並回滾queueStored訊號量。具體邏輯如下:

  • 得到takeList中的Event數量 int takes = takeList.size();
  • 首先把takeList中的Event放回到MemoryChannel中的queue中;
    • 先判斷queue中能否有足夠的空間將takeList的Events放回去;
    • 從takeList的尾部依次取出Event,放入queue的頭部;
    • 然後清空putList;
  • 因為清空了putList,所以需要把putList所佔用的空間大小新增到bytesRemaining中;

具體程式碼如下:

protected void doRollback() {
    	//獲取takeList的大小,然後bytesRemaining中釋放
      int takes = takeList.size();
    	//將takeList中的Event重新放回到queue佇列中。
      synchronized (queueLock)  { //操作Channel Queue時一定鎖住queueLock
      	//前置條件判斷,檢查是否有足夠容量回滾事務
        Preconditions.checkState(queue.remainingCapacity() >= takeList.size(),
            "Not enough space in memory channel " +
            "queue to rollback takes. This should never happen, please report");
    		//回滾事務的takeList佇列到Channel Queue
        while (!takeList.isEmpty()) {  //takeList不為空,將其events全部放回queue
          //removeLast()獲取並移除此雙端佇列的最後一個元素
          queue.addFirst(takeList.removeLast());
        }
      	//最後清空putList
    	   putList.clear();
      }
      
		//清空了putList,所以需要把putList佔用的空間新增到bytesRemaining中
    //即,釋放putByteCounter個bytesRemaining訊號量
    bytesRemaining.release(putByteCounter);
    //計數器重置
    putByteCounter = 0;
    takeByteCounter = 0;
    //釋放takeList佇列大小個已儲存事件容量
    queueStored.release(takes);
    channelCounter.setChannelSize(queue.size());
}

具體如下圖:

+----------+                                                                          +-------+
|  Source  |    +----------------------------------------------------------------+    | Sink  |
+-----+----+    | [MemoryChannel]                                                |    +---+---+
      |         |   +--------------------------------------------------------+   |        ^
      |         |   | [MemoryTransaction]                                    |   |        |
      |         |   |                                                        |   |        |
      |         |   |                                                        |   |        |
      |         |   |    channelCounter                                      |   |        |
      |         |   |                                                        |   |        |
      |         |   |    putByteCounter                     takeByteCounter  |   |        |
      |         |   |                                                        |   |        |
      |         |   |    +-----------+                      +------------+   |   |doTake  |
      +----------------> |  putList  |                      |  takeList  +----------------+
      doPut     |   |    +----+--+---+                      +----+---+---+   |   |
                |   |         |  ^                               |   ^       |   |
                |   |         |  |                               |   |       |   |
                |   +--------------------------------------------------------+   |
                |             |  |                               |   | poll      |
                |             |  |                               |   |           |
                |             |  |  rollback         rollback    |   |           |
                |             |  +--------------+  +-------------+   |           |
                |             |                 |  |                 |           |
                |             |                 |  v                 |           |
                |             |  doCommit    +--+--+---+  doCommit   |           |
                |             +------------> |  queue  | +-----------+           |
                |                            +---------+                         |
                +----------------------------------------------------------------+

手機上如圖:

img

0x05 動態擴容

此小節回答瞭如下問題:

  • 可升級的,易管理,可定製的;

MemoryChannel 中使用鎖配合訊號實現動態增減容量

MemoryChannel會通過configure方法獲取配置檔案系統,初始化MemoryChannel,其中對於配置資訊的讀取有兩種方法,只在啟動時讀取一次或者動態的載入配置檔案,動態讀取配置檔案時若修改了Channel 的容量大小,則會呼叫 resizeQueue 方法進行調整,如下:

 if (queue != null) { //queue不為null,則為動態修改配置檔案時,重新指定了capacity
      try {
        resizeQueue(capacity);
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
      }
    } else { //初始化queue,根據指定的capacity申請雙向阻塞佇列,並初始化訊號量
      synchronized (queueLock) {
        queue = new LinkedBlockingDeque<Event>(capacity);
        queueRemaining = new Semaphore(capacity);
        queueStored = new Semaphore(0);
      }
    }

動態調整 Channel 容量主要分為三種情況:

  • 新老容量相同,則直接返回;

  • 老容量大於新容量,縮容,需先給未被佔用的空間加鎖,防止在縮容時有執行緒再往其寫資料,然後建立新容量的佇列,將原本佇列加入中所有的 event 新增至新佇列中;

  • 老容量小於新容量,擴容,然後建立新容量的佇列,將原本佇列加入中所有的 event 新增至新佇列中。

具體程式碼如下:

  private void resizeQueue(int capacity) throws InterruptedException {
    int oldCapacity;
//首先計算擴容前的Channel Queue的容量
    //計算原本的Channel Queue的容量
    synchronized (queueLock) {
      //老的容量=佇列現有餘額+在事務被處理了但是是未被提交的容量
      oldCapacity = queue.size() + queue.remainingCapacity();
    }

    //新容量和老容量相等,不需要調整返回
    if (oldCapacity == capacity) {//如果老容量大於新容量,縮容
      return;
    } else if (oldCapacity > capacity) {
      //縮容
    //首先要預佔老容量-新容量的大小,以便縮容容量
     //首先要預佔用未被佔用的容量,防止其他執行緒進行操作
     //嘗試佔用即將縮減的空間,以防被他人佔用
      if (!queueRemaining.tryAcquire(oldCapacity - capacity, keepAlive, TimeUnit.SECONDS)) {

   //如果獲取失敗,預設是記錄日誌然後忽略
        LOGGER.warn("Couldn't acquire permits to downsize the queue, resizing has been aborted");
      } else {
        //直接縮容量
        //鎖定queueLock進行縮容,先建立新capacity的雙端阻塞佇列,然後複製老Queue資料。執行緒安全
  //否則,直接縮容,然後複製老Queue的資料,縮容時需要鎖定queueLock,因為這一系列操作要執行緒安全

        synchronized (queueLock) {
          LinkedBlockingDeque<Event> newQueue = new LinkedBlockingDeque<Event>(capacity);
          newQueue.addAll(queue);
          queue = newQueue;
        }
      }
    } else { //擴容,加鎖,建立新newQueue,複製老queue資料
     //擴容
      synchronized (queueLock) {
        LinkedBlockingDeque<Event> newQueue = new LinkedBlockingDeque<Event>(capacity);
        newQueue.addAll(queue);
        queue = newQueue;
      }
//增加/減少Channel Queue的新的容量

      //釋放capacity - oldCapacity個許可,即就是增加這麼多可用許可
      queueRemaining.release(capacity - oldCapacity);
    }
  }

0x06 丟失資料的可能

回到本文最初的錯誤資訊:Space for commit to queue couldn't be acquired

這說明Flume是會出現資料相關問題的。我們首先分析此問題。

6.1 錯誤

6.1.1 異常原因

因為“source往putList放資料,然後提交到queue中”與“sink從channel中取資料到sink和takeList,然後再從putList取資料到queue中”這兩部分是分開來,任他們自由搶鎖,所以,當前者多次搶到鎖,後者沒有搶到鎖,同時queue的大小又太小,撐不住多次往裡放資料,就會導致觸發這個異常。

6.1.2 失敗處理

正常情況下,如果遇到此問題,flume會暫停source向channel放資料,等待幾秒鐘,這期間sink應該會消費channel中的資料,當source再次開始想channel放資料時channel就有足夠的空間了。

但是如果一直出現異常,就需要啟用解決方案。

6.1.3 解決方案

解決這個問題最直接的辦法就是增大queue的大小,增大capacity和transacCapacity之間的差距,queue能撐住多次往裡面放資料即可。

6.2 丟失資料的可能

下面我們看看Flume使用中,丟失資料的可能。

6.2.1 事務保證

根據Flume的架構原理,採用FileChannel的Flume是不可能丟失資料的,因為其內部有完善的事務機制(ACID)。

  • Source到Channel是事務性的,
  • Channel到Sink也是事務性的,

這兩個環節都不可能丟失資料。

6.2.2 管道容量

一旦管道中所有Flume Agent的容量之和被使用完,Flume 將不再接受來自客戶端的資料。此時,客戶端需要緩衝資料,否則資料可能會丟失。因此,配置管道能夠處理最大預期的停機時間是非常重要的。

6.2.3 MemoryChannel

Channel採用MemoryChannel時候,會出現丟失。

  • MemoryChannel受記憶體空間的影響,如果資料產生的過快,同時獲取訊號量超時容易造成資料的丟失。此時Source不再寫入資料,造成未寫入的資料丟失;就是本文的情況;
  • Flume程式掛掉,資料也會丟失,因為之前資料在記憶體中;

所以如果想要不丟失資料,需要採用File channel。

Memory Channel 是一個記憶體緩衝區,因此如果Java23 虛擬機器(JVM)或機器重新啟動,任何緩衝區中的資料將丟失。另一方面,File Channel是在磁碟上的。即使JVM 或機器重新啟動,File Channel 也不丟失資料,只要磁碟上儲存的資料仍然是起作用的和可訪問的。機器和Agent 一旦開始執行,任何儲存在FileChannel 中的資料將最終被訪問。

6.2.4 資料重複

在Channel傳送到Sink這階段,容易出現資料重複問題。

比如:如果flush到HDFS的時候,資料flush了一半之後出問題了,這意味著已經有一半的資料已經傳送到HDFS上面了,現在出了問題,同樣需要呼叫doRollback方法來進行回滾。

回滾並沒有“一半”之說,它只會把整個takeList中的資料返回給channel,然後繼續進行資料的讀寫。這樣開啟下一個事務的時候就容易造成資料重複的問題。

所以,在某種程度上,flume對資料進行採集傳輸的時候,它有可能會造成資料的重複,但是其資料不丟失

Flume 保證事件至少一次被送到它們的目的地,只有一次傾力寫資料,且不存在任何型別的故障事件只被寫一次。但是像網路超時或部分寫入儲存系統的錯誤,可能導致事件不止被寫一次,因為Flume 將重試寫操作直到它們完全成功。網路超時可能表示寫操作的失敗,或者只是機器執行緩慢。如果是機器執行緩慢,當Flume 重試這將導致重複。因此,確保每個事件都有某種形式的唯一識別符號通常是一個好主意,如果需要,最終可以用來刪除事件資料。

0xFF 參考

基於Flume的美團日誌收集系統(一)架構和設計

基於Flume的美團日誌收集系統(二)改進和優化

事件序列化器 Flume 的無資料丟失保證,Channel 和事務

flume MemoryChannel分析

Flume 1.7 原始碼分析(一)原始碼編譯
Flume 1.7 原始碼分析(二)整體架構
Flume 1.7 原始碼分析(三)程式入口
Flume 1.7 原始碼分析(四)從Source寫資料到Channel
Flume 1.7 原始碼分析(五)從Channel獲取資料寫入Sink

Flume - MemoryChannel原始碼解析

flume到底會丟資料嗎?其可靠性如何?——輕鬆搞懂Flume事務機制

Flume會不會丟失資料?

flume MemoryChannel分析

Flume架構與原始碼分析-MemoryChannel事務實現

flume“Space for commit to queue couldn't be acquired”異常產生分析

原始碼趣事-flume-佇列動態擴容及容量使用

併發性標註 @GuardedBy @NotThreadSafe @ThreadSafe

秒懂,Java 註解 (Annotation)你可以這樣學

Flume之MemoryChannel原始碼解讀

Flume MemoryChannel原始碼分析

搞懂分散式技術17,18:分散式事務總結

相關文章