storm-kafka源码分析

版权声明:本文为博主原创文章,转载请注明来自http://blog.csdn.net/jediael_lu/ https://blog.csdn.net/jediael_lu/article/details/77149540

storm-kafka源码分析

@(KAFKA)[kafka, 大数据, storm]

一、概述

storm-kafka是storm用于读取kafka消息的连接器,本文主要对trident的实现部分作了解读。

(一)代码结构

storm-kafka中多7个package中,其中的org.apache.storm.kafka与org.apache.storm.kafka.trident中最核心的2个,分别用于处理storm-core与trident,其它package只是这2个的辅助。我们下面分别先简单看一下这2个package的内容。

注:还有一个包org.apache.storm.kafka.bolt用于向kafka写入数据,用得较少,暂不分析。

(二)org.apache.storm.kafka

org.apache.storm.kafka这个package包括了一些公共模块,以及storm-core的spout处理。

(三)org.apache.storm.kafka.trident

trident这个package中的类按照其功能可大致分为3类:spout, state和metric。除此之外,trident还调用了一些org.apache.storm.kafka中的类用于处理相同的事务,如metric, exception, DynamicBrokerReader等

1、spout

spout指定了如何从kafka中读取消息,根据trident的构架,它涉及的主要类为:
* OpaqueTridentKafkaSpout, TransactionalTridentKafkaSpout: 2种类型的spout
* Coordinator, TridentKafkaEmitter: 即Coordinator与Emitter的具体实现。
* GlobalPartitionInformation, ZkBrokerReader:2个重要的辅助类,分别记录了partition的信息以及如何从zk中读取kafka的状态(还有一个静态指定的,这里不分析)。

2、state

3、metric

主要涉及一个类:MaxMetric,其实还有其它metric,但在org.apache.storm.kafka中定义了。

(四)其它说明

1、线程与分区

注意,storm-kafka中的spout只是其中一个线程。
严格来说是每个partition只能由一个task负责,当然,一个task可以处理多个partition。但task和partition之间是怎么对应的呢?如何决定一个task处理哪些partition?

在trident拓扑中,多个batch会同时被处理(由MAX_SPOUT_PENDING决定),每个batch包含多个或者全部分区,每个batch读取的消息大小由fetchSizeBytes决定。

二、org.apache.storm.kafka

(一)基础类

这些基础的功能类可以大致分为以下几类:
* Bean类:表示某一种实体,包括Broker,BrokerHost, Partition 和trident.GlobalPartitionInformation
* 配置类: 包括KafkaConfig 和 SpoutConfig。
* zk读写类:包括获取state内容的ZkState,以及读取broker信息的DynamicBrokersReader和trident.ZkBrokerReader。
* 数据处理类:ZkCoordinator用于确定自已这个spout要处理哪些分区,以及某个分区对于的PartitionManager对象,而PartitionManager则真正的对某个分区进行处理了,DynamicPartitionConnections用于被PartitionManager调用以获取分区对应的SimpleConsumer,
* KafkaUtils: 一些功能方法。
另外还有一些metric和错误处理的类等,暂不介绍。

1、Broker

Broker只有2个变量:

public String host;
public int port;

表示一台kafka机器的地址与端口。

2、BrokerHosts

有2种实现:StaticHosts 与 ZkHost。
以ZkHost为例:

private static final String DEFAULT_ZK_PATH = "/brokers";
public String brokerZkStr = null;
public String brokerZkPath = null; // e.g., /kafka/brokers
public int refreshFreqSecs = 60;

可以看出,这是记录了kafka在zk中的位置(ip与路径),以及多久刷新一下这个信息。默认为/kafka/brokers,有2个子目录:

topic   ids

分别记录了topic信息及broker信息。

3、Partition

Partition记录了一个分区的具体信息,包括(所在的broker, 所属的topic,partition号)。

Partition(Broker host, String topic, int partition)

4、trident.GlobalPartitionInformation

GlobalPartitionInformation记录的是某个topic的所有分区信息,其中分区信息以一个TreeMap的形式来保存。

public String topic;
private Map<Integer, Broker> partitionMap;

它有一个getOrderedPartitions()方法,返回的就是这个topic的所有分区信息:

public List<Partition> getOrderedPartitions() {
    List<Partition> partitions = new LinkedList<Partition>();
    for (Map.Entry<Integer, Broker> partition : partitionMap.entrySet()) {
        partitions.add(new Partition(partition.getValue(), this.topic, partition.getKey(), this.bUseTopicNameForPartitionPathId));
    }
    return partitions;
}

注意,因为使用了TreeMap的数据结构,因此返回的结果就是有序的。

5、KafkaConfig

就是关于kafkaSpout的一些配置项,完整列表为:

public final BrokerHosts hosts;
public final String topic;
public final String clientId;

