Storm Kafka Spout上最多的元组重放次数

时间:2015-10-02 16:49:47

标签: apache-kafka apache-storm

我们正在使用Storm和Kafka Spout。当我们对消息失败时,我们想重播它们,但在某些情况下,错误的数据或代码错误会导致消息始终失败,因此我们将进入无限的重放周期。显然我们在找到错误时会修复错误,但希望我们的拓扑通常具有容错能力。在重放N次以后,我们怎么能得到一个元组?

查看Kafka Spout的代码,我发现它设计为使用指数退避计时器和comments on the PR状态重试:

“spout不会终止重试周期(我确信它不应该这样做,因为它不能报告关于中止请求的失败的上下文),它只处理延迟重试。拓扑仍然期望最终调用ack()而不是fail()来停止循环。“

我已经看到StackOverflow的回复建议编写一个自定义的喷口,但是如果有一种推荐的方法在Bolt中执行此操作,我宁可不要坚持维护Kafka Spout内部的自定义补丁。

在博尔特中这样做的正确方法是什么?我没有看到元组中的任何状态暴露了它被重播的次数。

5 个答案:

答案 0 :(得分:5)

Storm本身并不为您的问题提供任何支持。因此,定制解决方案是唯一的出路。即使您不想修补KafkaSpout,我认为,引入计数器并打破其中的重播周期,将是最好的方法。作为替代方案,您也可以从KafkaSpout继承并在您的子类中放置一个计数器。这当然有点类似于补丁,但可能不那么具有侵入性并且更容易实现。

如果你想使用Bolt,你可以执行以下操作(这也需要对KafkaSpout或其子类进行一些更改。)

  • 为每个元组分配一个唯一ID作为附加属性(可能已经有一个唯一ID可用;否则,您可以引入“counter-ID”或仅引入整个元组,即所有属性,以识别每个元组元组)。
  • KafkaSpout之后通过fieldsGrouping在ID上插入一个螺栓(以确保重播的元组流式传输到同一个螺栓实例)。
  • 在你的螺栓中,使用缓冲所有元组的HashMap<ID,Counter>并计算(重新)尝试的次数。如果计数器小于阈值,则转发输入元组,使其由后面的实际拓扑处理(当然,您需要适当地锚定元组)。如果计数大于您的阈值,请确认元组打破周期并从HashMap中删除其条目(您可能还想记录所有失败的元组)。
  • 为了从HashMap中删除成功处理的元组,每次在KafkaSpout中执行元组时,您需要将元组ID转发给螺栓,以便它可以从{{0}}中删除元组。 {1}}。只需为HashMap子类声明第二个输出流并覆盖KafkaSpout(当然,您需要调用Spout.ack(...)以确保super.ack(...)获得确认。

这种方法可能会占用大量内存。作为KafkaSpout中每个元组的条目的替代方法,您还可以使用第三个流(与另外两个连接到螺栓),并在元组失败时转发元组ID(即, HashMap)。每次,螺栓从该第三流接收“失败”消息,计数器增加。只要Spout.fail(...)中没有条目(或未达到阈值),螺栓就会转发元组进行处理。这应该减少使用的内存,但需要在你的喷口和螺栓中实现更多的逻辑。

这两种方法都有缺点,每个acked元组都会向新引入的螺栓产生额外的消息(从而增加网络流量)。对于第二种方法,似乎您只需要向螺栓发送“确认”消息,以获取之前失败的元组。但是,您不知道哪些元组确实失败了哪些元组失败了。如果您想摆脱这种网络开销,可以在HashMap中引入第二个HashMap来缓冲失败消息的ID。因此,如果成功重播失败的元组,则只能发送“确认”消息。当然,第三种方法使逻辑实现更加复杂。

如果不在某种程度上修改KafkaSpout,我认为没有解决方案。我个人会修补KafkaSpout或将第三种方法与KafkaSpout子类中的HashMap和螺栓一起使用(因为它消耗的内存很少,并且不会给网络带来很多额外的负担与前两个解决方案相比)。

答案 1 :(得分:0)

基本上它的工作原理如下:

  1. 如果部署拓扑,它们应该是生产级别(这是预期的质量水平,并且元组数量很低)。
  2. 如果元组失败,请检查元组是否确实有效。
  3. 如果元组有效(例如因为无法连接到外部数据库而无法插入,或者类似的东西)回复它。
  4. 如果一个元组错误形成且永远无法处理(例如数据库ID是文本而数据库期望一个整数)它应该是ack,你永远无法修复这样的事情或将其插入数据库。
  5. 应记录新的异常(以及元组内容本身)。您应该检查这些日志并生成规则以在将来验证元组。最后添加代码以便在将来正确处理它们(ETL)。
  6. 不记录所有内容,否则您的日志文件会非常庞大​​,对您的日志记录非常有选择性。日志文件的内容应该是有用的,而不是一堆垃圾。
  7. 继续这样做,最终你只会涵盖所有情况。

答案 2 :(得分:0)

我们也面临类似的数据,我们有不良数据导致螺栓无限失效。

为了在运行时解决这个问题,我们又引入了一个将其命名为&#34; DebugBolt&#34;以供参考。因此,喷嘴首先将消息发送到此螺栓,然后此螺栓为坏消息执行所需的数据修复,然后将它们发送到所需的螺栓。这样就可以动态修复数据错误。

另外,如果你需要删除一些消息,你实际上可以将一个来自DebugBolt的ignoreFlag传递给你的原始Bolt,如果ignoreFlag为True,原始的bolt应该只发送一个ack到spout而不进行处理。

答案 3 :(得分:0)

我们只是让我们的螺栓在错误流上发出坏元组并对其进行处理。另一个螺栓通过将其写回专门针对错误的Kafka主题来处理错误。这使我们可以轻松地将正常与错误数据流引导通过拓扑。

我们使元组失败的唯一情况是因为某些必需资源处于脱机状态,例如网络连接,数据库,......这些都是可重试的错误。其他任何内容都将指向错误流,以便根据需要进行修复或处理。

当然,这一切都假定您不希望招致任何数据丢失。如果您只想尝试尽力而且在几次重试后忽略,那么我会考虑其他选项。

答案 4 :(得分:0)

据我所知,Storm对此不提供内置支持。

我已经应用了以下提到的实现:

public class AuditMessageWriter extends BaseBolt {

        private static final long serialVersionUID = 1L;
        Map<Object, Integer> failedTuple = new HashMap<>();

        public AuditMessageWriter() {

        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void prepare(Map stormConf, TopologyContext context, OutputCollector collector) {
            this.collector = collector;
            //any initialization if u want
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void execute(Tuple input) {
            try {

            //Write your processing logic
            collector.ack(input);
            } catch (Exception e2) {
            //In case of any exception save the tuple in failedTuple map with a count 1
            //Before adding the tuple in failedTuple map check the count and increase it and fail the tuple

            //if failure count reaches the limit (message reprocess limit) log that and remove from map and acknowledge the tuple
            log(input);
            ExceptionHandler.LogError(e2, "Message IO Exception");
            }

        }

        void log(Tuple input) {

            try {
                //Here u can pass result to dead queue or log that
//And ack the tuple 
            } catch (Exception e) {
                ExceptionHandler.LogError(e, "Exception while logging");
            }
        }

        @Override
        public void cleanup() {
            // To declare output fields.Not required in this alert.
        }

        @Override
        public void declareOutputFields(OutputFieldsDeclarer declarer) {
            // To declare output fields.Not required in this alert.
        }

        @Override
        public Map<String, Object> getComponentConfiguration() {
            return null;
        }

    }