1. 得实现Storm指定的接口。这样Storm才能够使用它。那么需要实现什么接口?需要提供什么功能给Storm调用呢?
2. 需要给spout的每个task指定任务,也就是把Kafka里的消息分配给spout task去读取。这时候,就会有以下问题:
3. 如何读取?怎么使用Kafka API读取消息?每次读多大量的消息?需要预读和缓冲吗?
4. 无法从Kafka读取消息时如何处理?在spout里重试?认为spout出现异常,交由Storm重新调度?
5. 当KafkaSpout的进度严重落后于Kafka消息的数量时该如何处理?当spout读取的速度太小,使得Kafka里未被处理的消息越来越多时如何处理?
6. 需要读取的消息不存在该如何处理?比如从Kafka取消息时,想要获取的消息已经由于存储时间过久,被Kafka删除了,该如何处理?
7. 一个启用了log compaction的topic会有何不同?
GlobalPartitionInformation |
存储partition和leader broker的映射 |
Private Map<Integer,Broker> partitionMap; |
Partition |
存储某个partition和它的leader broker组成的元组 |
Public final Broker host; Public final int partition; |
KafkaSpout |
实现IRichSpout接口 |
|
BrokerReader |
获取分区信息。包括partition,以及partition的leader 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中对于partition和leader的静态配置信息,决定当前task所使用的PartitionManager集合。 不刷新,只根据配置一次性决定partition和leader的映射 |
|
DynamicParitionConnections |
存储broker, SimpleConsumer和partition的对应关系。 管理SimpleConsumer集合,包括建立,关闭SimpleConsumer 根据partition获取对应的SimpleConsumer,以复用SimpleConsumer |
public SimpleConsumer register(Partition partition)
public SimpleConsumer register(Broker host, int partition)
public SimpleConsumer getConnection(Partition partition) |
每个Spout task会有一个KafkaSpout的实例。当这个task初始化时,Storm会调用KafkaSpout的open方法,初始化这个spout task的运行环境,包括
关键代码如下: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的行为都有影响。
因为Kafka的每个SimpleConsumer都可以用于与一个broker通信,不管是否这些请求是针对同一个topic或partition。当一个broker作为多个partition的leader时,只需要为这一个broker建立一个SimpleConsumser,就可以用于消费这多个partition。所以需要DynamicPartitionConnection来管理partition与SimpleConsumser之间的对应关系,更好地复用。
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
public List<PartitionManager> getMyManagedPartitions() { if (_lastRefreshTime == null || (System.currentTimeMillis() - _lastRefreshTime) > _refreshFreqMs) { refresh(); _lastRefreshTime = System.currentTimeMillis(); } return _cachedList; }
那么何时getMyManagedPartition会被调用呢?是在KafkaSpout的nextTuple方法被调用时。也就是每次nextTuple被调用, ZkCoordinator都会检查是否需要更新PartitionManager集合。
KafkaSpout对于IRichSpout接口的实现
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方法返回的状态的意义:
根据这些状态,KafkaSpout做出以下处理:
不管是emit了tuple而退出循环, 或者把当前管理的partition循环了一遍之后还却没有emit任何消息而退出循环。nextTuple的最后都会检查是否需要在Zookeeper里记录进度。