kafka源碼---消費者(1)

《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));
                }
            }
        }
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章