Flink執行時之結果分割槽消費端

weixin_34208283發表於2018-10-16
5501600-d91a77e3f979e3a8.png

結果分割槽消費端

在前一篇,我們講解了生產者分割槽,生產者分割槽是生產者任務生產中間結果資料的過程。消費者任務在獲得結果分割槽可用的通知之後,會發起對資料的請求。我們仍然以生產者分割槽的例子作為假設,其在消費端示意圖如下:


5501600-9506f175c7f2e242.png

可以看到在生產端和消費端存在對等的模型,具體ResultSubpartition中的資料如何被消費,我們將在本篇進行深入剖析。

輸入閘道器

輸入閘道器(InputGate)用於消費中間結果(IntermediateResult)在並行執行時由子任務生產的一個或多個結果分割槽(ResultPartition)。

可以認為生產端的ResultPartition跟消費端的InputGate是對等的。

Flink當前提供了兩個輸入閘道器的實現,分別是:

SingleInputGate:常規輸入閘道器;
UnionInputGate:聯合輸入閘道器,它允許將多個輸入閘道器聯合起來;
我們主要分析SingleInputGate,因為它是消費ResultPartition的實體,而UnionInputGate主要充當InputGate容器的角色。

SingleInputGate主要的初始化邏輯被封裝在其靜態的create方法中,當一個Task被例項化時在其構造器中會呼叫該create方法初始化它對應的SingleInputGate例項。create方法根據傳遞進來的InputGateDeploymentDescriptor完成對其包含的所有InputChannel的例項化。因為InputChannel記錄是按照跟生產端任務的位置來分類的(我們會在下面進行具體分析),所以其例項化也是按照InputGateDeploymentDescriptor中每個InputChannelDeploymentDescriptor包含的ResultPartitionLocation屬性來初始化的。

不同的ResultPartitionLocation,消費端任務其請求結果子分割槽的方式也不同,這一點我們在講解生產者分割槽是有所說明。

作為資料的消費者,InputGate最關鍵的方法自然是獲取生產者所生產的緩衝區,提供該功能的方法為getNextBufferOrEvent,它返回的物件是我們之前談到的統一的資料交換物件BufferOrEvent。

BufferOrEvent的直接消費物件是通訊層API中的記錄讀取器,它會將Buffer中的資料反序列化為記錄供上層任務使用。

我們以getNextBufferOrEvent方法為主線來分析SingleInputGate類。

public BufferOrEvent getNextBufferOrEvent() throws IOException, InterruptedException {
    //如果已接收到所有EndOfPartitionEvent事件,則說明每個ResultSubpartition中的資料都被消費完成
    if (hasReceivedAllEndOfPartitionEvents) {
        return null;
    }

    //觸發所有的輸入通道向ResultSubpartition發起請求
    requestPartitions();

    InputChannel currentChannel = null;
    //阻塞並迴圈等待有可獲取資料的通道可用
    while (currentChannel == null) {
        if (isReleased) {
            throw new IllegalStateException("Released");
        }

        //從阻塞佇列中請求(並刪除)隊首的輸入通道,阻塞兩秒鐘,如果沒有獲取到則不斷請求,直到獲取到一個輸入通道位置
        currentChannel = inputChannelsWithData.poll(2, TimeUnit.SECONDS);
    }

    //從輸入通道中獲得下一個Buffer
    final Buffer buffer = currentChannel.getNextBuffer();

    if (buffer == null) {
        throw new IllegalStateException("Bug in input gate/channel logic: input gate got " +
            "notified by channel about available data, but none was available.");
    }

    //如果該Buffer是使用者資料,則構建BufferOrEvent物件並返回
    if (buffer.isBuffer()) {
        return new BufferOrEvent(buffer, currentChannel.getChannelIndex());
    }
    //否則把它當作事件來處理
    else {
        final AbstractEvent event = EventSerializer.fromBuffer(buffer, getClass().getClassLoader());

        //如果獲取到的是標識某ResultSubpartition已經生產完資料的事件
        if (event.getClass() == EndOfPartitionEvent.class) {
            //對獲取該ResultSubpartition的通道進行標記
            channelsWithEndOfPartitionEvents.set(currentChannel.getChannelIndex());
            //如果所有通道都被標記了,置全部通道獲取資料完成
            if (channelsWithEndOfPartitionEvents.cardinality() == numberOfInputChannels) {
                hasReceivedAllEndOfPartitionEvents = true;
            }
            //對外發出ResultSubpartition已被消費的通知同時釋放資源
            currentChannel.notifySubpartitionConsumed();
            currentChannel.releaseAllResources();
        }
        //以事件來構建BufferOrEvent物件
        return new BufferOrEvent(event, currentChannel.getChannelIndex());
    }
}

