storm kafka

1.1 KafkaSpout流程

  1. 建立zookeeper客户端,在zookeeper "borkers/topics/" + _topic + "/partitions" 路径下获取到partition列表
  2. 针对每个partition 到路径"borkers/topics/" + _topic + "/partitions"+"/" + partition_id + "/state"下面获取到leader partition 所在的broker id
  3. 到/brokers/ids/broker id 路径下获取broker的host 和 port 信息,并保存到Map中partition_id –-> learder broker
  4. 获取spout的任务个数和当前任务的index,然后再根据partition的个数来分配当前spout 所消费的partition列表
  5. 针对所消费的每个broker建立一个SimpleConsumer对象用来从kafka上获取数据,我们是从partition的leader读取数据,应该是连接leader所在的broker节点, 然后构建具体的SimpleConsumer对象
  6. 提交当前partition的消费信息到zookeeper上面保存(0.9以前的版本)

1.2 partition 的分配策略

  1. 在KafkaSpout中获取spout的task的个数,对应就是consumer的个数
  2. 在KafkaSpout中获取当前spout的 task index,注意,task index和task id是不同的,task id是当前spout在整个topology中的id,而task index是当前spout在组件中的id,取值范围为[0, spout_task_number-1]
  3. 获取所有的partiton与对应的leader partition所在broker的映射关系
  4. 获取当前spout消费的partition的列表,假设spout的并发度是3,当前spout的task index 是 1,总的partition的个数为5,那么当前spout消费的partition id为1,4

1.3 partition的更新策略

如果出现broker宕机,spout挂掉的情况,那么spout是要重新分配parition的,KafkaSpout并没有监听zookeeper上broker、partition和其他spout的状态,所以当有异常发生的时候KafkaSpout并不知道的,它采用了两种方法来更新partition的分配。

  1. 定时更新
    根据ZkHosts中的refreshFreqSecs字段来定时更新partition列表,我们可以通过修改配置来更改定时刷新的间隔。每一次调用kafkaspout的nextTuple方法时,都会首先调用ZkCoordinator的getMyManagedPartitions方法来获取当前spout消费的partition列表;getMyManagedPartitions方法中会判断是否已经到了该刷新的时间,如果到了就重新分配partition(默认60秒)
public List getMyManagedPartitions() {
        if (_lastRefreshTime == null || (System.currentTimeMillis() - _lastRefreshTime) > _refreshFreqMs) {
            refresh();
            _lastRefreshTime = System.currentTimeMillis();
        }
        return _cachedList;
    }
  1. 异常更新
    当调用kafkaspout的nextTuple方法出现异常时(除了UpdateOffsetException),强制更新当前spout的partition消费列表。

1.4 消费状态的维护

首先要分析一下当spout启动的时候是怎么获取初始offset的。在每个spout获取到消费的partition列表时,会针对每个partition来创建PartitionManager对象,下面看一下PartitionManager的初始化过程:

  1. 到连接池里注册partition leader所在的broker host,如果连接池里有该broker的连接,则直接返回该连接、如果连接池里没有,则建立broker的连接,并返回连接对象SimpleConsumer
Map _connections = new HashMap();
public SimpleConsumer register(Partition partition) {
    Broker broker = _reader.getCurrentBrokers().getBrokerFor(partition.partition);
    return register(broker, partition.partition);
}
  1. 获取zookeeper上offset的提交路径
private String committedPath() {
    return _spoutConfig.zkRoot + "/" + _spoutConfig.id + "/" + _partition.getId();
}
  1. 从提交路径上读取信息,提取记录的该partition的消费offset;如果zookeeper上没有该路径则表示当前topic没有被spout消费过

可以根据时间戳查询offset,细粒度为log segment,查询最新可能的offset在不大于这个时间戳下。segment size比较大的时候,offset会不准确。为了更精确,我们可以配置log segment的大小,基于时间(log.roll.ms) 代替基于大小 (log.segment.bytes).

  1. 从broker上获取当前partition的offset,默认为获取最新的offset,如果用户配置forceFromStart(KafkaConfig),则获取该partition最早的offset,也就是consume from beginning。
  • 情况1: 如果从zookeeper上没有获取topology和消费信息,则直接用从broker上获取到的offset
  • 情况2: 获取到的topology id 不一致 或者用户要求从新获取数据的时候,则从broker上获取offset。
  • 情况3: 使用zookeeper上保留的offset进行消费;
  • 如果zookeeper消费的offset已经过期,则直接消费新数据

PartitionManager 中的 _emittedToOffset用来保存当前消费的offset,在每一次获取到消息的时候都会更新这个值

offset的提交是周期性的,提交的周期可在SpoutConfig中的stateUpdateIntervalMs(2秒)中来配置。每次调用kafkaspout的nextTuple方法后都会判断是否需要提交offset;

如果需要提交则调用kafkaspout的commit方法,使用轮巡的方式提交每个partition的消费状况;具体的提交是委托PartitionManager来完成的

  1. 获取当前要提交的offset,如果pending Set剩余offset的话,就说明还有一些消息没有完成处理,则提交pending消息的第一个offset。
  2. 如果没有pending的消息,则提交当前消费的offset。