public int fetchSizeBytes = 1024 * 1024;
public int socketTimeoutMs = 10000;
public int fetchMaxWait = 10000;
public int bufferSizeBytes = 1024 * 1024;
public MultiScheme scheme = new RawMultiScheme();
public boolean ignoreZkOffsets = false;
public long startOffsetTime = kafka.api.OffsetRequest.EarliestTime();
public long maxOffsetBehind = Long.MAX_VALUE;
public boolean useStartOffsetTimeIfOffsetOutOfRange = true;
public int metricsTimeBucketSizeInSecs = 60;

6、SpoutConfig

SpoutConfig extends KafkaConfig

加了几个配置项:

public List<String> zkServers = null;
public Integer zkPort = null;
public String zkRoot = null;
public String id = null;

public String outputStreamId;

// setting for how often to save the current kafka offset to ZooKeeper
public long stateUpdateIntervalMs = 2000;

// Exponential back-off retry settings.  These are used when retrying messages after a bolt
// calls OutputCollector.fail().
public long retryInitialDelayMs = 0;
public double retryDelayMultiplier = 1.0;
public long retryDelayMaxMs = 60 * 1000;

7、ZkState

ZkState记录了每个partition的处理情况,它是通过读写zk来实现的,zk中的内容如下:

{"topology":{"id":"2e3226e2-ef45-4c53-b03f-aacd94068bc9","name":"ljhtest"},"offset":8066973,"partition":0,"broker":{"host":"gdc-kafka08-log.i.nease.net","port":9092},"topic":"ma30"}

上面的信息分别为topoId,拓扑名称,这个分区处理到的offset,分区号,这个分区在哪台kafka机器,哪个端口,以及topic名称。
ZkState只要提供了对这个zk信息的读写操作,如readJSON, writeJSON。

这些信息在zk中的位置通过构建KafkaConfig对象时的第3、4个参数指定,如下面的配置,则数据被写在/kafka2/ljhtest下面。因此第4个参数必须唯一,否则不同拓扑会有冲突。

 SpoutConfig kafkaConfig = new SpoutConfig(brokerHosts, "ma30", "/kafka2", "ljhtest");

而trident的默认位置为/transactional/${topo}

8、DynamicBrokersReader

读取zk中关于kafka的信息,如topic的分区等。

public List<GlobalPartitionInformation> getBrokerInfo() 

获取所有topic的分区信息。

private int getNumPartitions(String topic)

获取某个topic的分区数量。

9、trident.ZkBrokerReader

trident.ZkBrokerReader大部分功能通过DynamicBrokersReader完成,关于与zk的连接,都是通过前者完成。同时增加了以下2个方法:

  • getBrokerForTopic():返回某个topic的分区信息,返回的是GlobalPartitionInformation对象。这是由于可能同时读取多个分区的情况。
  • getAllBrokers():读取所有的分区,不指定topic。因为支持正则topic,所以有可能有多个topic。
  • refresh(): 这是一个private方法,每隔一段时间去refresh分区信息,在上面2个方法中被调用。
    每次发送一个新的batch时,会通过DynamicPartitionConnections#register()方法调用上面的方法,当时间超过refreshFreqSecs时,即会刷新分区信息。

10、ZkCoordinator

ZkCoordinator implements PartitionCoordinator

与之对应的还有个StaticCoordinator。
主要功能是读取zk中的分区信息,然后计算自己这个task负责哪些分区。

PartitionCoordinator只有3个方法:
(1)主要方法为getMyManagedPartitions(),即计算自己这个spout应该处理哪些分区。
还有refresh是去刷新分区信息的。

List<PartitionManager> getMyManagedPartitions();

(2)获取PartitionManager对象:

PartitionManager getManager(Partition partition);

(3)定期刷新分区信息

void refresh();

11、PartitionManager

记录了某个分区的连接信息,如:

Long _committedTo;
LinkedList<MessageAndOffset> _waitingToEmit = new LinkedList<MessageAndOffset>();
Partition _partition;
SpoutConfig _spoutConfig;
String _topologyInstanceId;
SimpleConsumer _consumer;
DynamicPartitionConnections _connections;
ZkState _state;

即这个分区的分区号,consumer等信息,还有用于发送消息的next()方法等,反正对某个分区的处理都在这个类中。
2个重点方法:
* fill()用于从kafka中获取消息,写到_waitingToEmit这个列表中。
* next()从上面准备的列表中读取数据,通过emit()发送出去。
* 还有ack(),fail等方法。
PartitionManager持有一个DynamicPartitionConnections对象,通过这个对象的regist方法可以获取到一个SimpleConsumer对象,从而对消息进行读取。

12、DynamicPartitionConnections

DynamicPartitionConnections用于记录broker—SimpleConsumber—-分区之间的关系。* 一个broker对应一个SimpleConsumber,但一个SimpleConsumer可以对应多个分区。尤其是spout的数量比分区数量少的时候*

主要用于创建SimpleConsumer,通过Partition信息,返回一个SimpleConsumer对象:

public SimpleConsumer register(Partition partition) {...}

以及unRegister()方法,取消关联。

Map<Broker, ConnectionInfo> _connections = new HashMap();

