Kafka消費邏輯
簡介:本文主要敘述KafkaConsumer消費邏輯(本文使用的是flink 中的Kafka-client),
是如何獲取獲取數據,這裏直奔主題,從KafkaConsumer直接看起來。
1、喚醒 KafakaConsumerThread 線程消費
KafakaConsumerThread的 run 方法的邏輯主要在 KafakaConsumer#poll(long timeout) 裏面,我們每次向 Kafka broker發送請求的時候,通常指定時間內不管有沒有數據都會立即返回而不是一直等待知道有數據,這裏一直在循環到指定的超時時間爲止。參考代碼如下:
public ConsumerRecords<K, V> poll(long timeout) {
do {
//進行一次消費
Map<TopicPartition, List<ConsumerRecord<K, V>>> records = pollOnce(remaining);
if (!records.isEmpty()) {
//獲取到數據不爲空時,繼續新建一個請求(異步)。
if (fetcher.sendFetches() > 0 || client.pendingRequestCount() > 0)
client.pollNoWakeup(); //將請求發送出去
if (this.interceptors == null)
return new ConsumerRecords<>(records);
else
return this.interceptors.onConsume(new ConsumerRecords<>(records));
}
// 根據timeout 參數,計算剩餘可用的時間
long elapsed = time.milliseconds() - start;
remaining = timeout - elapsed;
} while (remaining > 0); //在超時時間到達前會一直循環的
從上面代碼可以看到,在沒有獲取到數據的時候同時超時時間到達之前,會反覆通過調用 pollonce 方法進行消息的拉取,當 pollonce 方法如果獲取到數據的時候,都會接着再新增一個FetchRequest請求,並且發送了這個請求,這是爲了下一次poll的時候不需要發送請求直接返回數據。
如果沒有數據而且超時時間沒到的時候,會一直調用pollonce方法進行一次消息的獲取。我們看下KafkaConsumer#pollonce這個方法邏輯。參考代碼如下:
private Map<TopicPartition, List<ConsumerRecord<K, V>>> pollOnce(long timeout) {
// 確認服務端的GroupCoordinator可用,以及當前consumer已加入消費者羣組
coordinator.poll(time.milliseconds());
//從內存中嘗試獲取數據
Map<TopicPartition, List<ConsumerRecord<K, V>>> records = fetcher.fetchedRecords();
if (!records.isEmpty())
return records;
// send any new fetches (won't resend pending fetches)
//沒有獲取到數據,構建請求,將請求對象放入unsent隊列。等待發送
fetcher.sendFetches();
//真正請求broker,獲取到的數據存入FetchResponse#responseData中
client.poll(pollTimeout, now, new PollCondition() {
@Override
public boolean shouldBlock() {
// since a fetch might be completed by the background thread, we need this poll condition
// to ensure that we do not block unnecessarily in poll()
return !fetcher.hasCompletedFetches();
}
});
//是否有新的consumer加入等需要rebalance
if (coordinator.needRejoin())
return Collections.emptyMap();
//再次從內存嘗試獲取數據
return fetcher.fetchedRecords();
}
上面說過,我們在KafakaConsumer#poll的時候如果獲取到了數據的時候會再發送一次請求,如果這個請求請求到了數據,會將這個數據存在內存中,下一次獲取的時候直接調用fetcher.fetchedRecords()方法從內存中獲取到數據後返回。
如果這個請求沒有返回數據,則再會新增一個請求 (fetcher.sendFetches()) 併發送請求(client.poll),然後再次調用fetcher.fetchedRecords()返回,形成一個循環流程~~。
下面我們分別看下fetcher.sendFetches()、client.poll以及 fetcher.fetchedRecords()這3個方法是如何運行的。
首先看下fetcher.sendFetches()方法,參考代碼如下:
public int sendFetches() {
//創建FetchRequests,多個分區首領可能在不同的節點。
Map<Node, FetchRequest.Builder> fetchRequestMap = createFetchRequests();
//根據節點循環,
for(){
//創建ClientRequest節點信息、分區信息等,存入unsent 隊列,並不是真正發送。
client.send()
//添加監聽信息,等待回調將消息等寫入Fetcher#LinkedQueue中。
.addListener()
}
}
將請求進行封裝丟進隊列中,並且對這個請求加個回調函數方便對請求結果的處理。下一步就是發送這個請求,
2、網絡請求
下面我們主要看下這個NetworkClient#poll方法:
public List<ClientResponse> poll(long timeout, long now) {
//元數據更新
long metadataTimeout = metadataUpdater.maybeUpdate(now);
try {
//網絡IO進行Select.select
this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs));
} catch (IOException e) {
log.error("Unexpected error during I/O", e);
}
// process completed actions
long updatedNow = this.time.milliseconds();
List<ClientResponse> responses = new ArrayList<>();
handleAbortedSends(responses);
//處理已完成的請求
handleCompletedSends(responses, updatedNow);
//處理請求響應的數據、回調那個添加的監聽
handleCompletedReceives(responses, updatedNow);
handleDisconnections(responses, updatedNow);
handleConnections();
handleInitiateApiVersionRequests(updatedNow);
handleTimedOutRequests(responses, updatedNow);
}
這個方法所做的事情就是更新元數據信息、網絡IO處理、後面幾個方法都是用來處理請求後的邏輯的。我們還是往下看selector.poll方法做的什麼事情,接着上代碼:Select#poll。
public void poll(long timeout) throws IOException {
//Select.select,獲取下個事件的類型,讀、寫、可連接等
int readyKeys = select(timeout);
long endSelect = time.nanoseconds();
this.sensors.selectTime.record(endSelect - startSelect, time.milliseconds());
// 根據不同的事件做對應的事情,參照SelectionKey描述,OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT
if (readyKeys > 0 || !immediatelyConnectedKeys.isEmpty()) {
pollSelectionKeys(this.nioSelector.selectedKeys(), false, endSelect);
pollSelectionKeys(immediatelyConnectedKeys, true, endSelect);
}
//如果有數據返回記錄到completedReceives中
addToCompletedReceives();
}
這裏先是獲取當前事件類型。然後根據事件的類型做相應的讀寫等操作。
private void pollSelectionKeys(Iterable<SelectionKey> selectionKeys,
boolean isImmediatelyConnected,
long currentTimeNanos) {
if (isImmediatelyConnected || key.isConnectable()) {
if (channel.finishConnect()) {
this.connected.add(channel.id());
this.sensors.connectionCreated.record();
SocketChannel socketChannel = (SocketChannel) key.channel();
log.debug("Created socket with SO_RCVBUF = {}, SO_SNDBUF = {}, SO_TIMEOUT = {} to node {}",
socketChannel.socket().getReceiveBufferSize(),
socketChannel.socket().getSendBufferSize(),
socketChannel.socket().getSoTimeout(),
channel.id());
} else
continue;
}
/* if channel is not ready finish prepare */
if (channel.isConnected() && !channel.ready())
channel.prepare();
/* if channel is ready read from any connections that have readable data */
if (channel.ready() && key.isReadable() && !hasStagedReceive(channel)) {
NetworkReceive networkReceive;
while ((networkReceive = channel.read()) != null)
addToStagedReceives(channel, networkReceive);
}
/* if channel is ready write to any sockets that have space in their buffer and for which we have data */
if (channel.ready() && key.isWritable()) {
Send send = channel.write();
if (send != null) {
this.completedSends.add(send);
this.sensors.recordBytesSent(channel.id(), send.size());
}
}
}
}
這個方法就是就是根據不同的key,針對不同的key類型做不同的邏輯處理,是真正將請求發送出去以及接受響應的方法。
具體NIO方面操作就不往下細扒了。我還是回頭看下當前請求節點上的channel有可讀的數據時,是如何將數據放入Handover 隊列裏面供下游使用,這裏我們看到上述channel.isReadable的時候調用了addToStagedReceives方法,這個方法會將buffer中的數據最終放入Selector# stagedReceives的內存中,而剛纔在NetworkClient#poll方法中有個handleCompletedReceives方法,這個方法最終會回調RequestFutureListener#onSuccess() 將消息存入Fetch中的completedFetches。最後被fetcher.fetchedRecords()調用獲取。