KafkaSpout的處理流程

devos發表於2015-03-13

基於0.93版本Storm

首先,如果自己寫KafkaSpout,該怎麼辦?有哪些地方需要考慮呢

1. 得實現Storm指定的介面。這樣Storm才能夠使用它。那麼需要實現什麼介面?需要提供什麼功能給Storm呼叫呢?

2. 需要給spout的每個task指定任務,也就是把Kafka裡的訊息分配給spout task去讀取。這時候,就會有以下問題:

  • 是否一個KafkaSpout需要支援多個topic?鑑於每個topology裡可以有多個spout,這樣做沒有必要,而且會帶來較大的複雜性。
  • 如何把partitions分給tasks? 這時候存在如下問題:
    • 是否每個partition最多隻分給一個task?理論上,我們可以將一個partition的訊息給多個task處理,只要這些task區分自己負責的那部分訊息就行,比如一個task讀偶offset的訊息,一個讀奇offset的訊息。
    • 如何分才好。考慮到負載的平衡,而且要使得不同task間的任務不會衝突?比如,不會出現兩個task讀相同的訊息的情況。
  • 每個partition從何處開始讀取?如何記錄對當前Kafka topic的消費進度,使得在topology下線以後,這部分訊息不會丟失,以便以後可以接著上次的消費過度處理。

3. 如何讀取?怎麼使用Kafka API讀取訊息?每次讀多大量的訊息?需要預讀和緩衝嗎?

4. 無法從Kafka讀取訊息時如何處理?在spout裡重試?認為spout出現異常,交由Storm重新排程?

5. KafkaSpout的進度嚴重落後於Kafka訊息的數量時該如何處理?當spout讀取的速度太小,使得Kafka裡未被處理的訊息越來越多時如何處理?

6. 需要讀取的訊息不存在該如何處理?比如從Kafka取訊息時,想要獲取的訊息已經由於儲存時間過久,被Kafka刪除了,該如何處理?

7. 一個啟用了log compactiontopic會有何不同?


先列一下KafkaSpout的實現裡的關鍵類,以便接下來分析程式碼時更好理解

GlobalPartitionInformation

儲存partitionleader broker的對映

Private Map<Integer,Broker> partitionMap;

Partition

儲存某個partition和它的leader broker組成的元組

Public final Broker host;

Public final int partition;

KafkaSpout

實現IRichSpout介面

 

BrokerReader

獲取分割槽資訊。包括partition,以及partitionleader broker

GlobalPartitionInformation getCurrentBrokers();

PartitionManager

一個partition manager負責讀取一個partition中的訊息,並執行ack, fail, commit等操作

 

PartitionCoordinator

獲取當前task所使用的PartitionManager集合

重新整理當前task所使用的PartitionManager集合(以應於leader變更)

何時重新整理?

List<PartitionManager> getMyManagedPartitions();

PartitionManager getManager(Partition partition);

void refresh();

StaticCoordinator

根據SpoutConfig中對於partitionleader的靜態配置資訊,決定當前task所使用的PartitionManager集合。

不重新整理,只根據配置一次性決定partitionleader的對映

 

DynamicParitionConnections

儲存broker, SimpleConsumerpartition的對應關係。

管理SimpleConsumer集合,包括建立,關閉SimpleConsumer

根據partition獲取對應的SimpleConsumer,以複用SimpleConsumer

public SimpleConsumer register(Partition partition)

 

public SimpleConsumer register(Broker host, int partition)

 

public SimpleConsumer getConnection(Partition partition)

KafkaSpout的open方法

每個Spout task會有一個KafkaSpout的例項。當這個task初始化時,Storm會呼叫KafkaSpout的open方法,初始化這個spout task的執行環境,包括

  • a. 分配partiton給這個task
  • b. 為分到的每個partition生成一個PartitionManager。PartitionManager對於每個partition的訊息實現了Spout介面的ack, nextTuple, fail等主要功能。

關鍵程式碼如下:KafkaSpout的open方法主要用來為當前的spout task提供一個Coordinator.