这个变量记录了一个broker的连接信息,其中ConnectionInfo有2个成员变量:

static class ConnectionInfo {
    SimpleConsumer consumer;
    Set<String> partitions = new HashSet<String>();

    public ConnectionInfo(SimpleConsumer consumer) {
        this.consumer = consumer;
    }
}

因此一个broker对应一个ConnectionInfo对象,而ConnectionInfo对象内有一个SimpleConsumber对象和其对应的多个分区。

13、KafkaUtils

很多公用方法,以后一个一个解释:

(1)calculatePartitionsForTask

public static List<Partition> calculatePartitionsForTask(List<GlobalPartitionInformation> partitons, int totalTasks, int taskIndex) {

计算某个task负责哪些分区。
注意,tridentSpout并未使用这个方法计算所负责的分区。TridentSpout的分区计算不在storm-kafka中实现,而是Trident机制自带的。详细的说是在OpaquePartitionedTridentSpoutExecutor的emitBatch()方法中计算。这就有个问题了,为什么在trident中,会自己计算负责的分区,而一般的storm需要自己来实现。

(二)KafkaSpout

在用户代码中,用户通过使用KafKaConfig对象创建一个KafkaSpout,这是整个拓扑的起点:

    SpoutConfig kafkaConfig = new SpoutConfig(brokerHosts, "ma30", "/test2", "ljhtest");
    kafkaConfig.scheme = new SchemeAsMultiScheme(new StringScheme());
    TopologyBuilder builder = new TopologyBuilder();
    builder.setSpout("words", new KafkaSpout(kafkaConfig), 10);

KafkaSpout继承自BaseRichSpout,有open(), nextTuple(), ack(), fail()等方法。
下面我们详细分析一下KafkaSpout这个类。

1、open()

KafkaSpout完成初始化的方法,当一个spout 被创建时,这个方法被调用。这个方法主要完成了以下几个对象的初始化:
* _state : 获取state目录下的内容,详见ZkState中的介绍。
* _connection:用于在每次发送消息(nextTuple方法法)时,获取某个分区的SimpleConsumer对象。
* _coordinator:用于在每次必发送消息时获取这个spout要处理哪些分区。
此外还有2个metric。

2、nextTuple()

    //获取这个task要处理哪些分区,然后对每个分区数据开始处理
    List<PartitionManager> managers = _coordinator.getMyManagedPartitions();
    for (int i = 0; i < managers.size(); i++) {

        // in case the number of managers decreased
       _currPartitionIndex = _currPartitionIndex % managers.size();
        //发送消息,下面慢慢分析。
        mitState state = managers.get(_currPartitionIndex).next(_collector);
    }

只要就2个步骤:
* 获取到这个spout要处理哪些分区
* 然后遍历分区,对消息进行处理,处理的过程在ParitionManage中,稍后再详细介绍。

三、trident

OpaqueTridentKafkaSpout implements IOpaquePartitionedTridentSpout

(一)tridentspout的主要流程

1、主要调用流程回顾

先说明一下,一个spout的组成分成三个部分,简单的说就是消息是从MasterBatchCoordinator开始的,它是一个真正的spout,而TridentSpoutCoordinator与TridentSpoutExecutor都是bolt,MasterBatchCoordinator发起协调消息,最后的结果是TridentSpoutExecutor发送业务消息。而发送协调消息与业务消息的都是调用用户Spout中BatchCoordinator与Emitter中定义的代码。

MaterBatchCorodeinator —————> ITridentSpout.Coordinator#isReady
|
|
v
TridentSpoutCoordinator —————> ITridentSpout.Coordinator#[initialTransaction, success, close]
|
|
v
TridentSpoutExecutor —————> ITridentSpout.Emitter#(emitBatch, success(),close)
对于分区是OpaquePartitionedTridentSpoutExecutor等

如果需要详细了解这个过程,可参考:
http://blog.csdn.net/lujinhong2/article/details/49785077

我们先简单介绍一下所有的相关类及其位置,然后分别介绍Coordinator与Emitter的实现。尤其是着重分析一下Emitter部分,因为它是实际读取kafka消息,并向下游发送的过程。

2、指定spout

用户在代码中用以下语句指定使用哪个spout,如:

OpaqueTridentKafkaSpout kafkaSpout = new OpaqueTridentKafkaSpout(kafkaConfig);

然后storm根据这个spout的代码,找到对应的Coordinator与Emitter。我们看一下OpaqueTridentKafkaSpout的代码。
这代码很简单,主要完成了:
(1)初始化一个Spout时,会要求传递一个TridentKafkaConfig的参数,指定一些配置参数。

TridentKafkaConfig _config;

public OpaqueTridentKafkaSpout(TridentKafkaConfig config) {
    _config = config;
}

(2)然后就分别指定了Coordinator与Emitter:

@Override
public IOpaquePartitionedTridentSpout.Emitter<List<GlobalPartitionInformation>, Partition, Map> getEmitter(Map conf, TopologyContext context) {
    return new TridentKafkaEmitter(conf, context, _config, context
            .getStormId()).asOpaqueEmitter();
}

@Override
public IOpaquePartitionedTridentSpout.Coordinator getCoordinator(Map conf, TopologyContext tc) {
    return new org.apache.storm.kafka.trident.Coordinator(conf, _config);
}

(二)Coordinator

1、Coordinator的实例化

public Coordinator(Map conf, TridentKafkaConfig tridentKafkaConfig) {
    config = tridentKafkaConfig;
    reader = KafkaUtils.makeBrokerReader(conf, config);
}

2、close()与isReady()

Coordinator通过TridentKafkaConfig传入一个DefaultCoordinator的对象,Coordinator的close()及isReady()均是通过调用DefaultCoordinator的实现来完成的。

@Override
public void close() {
    config.coordinator.close();
}

@Override
public boolean isReady(long txid) {
    return config.coordinator.isReady(txid);
}

我们接着看一下DefaultCoordinator的实现:

@Override
public boolean isReady(long txid) {
    return true;
}

@Override
public void close() {
}

很简单,isReady()直接返回true,close()则不做任何事情。

3、getPartitionsForBatch()

这个方法的功能是在初始化一个事务时,去zk读取最新的分区信息(当然是缓存超时后才读)。

@Override
public List<GlobalPartitionInformation> getPartitionsForBatch() {
    return reader.getAllBrokers();
}

注释为:
Return the partitions currently in the source of data. The idea is is that if a new partition is added and a prior transaction is replayed, it doesn’t emit tuples for the new partition because it knows what partitions were in that transaction.

由下面可以看出,getPartitionsForBatch()都是在初始化一个事务时被调用的。
透明型:

    @Override
    public Object initializeTransaction(long txid, Object prevMetadata, Object currMetadata) {
        return _coordinator.getPartitionsForBatch();
    }

事务型:

    @Override
    public Integer initializeTransaction(long txid, Integer prevMetadata, Integer currMetadata) {
        if(currMetadata!=null) {
            return currMetadata;
        } else {
            return _coordinator.getPartitionsForBatch();            
        }
    }

那我们继续看看这个方法完成了什么功能:

@Override
public List<GlobalPartitionInformation> getAllBrokers() {
    refresh();
    return cachedBrokers;
}

除了这个,还有一个使用静态指定的,暂不管它。

private void refresh() {
    long currTime = System.currentTimeMillis();
    if (currTime > lastRefreshTimeMs + refreshMillis) {
        try {
            LOG.info("brokers need refreshing because " + refreshMillis + "ms have expired");
            cachedBrokers = reader.getBrokerInfo();
            lastRefreshTimeMs = currTime;
        } catch (java.net.SocketTimeoutException e) {
            LOG.warn("Failed to update brokers", e);
        }
    }
}

其它就是在超时的情况下去zk读取broker的信息,并返回partitions的信息。返回的信息为GlobalPartitionInformation列表,即topic与其具体分区信息的map。

public List<GlobalPartitionInformation> getBrokerInfo() throws SocketTimeoutException {
  List<String> topics =  getTopics();
  List<GlobalPartitionInformation> partitions =  new ArrayList<GlobalPartitionInformation>();

  for (String topic : topics) {
      GlobalPartitionInformation globalPartitionInformation = new GlobalPartitionInformation(topic, this._isWildcardTopic);
      try {
          int numPartitionsForTopic = getNumPartitions(topic);
          String brokerInfoPath = brokerPath();
          for (int partition = 0; partition < numPartitionsForTopic; partition++) {
              int leader = getLeaderFor(topic,partition);
              String path = brokerInfoPath + "/" + leader;
              try {
                  byte[] brokerData = _curator.getData().forPath(path);
                  Broker hp = getBrokerHost(brokerData);
                  globalPartitionInformation.addPartition(partition, hp);
              } catch (org.apache.zookeeper.KeeperException.NoNodeException e) {
                  LOG.error("Node {} does not exist ", path);
              }
          }
      } catch (SocketTimeoutException e) {
          throw e;
      } catch (Exception e) {
          throw new RuntimeException(e);
      }
      LOG.info("Read partition info from zookeeper: " + globalPartitionInformation);
      partitions.add(globalPartitionInformation);
  }
    return partitions;
}

以下内容均是对emitter的介绍
注意,在trident中,每个task负责哪些分区是在storm-core中计算好的,因此在emitter中只负责处理这个分区的消息就行了,具体来说是在OpaquePartitionedTridentSpoutExecutor.emitBatch()中计算分区的

(三)Emitter : TridentKafkaEmitter结构

TridentKafkaEmitter中有2个内部类,分别对应事务型与透明型的spout。事务型的spout重发batch时必须与上一批次相同,而透明型是没这个需要的,可以从其它可能的分区中取一批新的数据。

1、offset与nextOffset

消息处理的metaData中保存了offset与nextOffset2个数据,其中后者一般通过MessageAndOffset#nextOffset()来获取到。offset表示当前正在处理的消息的offset,nextOffset表示当前消息的下一个offset。举个例子:

(offset)*这是一批消息**(nextOffset)
因此正常情况下,应该offset

1、事务型的spout

有5个方法,我们这里先讨论其中2个核心方法。storm根据某个batch是否第一次发送来决定调用哪个方法。

emitPartitionBatchNew()

当某个batch是第一次发送时,调用此方法,这个方法的调用顺序为:

emitPartitionBatchNew() —-> failFastEmitNewPartitionBatch() —–> doEmitNewPartitionBatch()

emitPartitionBatch()

当某个batch是重发时,调用此方法,这个方法的调用顺序为:
emitPartitionBatch() —–> reEmitPartitionBatch()

2、透明型的spout

透明型的spout不需要保证重发的batch与上一批次是相同的,因此,对于每一次发送都是相同的逻辑即可,不需要管是否第一次发送,它只有一个发送方法。

emitPartitionBatch()

emitPartitionBatch() —–> emitNewPartitionBatch() —-> failFastEmitNewPartitionBatch() —–> doEmitNewPartitionBatch()

2种类型发送数据时只终均是调用doEmitNewPartitionBatch(),而透明型的spout在调用之前会先使用emitNewPartitionBatch()来捕获FailedFetchException,重新获取一份新的元数据,以准备读取新的消息

image

3、公共方法

除了以上的发送数据方法以外,它们均还有以下3个方法,下面再详细分析。

        @Override
        public void refreshPartitions(List<Partition> partitions) {
            refresh(partitions);
        }

        @Override
        public List<Partition> getOrderedPartitions(GlobalPartitionInformation partitionInformation) {
            return orderPartitions(partitionInformation);
        }

        @Override
        public void close() {
            clear();
        }

(四)透明型spout

1、emitPartitionBatch()

/**
         * Emit a batch of tuples for a partition/transaction.
         *
         * Return the metadata describing this batch that will be used as lastPartitionMeta
         * for defining the parameters of the next batch.
         */
        @Override
        public Map emitPartitionBatch(TransactionAttempt transactionAttempt, TridentCollector tridentCollector, Partition partition, Map map) {
            return emitNewPartitionBatch(transactionAttempt, tridentCollector, partition, map);
        }

当需要发送一个新的batch时,storm会调用emitPartitionBatch方法,此方法直接调用emitNewPartitionBatch。

参数说明:
* transactionAttempt,只有2个成员变量,即long _txId和int _attemptId,即记录了当前的事务id及已经尝试的次数。
* tridentCollector,就是用于发送消息的collector。
* partition,表示一个分区,可以理解为kafka的一个分区,有2个成员变量,分别为Broker host和int partition,即kafka的机器与分区id。
* map,用于记录这个事务的元数据,详细见后面分析。

2、emitNewPartitionBatch()

private Map emitNewPartitionBatch(TransactionAttempt attempt, TridentCollector collector, Partition partition, Map lastMeta) {
    try {
        return failFastEmitNewPartitionBatch(attempt, collector, partition, lastMeta);
    } catch (FailedFetchException e) {
        LOG.warn("Failed to fetch from partition " + partition);
        if (lastMeta == null) {
            return null;
        } else {
            Map ret = new HashMap();
            ret.put("offset", lastMeta.get("nextOffset"));
            ret.put("nextOffset", lastMeta.get("nextOffset"));
            ret.put("partition", partition.partition);
            ret.put("broker", ImmutableMap.of("host", partition.host.host, "port", partition.host.port));
            ret.put("topic", _config.topic);
            ret.put("topology", ImmutableMap.of("name", _topologyName, "id", _topologyInstanceId));
            return ret;
        }
    }
}

很明显,也只是简单调用failFastEmitNewPartitionBatch,但如果获取消息失败的话,则会创建一个新元数据。
如果lastMeta为null的话,则会直接返回null,则会从其它地方(如zk)进行初始化(邮见下面的分析);如果不为空,则根据lastMeta的值,根据一个新的元数据。元数据包括以下几个字段:
* offset:下一个需要处理的offset
* nextOffset:由于未开始处理batch,所以offset与nextOffset都是同一个值。注意,如果正在处理一个batch,则offset是正在处理的batch的offset,而nextOffset则是下一个需要处理的offset。
* partition:就是哪个分区了
* broker:哪台kafka机器以及端口
* topic:哪个kafka topic
* topology:拓扑的名称与id。

TODO:ImmutableMap.of()

ImmutableMap.of("name", _topologyName, "id", _topologyInstanceId)

TODO:如果获取失败,哪里更新了新的分区信息,是fetch里面作了处理吗?后面再看。

3、failFastEmitNewPartitionBatch()

    private Map failFastEmitNewPartitionBatch(TransactionAttempt attempt, TridentCollector collector, Partition partition, Map lastMeta) {
    SimpleConsumer consumer = _connections.register(partition);
    Map ret = doEmitNewPartitionBatch(consumer, partition, collector, lastMeta);
    _kafkaOffsetMetric.setLatestEmittedOffset(partition, (Long) ret.get("offset"));
    return ret;
}

先根据partition信息注册一个consumer,注意这里的分区信息包括了机器、端口还有分区id等。然后就调用doEmitNewPartitionBatch执行实际事务,最后的是metric的使用。

4、doEmitNewPartitionBatch()

(1)确定offset

简单的说,就是
* 如果lastMeta为空,则从其它地方(如zk)获取offset;
* 否则,如果当前topoid与之前的不同(表示拓扑重启过)而且ignoreZkOffsets为true,则从指定的offset开始;
* 如果当前topoid与之前的相同(表示在持续处理消息中),或者ignoreZkOffsets为false,则从之前的位置继续处理

    long offset;
    //1、如果lastMeta不为空,则:
    if (lastMeta != null) {
        String lastInstanceId = null;
        Map lastTopoMeta = (Map) lastMeta.get("topology");
        if (lastTopoMeta != null) {
            lastInstanceId = (String) lastTopoMeta.get("id");
        }
        //1.1:如果ignoreZkOffsets为true,而且当前拓扑id与之前的id不同时,则从指定的时间点开始获取消息。
        if (_config.ignoreZkOffsets && !_topologyInstanceId.equals(lastInstanceId)) {
            offset = KafkaUtils.getOffset(consumer, _config.topic, partition.partition, _config.startOffsetTime);
        } else {
            //1.2:如果ignoreZkOffsets为false,或者当前拓扑id与之前的id相同(表示拓扑没有重启过,一直在处理消息中),则继续之前的处理。
            offset = (Long) lastMeta.get("nextOffset");
        }
    } else {
        //2、如果lastMeta为空,则从其它地方(如zk)获取之前的offset
        offset = KafkaUtils.getOffset(consumer, _config.topic, partition.partition, _config);
    }

(2)读取消息

ByteBufferMessageSet msgs = null;
    try {
        msgs = fetchMessages(consumer, partition, offset);
    } catch (TopicOffsetOutOfRangeException e) {
        long newOffset = KafkaUtils.getOffset(consumer, _config.topic, partition.partition, kafka.api.OffsetRequest.EarliestTime());
        LOG.warn("OffsetOutOfRange: Updating offset from offset = " + offset + " to offset = " + newOffset);
        offset = newOffset;
        msgs = KafkaUtils.fetchMessages(_config, consumer, partition, offset);
    }

如果TopicOffsetOutOfRangeException,则从最旧的消息开始读。

(3)发送消息并更新offset

long endoffset = offset;
    for (MessageAndOffset msg : msgs) {
        emit(collector, msg.message());
        endoffset = msg.nextOffset();
    }

每发送一条消息则将endoffset往后移一位,直到发送完时,endoffset就是下一个需要处理的offset。

(4)构建下一个meta并返回

    Map newMeta = new HashMap();
    newMeta.put("offset", offset);
    newMeta.put("nextOffset", endoffset);
    newMeta.put("instanceId", _topologyInstanceId);
    newMeta.put("partition", partition.partition);
    newMeta.put("broker", ImmutableMap.of("host", partition.host.host, "port", partition.host.port));
    newMeta.put("topic", _config.topic);
    newMeta.put("topology", ImmutableMap.of("name", _topologyName, "id", _topologyInstanceId));
    return newMeta;

关于metric的设置以及读取kafka消息的实现,下面单独介绍

(五)事务型spout

1、emitPartitionBatchNew()

当某个batch第一次发送时调用此方法,返回是这个batch相关的元数据,可用于重构这个batch。如果这个batch出错需要重发,则调用emitPartitionBatch(),下面再介绍。

        /**
         * Emit a batch of tuples for a partition/transaction that's never been emitted before.
         * Return the metadata that can be used to reconstruct this partition/batch in the future.
         */
        @Override
        public Map emitPartitionBatchNew(TransactionAttempt transactionAttempt, TridentCollector tridentCollector, Partition partition, Map map) {
            return failFastEmitNewPartitionBatch(transactionAttempt, tridentCollector, partition, map);
        }

与透明型不同的是,它没有捕获FailedFetchException这个异常,因此出现获取消息失败时,会一直等待某个分区恢复。其它处理逻辑与透明型相同,参考上面的介绍即可。

2、emitPartitionBatch()

        /**
         * Emit a batch of tuples for a partition/transaction that has been emitted before, using
         * the metadata created when it was first emitted.
         */
        @Override
        public void emitPartitionBatch(TransactionAttempt transactionAttempt, TridentCollector tridentCollector, Partition partition, Map map) {
            reEmitPartitionBatch(transactionAttempt, tridentCollector, partition, map);
        }

当一个batch之前已经发送过,但失败了,则调用此方法重试。

3、reEmitPartitionBatch()

重试发送消息的主要实现,逻辑也相对简单。
直接去fetch消息。如果消息不为空的话,则判断offset:
* 如果offset与nextoffset相等,则表示消息已经处理完了
* 如果offset>nextOffset,则出错了,抛出以下运行时异常:

    throw new RuntimeException("Error when re-emitting batch. overshot the end offset");

最后发送消息,并更新nextOffset。
完整代码如下:

private void reEmitPartitionBatch(TransactionAttempt attempt, TridentCollector collector, Partition partition, Map meta) {
    LOG.info("re-emitting batch, attempt " + attempt);
    String instanceId = (String) meta.get("instanceId");
    if (!_config.ignoreZkOffsets || instanceId.equals(_topologyInstanceId)) {
        SimpleConsumer consumer = _connections.register(partition);
        long offset = (Long) meta.get("offset");
        long nextOffset = (Long) meta.get("nextOffset");
        ByteBufferMessageSet msgs = null;
        msgs = fetchMessages(consumer, partition, offset);

        if(msgs != null) {
            for (MessageAndOffset msg : msgs) {
                if (offset == nextOffset) {
                    break;
                }
                if (offset > nextOffset) {
                    throw new RuntimeException("Error when re-emitting batch. overshot the end offset");
                }
                emit(collector, msg.message());
                offset = msg.nextOffset();
            }
        }
    }
}

(六)2种spout的公共方法

1、refreshPartitions()

根据注释可知,当处理一些新的分区时,管理到这些分区的连接信息。

  /**
         * This method is called when this task is responsible for a new set of partitions. Should be used
         * to manage things like connections to brokers.
         */
        @Override
        public void refreshPartitions(List<Partition> partitions) {
            refresh(partitions);
        }

2、getOrderedPartitions()

getOrderedPartitions()方法会在分区元数据发生变化(即Partitions发生变化)时被调用。该方法与refreshPartitions()方法调用时机相同,用来应对分区的变化。例如,建立并维护与新增加Partitions的连接时就可以使用这个方法。

3、close()

看下面的实现,其实refreshPartitions()和close()都只是简单的清空了连接,而getOrderedPartitions是获取分区信息。

private void clear() {
    _connections.clear();
}

private List<Partition> orderPartitions(GlobalPartitionInformation partitions) {
    return partitions.getOrderedPartitions();
}

private void refresh(List<Partition> list) {
    _connections.clear();
    _kafkaOffsetMetric.refreshPartitions(new HashSet<Partition>(list));
}

4、Partitions与Partition

Partitions含义为分区的元数据,如一共存在多少个分区,分区所在的broker等,具体信息由用户定义,不过这些信息一般是比较稳定的。在kafka中,是通过以下代码指定的:

new ZkHosts(brokerHosts)

看如何将zk中的信息导入Partitions的:

Partition则是某个具体的分区了。

在coordinator的getPartitionsForBatch()中指定。

(七)fetch消息的逻辑

_connection包括了一些连接信息,如broker,端口,分区id等,通过它可以获取到一个simpleConsumer,下面重点分析这个获取消息的过程。

 msgs = fetchMessages(consumer, partition, offset);

1、fetchMessages()

private ByteBufferMessageSet fetchMessages(SimpleConsumer consumer, Partition partition, long offset) {
    long start = System.nanoTime();
    ByteBufferMessageSet msgs = null;
    msgs = KafkaUtils.fetchMessages(_config, consumer, partition, offset);
    long end = System.nanoTime();
    long millis = (end - start) / 1000000;
    _kafkaMeanFetchLatencyMetric.update(millis);
    _kafkaMaxFetchLatencyMetric.update(millis);
    return msgs;
}

主要调用 KafkaUtils.fetchMessages(_config, consumer, partition, offset);其余代码用于更新metric,统计获取消息的平均时长以及最大时长。

2、KafkaUtil.fetchMessages()

逻辑很简单,构建一个FetchRequest,然后得到FetchResponse。此外就是一些处理异常的代码了

public static ByteBufferMessageSet fetchMessages(KafkaConfig config, SimpleConsumer consumer, Partition partition, long offset)
        throws TopicOffsetOutOfRangeException, FailedFetchException,RuntimeException {
    ByteBufferMessageSet msgs = null;
    String topic = config.topic;
    int partitionId = partition.partition;
    FetchRequestBuilder builder = new FetchRequestBuilder();
    FetchRequest fetchRequest = builder.addFetch(topic, partitionId, offset, config.fetchSizeBytes).
            clientId(config.clientId).maxWait(config.fetchMaxWait).build();
    FetchResponse fetchResponse;
    try {
        fetchResponse = consumer.fetch(fetchRequest);
    } catch (Exception e) {
        if (e instanceof ConnectException ||
                e instanceof SocketTimeoutException ||
                e instanceof IOException ||
                e instanceof UnresolvedAddressException
                ) {
            LOG.warn("Network error when fetching messages:", e);
            throw new FailedFetchException(e);
        } else {
            throw new RuntimeException(e);
        }
    }
    if (fetchResponse.hasError()) {
        KafkaError error = KafkaError.getError(fetchResponse.errorCode(topic, partitionId));
        if (error.equals(KafkaError.OFFSET_OUT_OF_RANGE) && config.useStartOffsetTimeIfOffsetOutOfRange) {
            String msg = "Got fetch request with offset out of range: [" + offset + "]";
            LOG.warn(msg);
            throw new TopicOffsetOutOfRangeException(msg);
        } else {
            String message = "Error fetching data from [" + partition + "] for topic [" + topic + "]: [" + error + "]";
            LOG.error(message);
            throw new FailedFetchException(message);
        }
    } else {
        msgs = fetchResponse.messageSet(topic, partitionId);
    }
    return msgs;
}

(八)KafkaOffsetMetric

TODO:还有其它metric吧

storm-kafka中定义了一个metric用来计算目前正在处理的offset与最新的offset之间有多少差距,即落后了多少条数据。

这个类定义在KafkaUtil中,主要有2个核心变量:
_partitionToOffset是一个hashMap,内容为(分区,正在处理的offset)
_partitions就是_partitionToOffset的key组成的一个集合。

public static class KafkaOffsetMetric implements IMetric {
    Map<Partition, Long> _partitionToOffset = new HashMap<Partition, Long>();
    Set<Partition> _partitions;
    String _topic;
    DynamicPartitionConnections _connections;

    public KafkaOffsetMetric(String topic, DynamicPartitionConnections connections) {
        _topic = topic;
        _connections = connections;
    }

    public void setLatestEmittedOffset(Partition partition, long offset) {
        _partitionToOffset.put(partition, offset);
    }

    @Override
    public Object getValueAndReset() {
        try {
            long totalSpoutLag = 0;
            long totalEarliestTimeOffset = 0;
            long totalLatestTimeOffset = 0;
            long totalLatestEmittedOffset = 0;
            HashMap ret = new HashMap();
            if (_partitions != null && _partitions.size() == _partitionToOffset.size()) {
                for (Map.Entry<Partition, Long> e : _partitionToOffset.entrySet()) {
                    Partition partition = e.getKey();
                    SimpleConsumer consumer = _connections.getConnection(partition);
                    if (consumer == null) {
                        LOG.warn("partitionToOffset contains partition not found in _connections. Stale partition data?");
                        return null;
                    }
                    long latestTimeOffset = getOffset(consumer, _topic, partition.partition, kafka.api.OffsetRequest.LatestTime());
                    long earliestTimeOffset = getOffset(consumer, _topic, partition.partition, kafka.api.OffsetRequest.EarliestTime());
                    if (latestTimeOffset == KafkaUtils.NO_OFFSET) {
                        LOG.warn("No data found in Kafka Partition " + partition.getId());
                        return null;
                    }
                    long latestEmittedOffset = e.getValue();
                    long spoutLag = latestTimeOffset - latestEmittedOffset;
                    ret.put(_topic + "/" + partition.getId() + "/" + "spoutLag", spoutLag);
                    ret.put(_topic + "/" + partition.getId() + "/" + "earliestTimeOffset", earliestTimeOffset);
                    ret.put(_topic + "/" + partition.getId() + "/" + "latestTimeOffset", latestTimeOffset);
                    ret.put(_topic + "/" + partition.getId() + "/" + "latestEmittedOffset", latestEmittedOffset);
                    totalSpoutLag += spoutLag;
                    totalEarliestTimeOffset += earliestTimeOffset;
                    totalLatestTimeOffset += latestTimeOffset;
                    totalLatestEmittedOffset += latestEmittedOffset;
                }
                ret.put(_topic + "/" + "totalSpoutLag", totalSpoutLag);
                ret.put(_topic + "/" + "totalEarliestTimeOffset", totalEarliestTimeOffset);
                ret.put(_topic + "/" + "totalLatestTimeOffset", totalLatestTimeOffset);
                ret.put(_topic + "/" + "totalLatestEmittedOffset", totalLatestEmittedOffset);
                return ret;
            } else {
                LOG.info("Metrics Tick: Not enough data to calculate spout lag.");
            }
        } catch (Throwable t) {
            LOG.warn("Metrics Tick: Exception when computing kafkaOffset metric.", t);
        }
        return null;
    }

    public void refreshPartitions(Set<Partition> partitions) {
        _partitions = partitions;
        Iterator<Partition> it = _partitionToOffset.keySet().iterator();
        while (it.hasNext()) {
            if (!partitions.contains(it.next())) {
                it.remove();
            }
        }
    }
}

这个metric只在2个地方被调用:
(1)第一次读取一个分区时

_kafkaOffsetMetric.setLatestEmittedOffset(partition, (Long) ret.get("offset"));

(2)refresh时

_kafkaOffsetMetric.refreshPartitions(new HashSet<Partition>(list));

refreshPartitions()时会调用refresh方法。This method is called when this task is responsible for a new set of partitions. Should be used to manage things like connections to brokers.

猜你喜欢

转载自blog.csdn.net/jediael_lu/article/details/77149540