Handling catastrophic failover in Kafka

Viewed 100

Let's imaging a simple message processing pipeline, like on the image below: enter image description here

A group of consumers listens to a topic, picks messages one by one, does some sort of processing and sends them over to the next topic.

Some messages crash the consumer or make it stuck forever (so then a liveness probe kills the consumer after timeout). In this case a consumer is not able to commit the offset, so the malicious message gets picked up by another consumer. And also makes it crash. Ideally we want to move the message to a dead letter topic after N such attempts.

This can be achieved by introducing a shared storage: enter image description here

But this creates coupling between the services and introduces a Single Point of Failure (SPOF) which is the shared database.

I'm looking for ideas on how to work this around with stateless services.

1 Answers

If your context is correct with this approach (that's something you should judge, as I'm only trying to give a suggestion), please consider decoupling the consumption and the processing.

In your case, the consumer is stopped, not because it was not able to read from kafka, and/or the kafka broker wasn't able to provide messages, but because the processing of the message was too slow and/or unsuccesful.

The consumer, in fact, was correctly receiving the messages. It was the processing of them that made it be declared dead.

First of all, the KafkaConsumer javadoc block regarding this (just above the constructor summary). The second option is the one quoted here

2. Decouple Consumption and Processing

Another alternative is to have one or more consumer threads that do all data consumption and hands off ConsumerRecords instances to a blocking queue consumed by a pool of processor threads that actually handle the record processing. This option likewise has pros and cons:

PRO: This option allows independently scaling the number of consumers and processors. This makes it possible to have a single consumer that feeds many processor threads, avoiding any limitation on partitions.

CON: Guaranteeing order across the processors requires particular care as the threads will execute independently an earlier chunk of data may actually be processed after a later chunk of data just due to the luck of thread execution timing. For processing that has no ordering requirements this is not a problem.

CON: Manually committing the position becomes harder as it requires that all threads co-ordinate to ensure that processing is complete for that partition.**

Esentially, works like this. The consumer keeps reading and gives the responsibility of the processing and process-timeout management to the processor threads .

The error handling of the message processing would be responsibility of the processor threads as well. For example, if a timeout is thrown or an exception occurs, the processor will send the message to your defined "dead" queue, or whatever management of this you wish to perform, without involving the consumer. Regardless of the processor threads' success or fail, the consumer will continue its job and never be considered dead for not calling poll() in the specified timeout.

You should control the amount of messages the consumer retrieves in its poll call in order not to saturate the processors. Its a game regarding how fast the processors finish their job, how many messages the consumer retrieves (max.poll.records) at each iteration, and what's the specified timeout for the consumer.


Decoupled workflow

The first element to be quoted is the queue (with a limited size, which you should also manage in order not getting too filled - OOM).

This queue would be the link between consumer and processor threads, essentially a buffer that could dynamically get bigger or smaller depending on the specific word load at each time; It would manage overloads, something like a dam, or barrier, to find a similarity.

                                            ----->WORKERTHREAD1
  KAFKA <------> CONSUMER ----> QUEUE -----|
                                            ----->WORKERTHREAD2

What you get is a second queue-lag mechanism:

1. Kafka Consumer LAG (the messages still to be read from the partition/topic)

2. Queue LAG (received messages still need to be processed)

                                                     --->WORKERTHREAD1
    KAFKA <--(LAG)--> CONSUMER ----> QUEUE --(LAG)--|
                                                     --->WORKERTHREAD2

The queue could be some kind of synchronized queue, such a ConcurrentLinkedQueue. for example. Or you could manage yourself the synchronization with a customized queue.

Essentially, the duties would be divided, and the consumer is given the easiest one (as its the one that is most crucial).

Responsibilities:

  • Consumer

     consume-->send to queue
    
  • Workers

     read from queue|-->[manage timeout]
                    |==>PROCESS MESSAGE ==> send to topic
                    |-->[handle failed messages]
    

You should also manage if the processor threads die/deadlock; but usually those mechanisms are already implemented in most of ThreadPool variants.

I suggest the workers to share a unique KafkaProducer; The producer is thread safe and since the output topic would be the same for the group of consumers, this would also increase its performance. Also from the Kafka Producer javadoc:

The producer is thread safe and sharing a single producer instance across threads will generally be faster than having multiple instances.

In resume, each consumer thread feeds n processor threads. Some variants could be:

 - 1 consumer  - 1 worker  (no processing paralellization, just division of duties)
 - 1 consumer  - 2 workers
 - 1 consumer  - 4 workers
 - 2 consumers - 4 workers (2 for each)
 - 2 consumers - 8 workers (4 for each)
          ...

Read carefully the pros and contras from this mechanism in the javadoc, and judge if this could be a solution to your specific case.

In my oppinion, there's a PRO that doesn't get reflected in the docs, which is the root of this answer/suggestion:

Consumption shouldn't be affected by processing. This approach avoids any consumer thread being considered dead due to a slow processing of the messages, and offers an extra "safety-window" thanks to the queue. I'm not saying that, at the point in which all processors fail for every message, or the queue hits maximum size, for example, the consumer would continue happily as if that didn't affect it; It will in fact be stopped by processing, but much, much later and due to bigger reasons that couldn't be avoided. This approach offers some extra time, or extra shield, for that to happen. Just like a dam can fail if it can't hold any more water.


Well, hope you take this as a suggestion, and may it be helpful somehow. It may avoid most of the dead consumer issues you're having. If well managed, it's a good approach for 24/7 real time data workflow.

Related