//建立一個DynamicPartitionConnections,用於獲取partition對應的SimpleConsumer
        _connections = new DynamicPartitionConnections(_spoutConfig, KafkaUtils.makeBrokerReader(conf, _spoutConfig));

        // using TransactionalState like this is a hack
        //總共有多少task
        int totalTasks = context.getComponentTasks(context.getThisComponentId()).size();
        if (_spoutConfig.hosts instanceof StaticHosts) {
            _coordinator = new StaticCoordinator(_connections, conf, _spoutConfig, _state, context.getThisTaskIndex(), totalTasks, _uuid);
        } else {
            _coordinator = new ZkCoordinator(_connections, conf, _spoutConfig, _state, context.getThisTaskIndex(), totalTasks, _uuid);
        }

其中,在KafkaConfig中使用StaticHosts還是ZkHosts對DynamicParitionConnections和Coordinator的行為都有影響。

DynamicPartitonConnections  為Partition提供SimpleConsumer

因為Kafka的每個SimpleConsumer都可以用於與一個broker通訊,不管是否這些請求是針對同一個topic或partition。當一個broker作為多個partition的leader時,只需要為這一個broker建立一個SimpleConsumser,就可以用於消費這多個partition。所以需要DynamicPartitionConnection來管理partition與SimpleConsumser之間的對應關係,更好地複用。

  • 當使用StaticHosts時,KafkaUtils.makeBrokerReader(conf, _spoutConfig)會生成一個StaticBrokerReader. 這個BrokerReader只會提供StaticHosts例項化時使用的分割槽資訊。使得DynamicPartitionConnection的register(Partition partition)方法被呼叫時,只會返回同樣的SimpleConsumer。
  • 當使用ZkHosts時,KafkaUtils.makeBrokerReader(conf, _spoutConfig)會生成一個ZkBrokerReader。這個BrokerReader帶有自動重新整理功能,當兩次對它的的getCurrentBrokers的呼叫間隔較長,它就會重新獲取這個topic的GlobalParitionInformation,即重新獲取分割槽和分割槽的leader。使得DynamicPartitionConnection的register(Partition partition)方法被呼叫時,有可能會重新獲取最新的分割槽資訊。

Coordinator 為task分配partition,並且為每個partition建立PartitionManager

  Coordinator如何為task分配Partition?

無論是StaticCoordinator還是ZkCoordinator都是使用KafkaUtilsCalculatorPartitionsForTask方法來給task分配partitions

    public static List<Partition> calculatePartitionsForTask(GlobalPartitionInformation partitionInformation, int totalTasks, int taskIndex) {
        Preconditions.checkArgument(taskIndex < totalTasks, "task index must be less that total tasks");
        List<Partition> partitions = partitionInformation.getOrderedPartitions();
        int numPartitions = partitions.size();
        if (numPartitions < totalTasks) {
            LOG.warn("there are more tasks than partitions (tasks: " + totalTasks + "; partitions: " + numPartitions + "), some tasks will be idle");
        }
        List<Partition> taskPartitions = new ArrayList<Partition>();
        for (int i = taskIndex; i < numPartitions; i += totalTasks) {
            Partition taskPartition = partitions.get(i);
            taskPartitions.add(taskPartition);
        }
        logPartitionMapping(totalTasks, taskIndex, taskPartitions);
        return taskPartitions;
    }

  若一個task的index為a, 那麼分給它的partition在所有partition中的index(如果用StaticHosts,並且只提供了部分partition,那麼可能partition的index並不是partition id)為:

  partitionIndex = a + k*totalTasks, k是正整數,且partitionIndex < numPartitions

  • 當使用StaticHosts時,KafkaSpout會使用StaticCoordinator,這種Cooridnator的refresh方法什麼都不會做。
  • 當使用ZkHosts時,KafkaSpout會使用ZkCoordinator。這種Coordinator的refresh方法被呼叫時,它會通過BrokerReader獲取最新的分割槽資訊,重新為當前的task計算分割槽,然後為新的分割槽提供PartitionManager,從當前task的分割槽表時移除舊的分割槽,關閉舊的分割槽。注意,當某個分割槽的leader變更後,它對應的Partition例項的broker欄位會和以前的不同,因此會認為是新的Partition。當這種Coordinator的getMyManagedPartitions方法被呼叫時,如果過太久沒重新整理,它就會呼叫refresh()方法,重新獲取這個task對應的PartitionManager集合。
    public List<PartitionManager> getMyManagedPartitions() {
            if (_lastRefreshTime == null || (System.currentTimeMillis() - _lastRefreshTime) > _refreshFreqMs) {
                refresh();
                _lastRefreshTime = System.currentTimeMillis();
            }
            return _cachedList;
        }
    • 那麼何時getMyManagedPartition會被呼叫呢?是在KafkaSpout的nextTuple方法被呼叫時。也就是每次nextTuple被呼叫, ZkCoordinator都會檢查是否需要更新PartitionManager集合。

    • 如果partition的leader發生成了變更,而Coordinator沒有重新整理呢?此時,按照舊的leader獲取訊息,就丟擲異常。而KafkaSpout的nextTuple方法會捕獲異常,然後主動呼叫coordinator的refresh()方法獲取新的PartitionManager集合

