《kafka源碼刨析》讀書筆記
一,概念
1.offset
消費者如何確定自己消費到了分區的哪個位置?如果消費者宕機了,當下一個消費者來繼續處理這個分區的時候,如何繼續消費?都指向了消費位移的概念,也就是我們需要一個變量來保存消費位置。我們將這個變量稱爲 offset。
在舊版本中,消費者會把消費位置記錄到zookeeper中,新版本爲了緩解zookeeper的壓力,在kafka服務端中添加了一個名爲“_comsumer_offsets”的內部 topic,簡稱爲offset topic,它保存消費者提交的offset,使用offset topic記錄消費位置是默認選項。
這裏存在一個問題:消費者水麼時候提交offset值?
1.自動提交方式,當使用poll()拉取的時候,把上次poll()消費的offset提交給offset topic。這裏存在的問題:當消費者還沒有消費完poll()拉取的消息,但是宕機了,自然沒有提交offset,下一次繼續消費的時候,會存在重複消費!
2.當poll()拉取消息的時候,還沒有消費就把本次的offset提交了,存在的問題:避免了重複消費,但是如果還沒有消費就宕機了,這就存在了消息丟失。
所以,出現了傳遞保證,三個級別:
1.At most once:消息可能會丟,但絕對不會重複,對應上面的第二種方案
2.At least once:消息不會丟失,但是可能會重複,對應上面的第一種方案
3.Exactly once:每條消息只會被傳遞一次 (最好狀態)
很少有讓消息丟失的情況,我們大多數都希望達到第三種情況,但是保證第三種情況,不僅僅是消費者,生產者也要保證消息只會被成功發送一次保存在服務器分區中,如果分區中已經存在同樣的消息,自然無法避免重複消息(除非有一個判斷機制,比如消息唯一id)
生產者確保exactly once:
1.每個分區只有一個生產者寫入消息,當宕機,重啓之後,自己去確認最後一次消息發送到了哪裏,再決定是重發還是繼續
2.爲每個消息設置一個全局主鍵,生產者不做其他處理,可以直接重傳,消費者來進行去重。
消費者確保exactly once:
關閉offset自動提交,不再使用offset topic這個內部topic,而是消費者自己保存offset,利用事務的原子性,當poll()一次,必須完整消費完,才提交offset,如果失敗就回滾。將offset保存到數據庫中,
2.消費組與消費者
這裏和其他的MQ有不同的地方,多了一個消費組,消費者是放在消費組裏面的,分區是映射到消費者的,每個分區只能被一個消費組中的一個消費者所消費,如果要同一個分區要映射到多個消費者,這些消費者必然不是一個消費組的,但是一個消費者是可以任意映射分區,只要不重複。
當一個消費組內加入新的消費者的時候,前面的消費者會釋放一些分區出來,拿給新的消費者進行消費,而這裏的和redis的集羣裏面很像,消費者分開處理topic的分區。
每個不同的消費組中的消費者都去訂閱同一個分區,那麼就實現了廣播模式。當有消費者宕機,或者消費者加入消費組,都會觸發rebalance,在rebalance期間,消費者是不能消費的,直到rebalance完成,這個時候就需要使用第一小節中的offset重新開始消費。
問題:當消費組連接上服務器的時候,如何進行分區到消費者的映射分配呢?
方案1:kafka最開始使用zk的watcher實現,每個consumer group都在zk下維護一個/consumer/group_id/ids,記錄消費者的id,同級別下,還有offsets節點,記錄group在某個分區上的消費位置,owners記錄分區與消費者的映射關係(重點),同樣broker也在zk中有節點,保存了broker分區信息,leader信息,isr信息,如此涉及,zk負擔很重,而且zk存在腦裂問題,評價:不好!
方案2:kafka後續做出調整,將全部的consumer group 分成多個子集,每個consumer group 子集在服務端對應一個GroupCoordinator對其進行管理,消費者不再依賴zk,而只有GroupCoordinator在zk上面有watcher,這樣就大大的減小了zk的負擔,當有新的消費者加入,或者舊的宕機,就會改變zk的值,就會觸發GroupCoordinator綁定的watcher,GroupCoordinator就會進行rebalance。
簡述過程:
1.當消費者準備計入Consumer Group,或者GroupCoordinator發生故障轉移時,消費者並不知道GroupCoordinator網絡位置,消費者會向任意broker發送一個ConmuserMetadataRequest請求,裏面附帶了GroupId,然後broker收到返回response,裏面就待了groupId對應的CroupCoordinator信息。
2.消費者根據ConsumerMetadataResponse中的GroupCoordinator信息,連接到GroupCoordinator並週期性發送HeartbeatReqeust,心跳檢測,徵明消費者還活着,如果GroupCoordinator長期收不到,就默認爲消費者死了,會發起新一輪Rebalance。
3.如果HeartbeatResponse中帶有IllegalGeneration異常,說明GroupCoordinator發起了rebalance操作,此時消費者發送JoinGroupRequest給GroupCoordinator(通知GroupCoordinator,消費者要加入指定的GroupId)
4.GroupCoordinator分配完成之後,將分配結果寫入zookeeper,並通過JoinGroupResponse返回給消費者,消費者根據JoinGroupResponse結果進行消費數據。
5.consumer計入Group之後,週期發送heartbeanRequest,如果發現異常返回就發送JoinGroupRequest,循環操作。
方案3:將rebalance工作放到了消費者處理,Consumer Group管理以然在GroupCoordinator中,當consumer發現GroupCoordinator後,就進入joinGroup階段,發送JoinGroupRequest請求,服務端收齊所有消費者之後(這裏如何確定所有消費者呢?),會從消費者中選出一個Leader,把所有消費者信息發送給leader,讓它來進行分區分配,服務端只需要指定分配的方式就可以了,具體的操作交給leader來解決,leader把結果返回給GroupCoordinator,然後服務端再把結果 返回給Group中所有的消費者。
二,KafkaConuser分析
它不是一個線程安全的類,而kafkaProducer是線程安全的,先看一下重要字段:
public class KafkaConsumer<K, V> implements Consumer<K, V> {
private final Logger log;
private final String clientId;
private final ConsumerCoordinator coordinator; //分配策略管理
private final Deserializer<K> keyDeserializer;
private final Deserializer<V> valueDeserializer;
private final Fetcher<K, V> fetcher; //負責從服務端獲取消息
private final ConsumerInterceptors<K, V> interceptors;
private final Time time;
private final ConsumerNetworkClient client; //負責通信
private final SubscriptionState subscriptions; //訂閱指定的topic
private final Metadata metadata;
private final long retryBackoffMs;
private final long requestTimeoutMs;
private volatile boolean closed = false;
private List<PartitionAssignor> assignors;
...
}
2.1 ConsumerNetworkClient
它封裝了NetworkClient。 NetwordkClient 它依賴於Kselector,InFlightRequest,Metadata組件,負責管理客戶端與kafka集羣中各個Node間的連接,
public class ConsumerNetworkClient implements Closeable {
private final KafkaClient client; //NetworkClient對象
private final UnsentRequests unsent = new UnsentRequests(); //緩衝隊列
private final Metadata metadata; //用於管理kafka集羣元數據
private final Time time;
private final long retryBackoffMs;
private final long unsentExpiryMs; //緩衝的超時 時長
private final AtomicBoolean wakeupDisabled = new AtomicBoolean();
...
}
它的核心方法自然是poll()
public void poll(long timeout, long now, PollCondition pollCondition, boolean disableWakeup) {
firePendingCompletedRequests();
synchronized (this) {
//1. send all the requests we can send now
//使用client 進行send() request
trySend(now);
if (pollCondition == null || pollCondition.shouldBlock()) {
if (client.inFlightRequestCount() == 0)
timeout = Math.min(timeout, retryBackoffMs);
//3,
client.poll(Math.min(MAX_POLL_TIMEOUT_MS, timeout), now);
now = time.milliseconds();
} else {
client.poll(0, now);
}
//4.
checkDisconnects(now);
if (!disableWakeup) {
//5.
maybeTriggerWakeup();
}
// throw InterruptException if this thread is interrupted
maybeThrowInterruptException();
//6.
trySend(now);
//7.
failExpiredRequests(now);
unsent.clean();
}
firePendingCompletedRequests();
}
1.trySend(),發送clientRequest請求,unsent是ConsumerNetworkClient內部類
private boolean trySend(long now) {
// send any requests that can be sent now
boolean requestsSent = false;
for (Node node : unsent.nodes()) {
Iterator<ClientRequest> iterator = unsent.requestIterator(node);
while (iterator.hasNext()) {
ClientRequest request = iterator.next();
if (client.ready(node, now)) {
client.send(request, now);
iterator.remove();
requestsSent = true;
}
}
}
return requestsSent;
}
5.checkDisconnects()檢測連接狀態
把斷開的Node,從unsent中移除
private void checkDisconnects(long now) {
for (Node node : unsent.nodes()) {
if (client.connectionFailed(node)) {
// Remove entry before invoking request callback to avoid callbacks handling
// coordinator failures traversing the unsent list again.
Collection<ClientRequest> requests = unsent.remove(node);
for (ClientRequest request : requests) {
RequestFutureCompletionHandler handler = (RequestFutureCompletionHandler) request.callback();
AuthenticationException authenticationException = client.authenticationException(node);
if (authenticationException != null)
handler.onFailure(authenticationException);
else
handler.onComplete(new ClientResponse(request.makeHeader(request.requestBuilder().latestAllowedVersion()),
request.callback(), request.destination(), request.createdTimeMs(), now, true,
null, null));
}
}
}
}