以上程式碼段中,第一個關鍵呼叫是requestPartitions方法。它會觸發所有InputChannel發起對requestSubpartition方法的呼叫以請求生產端的ResultSubpartition。

有一種佔位目的的UnknowInputChannel不響應該方法,因為它最終會被確定為是LocalInputChannel還是RemoteInputChannel,確定的時機通常是JobManager通知器其可消費,TaskManager呼叫當前SingleInputGate的updateInputChannel方法,確定UnknowInputChannel會轉變的具體的通道型別後再呼叫requestSubpartition方法。

由於requestPartitions只是起到觸發其內部的InputChannel去請求的作用,這個呼叫可能並不會阻塞等待遠端資料被返回。因為不同的InputChannel其請求的機制並不相同,RemoteChannel就是利用Netty非同步請求的。所以SingleInputGate採用阻塞等待以及事件回撥的方式來等待InputChannel上的資料可用。具體而言,它在while程式碼塊中迴圈阻塞等待有可獲取資料的InputChannel。而可用的InputChannel則由它們自己通過回撥SingleInputGate的onAvailableBuffer新增到阻塞佇列inputChannelsWithData中來。當有可獲取資料的InputChannel之後,即可獲取到Buffer。

Flink除了提供了SingleInputGate這種常規的輸入閘道器之外,還提供了UnionInputGate,它更像一個包含SingleInputGate的容器,同時可以這些SingleInputGate擁有的InputChannel聯合起來。並且多數InputGate約定的介面方法的實現,都被委託給了每個SingleInputGate。

那麼它在實現getNextBufferOrEvent方法的時候,到底從哪個InputGate來獲得緩衝區呢。它採用的是事件通知機制,所有加入UnionInputGate的InputGate都會將自己註冊到InputGateListener。當某個InputGate上有資料可獲取,該InputGate將會被加入一個阻塞佇列。接著我們再來看getNextBufferOrEvent方法的實現:

public BufferOrEvent getNextBufferOrEvent() throws IOException, InterruptedException {
    if (inputGatesWithRemainingData.isEmpty()) {
        return null;
    }

    //遍歷每個InputGate,依次呼叫其requestPartitions方法
    requestPartitions();

    //阻塞等待輸入閘道器佇列中有可獲取資料的輸入閘道器
    final InputGate inputGate = inputGateListener.getNextInputGateToReadFrom();

    //從輸入閘道器中獲得資料
    final BufferOrEvent bufferOrEvent = inputGate.getNextBufferOrEvent();

    //如果獲取到的是事件且該事件為EndOfPartitionEvent且輸入閘道器已完成
    if (bufferOrEvent.isEvent()
        && bufferOrEvent.getEvent().getClass() == EndOfPartitionEvent.class
        && inputGate.isFinished()) {
        //嘗試將該輸入閘道器從仍然可消費資料的輸入閘道器集合中刪除
        if (!inputGatesWithRemainingData.remove(inputGate)) {
            throw new IllegalStateException("Couldn't find input gate in set of remaining " +
                "input gates.");
        }
    }

    //獲得通道索引偏移
    final int channelIndexOffset = inputGateToIndexOffsetMap.get(inputGate);
    //計算真實通道索引
    bufferOrEvent.setChannelIndex(channelIndexOffset + bufferOrEvent.getChannelIndex());

    return bufferOrEvent;
}

基本上這個機制跟SingleInputGate是一致的,只不過在UnionInputGate中它是從InputGate中而非從InputChannel中罷了。

輸入通道

一個InputGate包含多個輸入通道(InputChannel),輸入通道用於請求ResultSubpartitionView,並從中消費資料。