KafkaSpout對於IRichSpout介面的實現

  nextTuple方法的實現 

 public void nextTuple() {
        List<PartitionManager> managers = _coordinator.getMyManagedPartitions();
        for (int i = 0; i < managers.size(); i++) {

            try {
                // in case the number of managers decreased
                _currPartitionIndex = _currPartitionIndex % managers.size();
                EmitState state = managers.get(_currPartitionIndex).next(_collector);
                if (state != EmitState.EMITTED_MORE_LEFT) {
                    _currPartitionIndex = (_currPartitionIndex + 1) % managers.size();
                }
                if (state != EmitState.NO_EMITTED) {
                    break;
                }
            } catch (FailedFetchException e) {
                LOG.warn("Fetch failed", e);
                _coordinator.refresh();
            }
        }

        long now = System.currentTimeMillis();
        if ((now - _lastUpdateMs) > _spoutConfig.stateUpdateIntervalMs) {
            commit();
        }
    }

首先,它會從coordinator處獲取當前所管理的所有partition.然後試著從這些partition的訊息中emit tuple, 由於可以採用schema解析Kafka的訊息,使得一個訊息對應多個tuple,所以這裡每次試用nextTuple,可能實際上會emit多個tuple。這就帶來了一個問題,如果一個 Kafka message生成多個tuple,那麼是否這些tuple都被ack了,才認為這個Kafka訊息處理完了呢?實際上,現在的KafkaSpout的實現裡,只要其中有一個tuple失敗了,就認為message失敗了。

可以看到,程式碼裡的for迴圈最多會迴圈manager.size()次,也就是它管理多少個partition,就最多迴圈幾次。但實際上,只要有一個訊息產生了tuple,for迴圈就會終止。也就是nextTuple被呼叫後,只要有一條訊息被成功解析為tuple,它就不再繼續處理訊息,在按配置時間間隔記錄下進度後,方法就執行完畢。nextTuple方法呼叫PartitionManager來emit tuple,根據PartitionManager的next方法返回的狀態nextTuple的控制流程。PartitionManager的next方法最多隻emit一條訊息產生的所有tuple,先說一下這個next方法返回的狀態的意義:

  • NO_EMITTED 表示此次呼叫沒有emit任何tuple。其它狀態都是已經從一條訊息emit了tuple,有可能處理了多條訊息,但可能最初的訊息沒能解析成tuple,但只有一條訊息解析成tuple,next方法就不會再處理訊息。
  • EMITTED_MORE_LEFT 表示已經處理了一個訊息emit了一個或一些tuple, 但是這個partition還有訊息已經被讀取卻還沒有處理。
  • EMITTED_END 表示已經從一個訊息emit了一個或一些tuple,並且這個partition所有已經獲取的訊息都已經被處理了。

根據這些狀態,KafkaSpout做出以下處理:

  • 如果不是NO_EMITTED,也就是EMITTED_MORE_LEFT或者EMITTED_END,表示已經emit了tuple,所以就退出for迴圈,不再emit新的tuple.
  • 如果不是EMITTED_MORE_LEFT,說明這個PartitionManager已讀的訊息都已進行了處理,下次就從另一個PartitionManager處獲取訊息,所以更新_currentPartitionIndex

不管是emit了tuple而退出迴圈, 或者把當前管理的partition迴圈了一遍之後還卻沒有emit任何訊息而退出迴圈。nextTuple的最後都會檢查是否需要在Zookeeper裡記錄進度。

 KafkaSpout的ack, commit, fail方法的具體邏輯都由PartitionManager來實現。下一篇會詳細進行分析。

相關文章