public void commit() {
    long lastCompletedOffset = lastCompletedOffset();
    if (_committedTo != lastCompletedOffset) {
        LOG.debug("Writing last completed offset (" + lastCompletedOffset + ") to ZK for " + _partition + " for topology: " + _topologyInstanceId);
        Map data = (Map) ImmutableMap.builder()
            .put("topology", ImmutableMap.of("id", _topologyInstanceId,
                "name", _stormConf.get(Config.TOPOLOGY_NAME)))
            .put("offset", lastCompletedOffset)
            .put("partition", _partition.partition)
            .put("broker", ImmutableMap.of("host", _partition.host.host,
                "port", _partition.host.port))
            .put("topic", _spoutConfig.topic).build();
        _state.writeJSON(committedPath(), data);

        _committedTo = lastCompletedOffset;
        LOG.debug("Wrote last completed offset (" + lastCompletedOffset + ") to ZK for " + _partition + " for topology: " + _topologyInstanceId);
    } else {
        LOG.debug("No new offset for " + _partition + " for topology: " + _topologyInstanceId);
    }
}

1.5 kafkaspout ack 和 fail的处理

  1. 当调用kafkaspout的nextTuple方法时,kafkaspout委托PartitionManager next方法来发送数据
 public EmitState next(SpoutOutputCollector collector) {
    if (_waitingToEmit.isEmpty()) {
            fill();
        }
        while (true) {
            MessageAndRealOffset toEmit = _waitingToEmit.pollFirst();
            if (toEmit == null) {
                return EmitState.NO_EMITTED;
            }
            Iterable> tups = KafkaUtils.generateTuples(_spoutConfig, toEmit.msg);
            if (tups != null) {
                for (List tup : tups) {
                    collector.emit(tup, new KafkaMessageId(_partition, toEmit.offset));
                }
                break;
            } else {
                ack(toEmit.offset);
            }
        }
        if (!_waitingToEmit.isEmpty()) {
            return EmitState.EMITTED_MORE_LEFT;
        } else {
            return EmitState.EMITTED_END;
        }
}

  1. 判断等待队列是否为空,如果为空则调用fill方法从broker上取数据进行填充
  2. 对kafka的消息进行解码,KafkaUtils.generateTuples方法
  3. 如果tuple不为null,则发送该tuple,messageID为new KafkaMessageId(_partition, toEmit.offset),这样在ack 或者 fail的时候才能根据_partition找到相应的PartitionManager
  4. 在PartitionManager会维护一个pending 列表,用来保存已经发送但是没有被成功处理的消息,一个failed列表,用来保存已经失败的消息
  5. 当一个消息成功处理时会调用spout的ack方法,kafkaspout会根据message id中包含的partition id 来委托相应的PartitionManager来处理
  6. PartitionManager 接收到ack消息后,会判断pending的最早的一条消息是否已经过质保,如果过质保,则清除队列中所有过保的消息,如果没有过保的消息,则在pending队列中移除当前消息
public void ack(Long offset) {
        if (!_pending.isEmpty() && _pending.first() < offset - _spoutConfig.maxOffsetBehind) {
            // Too many things pending!
            _pending.headSet(offset - _spoutConfig.maxOffsetBehind).clear();
        }
        _pending.remove(offset);
        numberAcked++;
}
  1. 当一条消息处理失败时,会调用spout的fail方法,同样,kafkaspout会根据message id中包含的partition id 来委托相应的PartitionManager来处理
  2. PartitionManager接收到fail消息,会判断失败的消息是否已经过保,如果过保则忽略掉,如果在保质期内,则加入failed列表,如果没有成功响应的消息,并且失败的消息个数已经超过保质期个数,则认为没有消息成功,系统有问题,丢异常(最大保存个数maxOffsetBehind)
 public void fail(Long offset) {
        if (offset < _emittedToOffset - _spoutConfig.maxOffsetBehind) {
            LOG.info(
                    "Skipping failed tuple at offset=" + offset +
                            " because it's more than maxOffsetBehind=" + _spoutConfig.maxOffsetBehind +
                            " behind _emittedToOffset=" + _emittedToOffset
            );
        } else {
            LOG.debug("failing at offset=" + offset + " with _pending.size()=" + _pending.size() + " pending and _emittedToOffset=" + _emittedToOffset);
            failed.add(offset);
            numberFailed++;
            if (numberAcked == 0 && numberFailed > _spoutConfig.maxOffsetBehind) {
                throw new RuntimeException("Too many tuple failures");
            }
        }
}
  1. 对于failed的消息会进行重发
private void fill() {
        long offset;
        final boolean had_failed = !failed.isEmpty();
        // Are there failed tuples? If so, fetch those first.
        if (had_failed) {
            offset = failed.first();
        } else {
            offset = _emittedToOffset;
        }
        ByteBufferMessageSet msgs = null;
        try {
            msgs = KafkaUtils.fetchMessages(_spoutConfig, _consumer, _partition, offset);
        } catch (UpdateOffsetException e) {
            _emittedToOffset = KafkaUtils.getOffset(_consumer, _spoutConfig.topic, _partition.partition, _spoutConfig);
            LOG.warn("Using new offset: {}", _emittedToOffset);
            // fetch failed, so don't update the metrics
            return;
        }
        if (msgs != null) {
            int numMessages = 0;
            for (MessageAndOffset msg : msgs) {
                final Long cur_offset = msg.offset();
                if (cur_offset < offset) {
                    // Skip any old offsets.
                    continue;
                }
                if (!had_failed || failed.contains(cur_offset)) {
                    numMessages += 1;
                    _pending.add(cur_offset);
                    _waitingToEmit.add(new MessageAndRealOffset(msg.message(), cur_offset));
                    _emittedToOffset = Math.max(msg.nextOffset(), _emittedToOffset);
                    if (had_failed) {
                        failed.remove(cur_offset);
                    }
                }
            }
        }
}

你可能感兴趣的:(storm kafka)