所謂的ResultSubpartitionView是由ResultSubpartition所建立的用於供消費者任務消費資料的檢視物件。

對於每個InputChannel,消費的生命週期會經歷如下的方法呼叫過程:

requestSubpartition:請求ResultSubpartition;
getNextBuffer:獲得下一個Buffer;
releaseAllResources:釋放所有的相關資源;
InputChannel根據ResultPartitionLocation提供了三種實現:

LocalInputChannel:用於請求同例項中生產者任務所生產的ResultSubpartitionView的輸入通道;
RemoteInputChannel:用於請求遠端生產者任務所生產的ResultSubpartitionView的輸入通道;
UnknownInputChannel:一種用於佔位目的的輸入通道,需要佔位通道是因為暫未確定相對於生產者任務位置,但最終要麼被替換為RemoteInputChannel,要麼被替換為LocalInputChannel。
LocalInputChannel會從相同的JVM例項中消費生產者任務所生產的Buffer。因此,這種模式是直接藉助於方法呼叫和物件共享的機制完成消費,無需跨節點網路通訊。具體而言,它是通過ResultPartitionManager來直接建立對應的ResultSubpartitionView的例項,這種通道相對簡單。

RemoteInputChannel是我們重點關注的輸入通道,因為它涉及到遠端請求結果子分割槽。遠端資料交換的通訊機制建立在Netty框架的基礎之上,因此會有一個主互動物件PartitionRequestClient來銜接通訊層跟輸入通道。

我們以請求子分割槽的requestSubpartition為入口來進行分析。首先,通過一個ConnectionManager根據連線編號(對應著目的主機)來建立PartitionRequestClient例項。接著具體的請求工作被委託給PartitionRequestClient的例項:

partitionRequestClient.requestSubpartition(partitionId, subpartitionIndex, this, 0);

因為Netty以非同步的方式處理請求。因此,上面的程式碼段中會看到將代表當前RemoteChannel例項的this物件作為引數注入到Netty的特定的ChannelHandler中去,在處理時根據特定的處理邏輯會觸發RemoteChannel中相應的回撥方法。

在RemoteChannel中定義了多個“onXXX”回撥方法來銜接Netty的事件回撥。其中,較為關鍵的自然是接收到資料的onBuffer方法:

public void onBuffer(Buffer buffer, int sequenceNumber) {
    boolean success = false;

    try {
        synchronized (receivedBuffers) {
            if (!isReleased.get()) {
                //如果實際的序列號跟所期待的序列號相等
                if (expectedSequenceNumber == sequenceNumber) {
                    //將資料加入接收佇列同時將預期序列號計數器加一
                    receivedBuffers.add(buffer);
                    expectedSequenceNumber++;

                    //發出有可用Buffer的通知,該通知隨後會被傳遞給其所歸屬的SingleInputGate,
                    //以通知其訂閱者,有可用資料
                    notifyAvailableBuffer();

                    success = true;
                }
                else {
                    //如果實際序列號跟所期待的序列號不一致,則會觸發onError回撥,並相應以一個特定的異常物件
                    //該方法呼叫在成功設定完錯誤原因後,同樣會觸發notifyAvailableBuffer方法呼叫
                    onError(new BufferReorderingException(expectedSequenceNumber, sequenceNumber));
                }
            }
        }
    }
    finally {
        //如果不成功,則該Buffer會被回收
        if (!success) {
            buffer.recycle();
        }
    }
}

從程式碼段可以看出,消費時首先會進行序列號比對,這可以看作是一種“校驗”機制。服務端每響應客戶端一個Buffer都會將序列號加一併隨響應資料一起發回給客戶端,而客戶端則會在消費時也同時累加本地的序列號計數器。在消費的過程中,兩個序列號必須一致才能保證消費的順利進行,否則InputChannel將會丟擲IOException異常。

onBuffer方法的執行處於Netty的I/O執行緒上,但RemoteInputChannel中getNextBuffer卻不會在Netty的I/O執行緒上被呼叫,所以必須有一個資料共享的容器,這個容器就是receivedBuffers佇列。getNextBuffer就是直接從receivedBuffers佇列中出隊一條資料然後返回。

參考:
https://www.jianshu.com/p/8d8b3539b060

相關文章