Flink執行時之結果分割槽消費端
結果分割槽消費端
在前一篇,我們講解了生產者分割槽,生產者分割槽是生產者任務生產中間結果資料的過程。消費者任務在獲得結果分割槽可用的通知之後,會發起對資料的請求。我們仍然以生產者分割槽的例子作為假設,其在消費端示意圖如下:
可以看到在生產端和消費端存在對等的模型,具體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佇列中出隊一條資料然後返回。
相關文章
- 【kafka】-分割槽-消費端負載均衡Kafka負載
- 分割槽 執行計劃
- Oracle分割槽之五:建立分割槽索引總結Oracle索引
- Flink的分割槽策略
- 執行結果
- 對刪除分割槽的分割槽表執行TSPITR
- 對分割槽表的部分分割槽執行TSPITR
- Flink實戰:消費Wikipedia實時訊息
- java虛擬機器執行時記憶體分割槽Java虛擬機記憶體
- oracle分割槽表執行計劃Oracle
- java多執行緒之消費生產模型Java執行緒模型
- 多執行緒之生產者消費者執行緒
- flink連線消費kafkaKafka
- 實時數倉之Flink消費kafka訊息佇列資料入hbaseKafka佇列
- Kafka訊息分發、主題分割槽與消費組的概念Kafka
- celery筆記九之task執行結果檢視筆記
- MySql資料分割槽操作之新增分割槽操作MySql
- 分割槽表總結
- 表分割槽總結
- 分割槽表 總結
- Java多執行緒消費訊息Java執行緒
- kafka多執行緒順序消費Kafka執行緒
- Oracle分割槽表增加分割槽報錯“ORA-14760:不允許對間隔分割槽物件執行 ADD PARTITION”Oracle物件
- 分割槽表中的區域性分割槽索引及全域性索引與執行計劃索引
- 實時計算Flink>獨享模式>Batch(試用)>建立結果表——建立CSV結果表模式BAT
- 多執行緒的補充 獲取一定時間的執行結果執行緒
- 《RHEL6硬碟的分割槽和swap分割槽管理》——硬碟分割槽的大總結硬碟
- Hive中靜態分割槽和動態分割槽總結Hive
- 分割槽表、分割槽索引和全域性索引部分總結索引
- 多執行緒並行執行,然後彙總結果執行緒並行
- 分割槽表與堆表執行計劃的不同
- MySQL 5.5 檢視分割槽表的執行計劃MySql
- 分割槽索引(Partition Index)與SQL執行計劃(中)索引IndexSQL
- 分割槽索引(Partition Index)與SQL執行計劃(下)索引IndexSQL
- 分割槽索引(Partition Index)與SQL執行計劃(上)索引IndexSQL
- 使用多執行緒增加kafka消費能力執行緒Kafka
- oracle 分割槽表總結Oracle
- Oracle 分割槽表 總結Oracle