Streaming Apps and Poison Pills: handle the unexpected with Kafka Streams, Loïc Divad, Software Engineer, Publicis Sapient Engineering
https://www.meetup.com/Lille-Kafka/events/272064179/
4. 4@loicmdivad @PubSapientEng 4@loicmdivad @PubSapientEng
> println(sommaire)
Incoming records may be corrupted, or cannot be
handled by the serializer / deserializer. These
records are referred to as “poison pills”
1. Log and Crash
2. Skip the Corrupted
3. Sentinel Value Pattern
4. Dead Letter Queue Pattern
11. 11@loicmdivad @PubSapientEng
Really old systems receive raw bytes
directly from message queues
With Kafka (Connect and Streams)
we’d like to continuously transform
these messages
10100110111010101
Kafka Connect
Kafka Brokers
Exercise #1 - breakfast
12. 12@loicmdivad @PubSapientEng
Really old systems receive raw bytes
directly from message queues
With Kafka (Connect and Streams)
we’d like to continuously transform
these messages
But we need a deserializer with
special decoder to understand each
event
What happens if we get a buggy
implementation of the deserializer?
10100110111010101
Kafka Connect
Kafka Brokers
Kafka Streams
Exercise #1 - breakfast
19. 19@loicmdivad @PubSapientEng
Log and Crash
2019-04-17 03:43:12 macbook-de-lolo [ERROR] (LogAndFailExceptionHandler.java:39) - Exception caught during
Deserialization, taskId: 0_0, topic: input-food-order, partition: 0, offset: 109
Exception in thread "answer-one-breakfast-0d808ce7-0ef1-44c6-808a-f594bc7fceae-StreamThread-1"
org.apache.kafka.streams.errors.StreamsException: Deserialization exception handler is set to fail upon a
deserialization error. If you would rather have the streaming pipeline continue after a deserialization error, please
set the default.deserialization.exception.handler appropriately.
at org.apache.kafka.streams.processor.internals.RecordDeserializer.deserialize(RecordDeserializer.java:80)
at org.apache.kafka.streams.processor.internals.RecordQueue.addRawRecords(RecordQueue.java:101)
at org.apache.kafka.streams.processor.internals.PartitionGroup.addRawRecords(PartitionGroup.java:124)
...
at org.apache.kafka.streams.processor.internals.StreamTask.addRecords(StreamTask.java:711)
at org.apache.kafka.streams.processor.internals.StreamThread.run(StreamThread.java:747)
Caused by: java.lang.IllegalArgumentException: dishes: Insufficient number of elements: decoded 0 but should have
decoded 268435712
at scodec.Attempt$Failure.require(Attempt.scala:108)
at fr.xebia.ldi.ratatouille.serde.BreakfastDeserializer.deserialize(BreakfastDeserializer.scala:22)
at fr.xebia.ldi.ratatouille.serde.BreakfastDeserializer.deserialize(BreakfastDeserializer.scala:15)
at org.apache.kafka.common.serialization.Deserializer.deserialize(Deserializer.java:58)
at fr.xebia.ldi.ratatouille.serde.BreakfastDeserializer.deserialize(BreakfastDeserializer.scala:15)
at org.apache.kafka.streams.processor.internals.SourceNode.deserializeValue(SourceNode.java:60)
at org.apache.kafka.streams.processor.internals.RecordDeserializer.deserialize(RecordDeserializer.java:66)
20. 20@loicmdivad @PubSapientEng
Log and Crash
2019-04-17 03:43:12 macbook-de-lolo [ERROR] (LogAndFailExceptionHandler.java:39) - Exception caught during
Deserialization, taskId: 0_0, topic: exercise-breakfast, partition: 0, offset: 109
Exception in thread "answer-one-breakfast-0d808ce7-0ef1-44c6-808a-f594bc7fceae-StreamThread-1"
org.apache.kafka.streams.errors.StreamsException: Deserialization exception handler is set to fail upon a
deserialization error. If you would rather have the streaming pipeline continue after a deserialization error, please
set the default.deserialization.exception.handler appropriately.
at org.apache.kafka.streams.processor.internals.RecordDeserializer.deserialize(RecordDeserializer.java:80)
at org.apache.kafka.streams.processor.internals.RecordQueue.addRawRecords(RecordQueue.java:101)
at org.apache.kafka.streams.processor.internals.PartitionGroup.addRawRecords(PartitionGroup.java:124)
...
at org.apache.kafka.streams.processor.internals.StreamTask.addRecords(StreamTask.java:711)
at org.apache.kafka.streams.processor.internals.StreamThread.run(StreamThread.java:747)
Caused by: java.lang.IllegalArgumentException: dishes: Insufficient number of
elements: decoded 0 but should have decoded 268435712
at scodec.Attempt$Failure.require(Attempt.scala:108)
at fr.xebia.ldi.ratatouille.serde.BreakfastDeserializer.deserialize(BreakfastDeserializer.scala:22)
at fr.xebia.ldi.ratatouille.serde.BreakfastDeserializer.deserialize(BreakfastDeserializer.scala:15)
at org.apache.kafka.common.serialization.Deserializer.deserialize(Deserializer.java:58)
at fr.xebia.ldi.ratatouille.serde.BreakfastDeserializer.deserialize(BreakfastDeserializer.scala:15)
at org.apache.kafka.streams.processor.internals.SourceNode.deserializeValue(SourceNode.java:60)
at org.apache.kafka.streams.processor.internals.RecordDeserializer.deserialize(RecordDeserializer.java:66)
24. 24@loicmdivad @PubSapientEng
▼ Change consumer group
▼ Manually update my offsets
▼ Reset my streaming app and set my auto reset to
LATEST
▽ $ kafka-streams-application-reset ...
▼ Destroy the topic, no message = no poison pill
▽ $ kafka-topics --delete --topic ...
▼ My favourite <3
▽ $ confluent destroy && confluent start
Don’t Do
▼ Fill an issue and suggest a fix to the tooling team
26. 26@loicmdivad @PubSapientEng 26@loicmdivad @PubSapientEng
Log and Crash
Like all consumers, Kafka Streams applications
deserialize messages from the broker.
The deserialization process can fail. It raises an
exception that cannot be caught by our code.
Buggy deserializers have to be fixed before the
application restarts, by default ...
31. 31@loicmdivad @PubSapientEng
Skip the Corrupted
2019-04-17 03:43:12 macbook-de-lolo [ERROR] (LogAndFailExceptionHandler.java:39) - Exception caught during
Deserialization, taskId: 0_0, topic: exercise-breakfast, partition: 0, offset: 109
Exception in thread "answer-one-breakfast-0d808ce7-0ef1-44c6-808a-f594bc7fceae-StreamThread-1"
org.apache.kafka.streams.errors.StreamsException: Deserialization exception handler is set to fail upon a
deserialization error. If you would rather have the streaming pipeline continue after a
deserialization error, please set the default.deserialization.exception.handler
appropriately.
at org.apache.kafka.streams.processor.internals.RecordDeserializer.deserialize(RecordDeserializer.java:80)
at org.apache.kafka.streams.processor.internals.RecordQueue.addRawRecords(RecordQueue.java:101)
at org.apache.kafka.streams.processor.internals.PartitionGroup.addRawRecords(PartitionGroup.java:124)
...
at org.apache.kafka.streams.processor.internals.StreamTask.addRecords(StreamTask.java:711)
at org.apache.kafka.streams.processor.internals.StreamThread.run(StreamThread.java:747)
Caused by: java.lang.IllegalArgumentException: ... decoded 0 but should have decoded 268435712
at scodec.Attempt$Failure.require(Attempt.scala:108)
at fr.xebia.ldi.ratatouille.serde.BreakfastDeserializer.deserialize(BreakfastDeserializer.scala:22)
at fr.xebia.ldi.ratatouille.serde.BreakfastDeserializer.deserialize(BreakfastDeserializer.scala:15)
at org.apache.kafka.common.serialization.Deserializer.deserialize(Deserializer.java:58)
at fr.xebia.ldi.ratatouille.serde.BreakfastDeserializer.deserialize(BreakfastDeserializer.scala:15)
at org.apache.kafka.streams.processor.internals.SourceNode.deserializeValue(SourceNode.java:60)
at org.apache.kafka.streams.processor.internals.RecordDeserializer.deserialize(RecordDeserializer.java:66)
32. 32@loicmdivad @PubSapientEng 32@loicmdivad @PubSapientEng
public class LogAndFailExceptionHandler implements DeserializationExceptionHandler
/* ... */
public class LogAndContinueExceptionHandler implements DeserializationExceptionHandler
/* ... */
33. 33@loicmdivad @PubSapientEng
public class LogAndFailExceptionHandler implements DeserializationExceptionHandler
/* ... */
public class LogAndContinueExceptionHandler implements DeserializationExceptionHandler
/* ... */
public interface DeserializationExceptionHandler extends Configurable {
DeserializationHandlerResponse handle(final ProcessorContext context,
final ConsumerRecord<byte[], byte[]> record,
final Exception exception);
enum DeserializationHandlerResponse {
CONTINUE(0, "CONTINUE"),
FAIL(1, "FAIL");
/* ... */
}
}
}
34. 34@loicmdivad @PubSapientEng
public class LogAndFailExceptionHandler implements DeserializationExceptionHandler
/* ... */
public class LogAndContinueExceptionHandler implements DeserializationExceptionHandler
/* ... */
public interface DeserializationExceptionHandler extends Configurable {
DeserializationHandlerResponse handle(final ProcessorContext context,
final ConsumerRecord<byte[], byte[]> record,
final Exception exception);
enum DeserializationHandlerResponse {
CONTINUE(0, "CONTINUE"),
FAIL(1, "FAIL");
/* ... */
}
}
}
Take
Away
40. 40@loicmdivad @PubSapientEng 40@loicmdivad @PubSapientEng
Skip the Corrupted
All exceptions thrown by deserializers are caught by
a DeserializationExceptionHandler
A handler returns Fail or Continue
You can implement your own Handler
But the two handlers provided by the library are
really basic… let’s explore other methods
41. 41@loicmdivad @PubSapientEng 41@loicmdivad @PubSapientEng
All exceptions thrown by deserializers are caught by
a DeserializationExceptionHandler
A handler returns Fail or Continue
You can implement your own Handler
But the two handlers provided by the library are
really basic… let’s explore other methods
Skip the Corrupted
Take
Away
45. 45@loicmdivad @PubSapientEng
We need to turn the deserialization process into a
pure transformation that cannot crash
To do so, we will replace corrupted message by a
sentinel value. It’s a special-purpose record (e.g: null,
None, Json.Null, etc ...)
Sentinel Value Pattern
f: G → H
G H
46. 46@loicmdivad @PubSapientEng
We need to turn the deserialization process into a
pure transformation that cannot crash
To do so, we will replace corrupted message by a
sentinel value. It’s a special-purpose record (e.g: null,
None, Json.Null, etc ...)
This allows downstream processors to recognize and
handle such sentinel values
Sentinel Value Pattern
f: G → H
G H
G H
47. 47@loicmdivad @PubSapientEng
We need to turn the deserialization process into a
pure transformation that cannot crash
To do so, we will replace corrupted message by a
sentinel value. It’s a special-purpose record (e.g: null,
None, Json.Null, etc ...)
This allows downstream processors to recognize and
handle such sentinel values
With Kafka Streams this can be achieved by
implementing a Deserializer
Sentinel Value Pattern
f: G → H
G H
G H
null
54. 54@loicmdivad @PubSapientEng 54@loicmdivad @PubSapientEng
Sentinel Value Pattern
By implementing a custom serde we can create a safe
Deserializer.
Downstreams now receive a sentinel value
indicating a deserialization error.
Errors can then be treated correctly, example:
monitoring the number of deserialization
errors with a custom metric
But we lost a lot of information about the error…
let’s see a last method
55. 55@loicmdivad @PubSapientEng 55@loicmdivad @PubSapientEng
Sentinel Value Pattern
By implementing a custom serde we can create a safe
Deserializer.
Downstreams now receive a sentinel value
indicating a deserialization error.
Errors can then be treated correctly, example:
monitoring the number of deserialization
errors with a custom metric
But we lost a lot of information about the error…
let’s see a last method
Take
Away
58. 58@loicmdivad @PubSapientEng
Dead Letter Queue Pattern
In this method we will let the deserializer fail.
For each failure we will send a message to a topic
containing corrupted messages.
Each message will have the original content of the
input message (for reprocessing) and additional
meta data about the failure.
With Kafka Streams this can be achieved by
implementing a DeserializationExceptionHandler
Streaming
APP
dead letter queue
input topic output topic
60. 60@loicmdivad @PubSapientEng
class DeadLetterQueueFoodExceptionHandler() extends DeserializationExceptionHandler {
override def handle(context: ProcessorContext,
record: ConsumerRecord[Array[Byte], Array[Byte]],
exception: Exception): DeserializationHandlerResponse = {
val producerRecord = new ProducerRecord(topic, /*same key, value and ts,*/ headers.asJava)
producer.send(producerRecord, /* Producer Callback */ )
DeserializationHandlerResponse.CONTINUE
}
61. 61@loicmdivad @PubSapientEng
class DeadLetterQueueFoodExceptionHandler() extends DeserializationExceptionHandler {
var topic: String = _
var producer: KafkaProducer[Array[Byte], Array[Byte]] = _
override def configure(configs: util.Map[String, _]): Unit = ???
override def handle(context: ProcessorContext,
record: ConsumerRecord[Array[Byte], Array[Byte]],
exception: Exception): DeserializationHandlerResponse = {
val producerRecord = new ProducerRecord(topic, /*same key, value and ts,*/ headers.asJava)
producer.send(producerRecord, /* Producer Callback */ )
DeserializationHandlerResponse.CONTINUE
}
62. 62@loicmdivad @PubSapientEng
class DeadLetterQueueFoodExceptionHandler() extends DeserializationExceptionHandler {
var topic: String = _
var producer: KafkaProducer[Array[Byte], Array[Byte]] = _
override def configure(configs: util.Map[String, _]): Unit = ???
override def handle(context: ProcessorContext,
record: ConsumerRecord[Array[Byte], Array[Byte]],
exception: Exception): DeserializationHandlerResponse = {
val headers = record.headers().toArray ++ Array[Header](
new RecordHeader("processing-time", ???),
new RecordHeader("hexa-datetime", ???),
new RecordHeader("error-message", ???),
...
)
val producerRecord = new ProducerRecord(topic, /*same key, value and ts,*/ headers.asJava)
producer.send(producerRecord, /* Producer Callback */ )
DeserializationHandlerResponse.CONTINUE
}
63. 63@loicmdivad @PubSapientEng
Fill the headers with some meta data
01061696e0016536f6d6500000005736f6d65206f
Value message to hexa
Restaurant
description
Event date and time
Food order category
64. 64@loicmdivad @PubSapientEng
class DeadLetterQueueFoodExceptionHandler() extends DeserializationExceptionHandler {
var topic: String = _
var producer: KafkaProducer[Array[Byte], Array[Byte]] = _
override def configure(configs: util.Map[String, _]): Unit = ???
override def handle(context: ProcessorContext,
record: ConsumerRecord[Array[Byte], Array[Byte]],
exception: Exception): DeserializationHandlerResponse = {
val headers = record.headers().toArray ++ Array[Header](
new RecordHeader("processing-time", ???),
new RecordHeader("hexa-datetime", ???),
new RecordHeader("error-message", ???),
...
)
val producerRecord = new ProducerRecord(topic, /*same key, value and ts,*/ headers.asJava)
producer.send(producerRecord, /* Producer Callback */ )
DeserializationHandlerResponse.CONTINUE
}
65. 65@loicmdivad @PubSapientEng
class DeadLetterQueueFoodExceptionHandler() extends DeserializationExceptionHandler {
var topic: String = _
var producer: KafkaProducer[Array[Byte], Array[Byte]] = _
override def configure(configs: util.Map[String, _]): Unit = ???
override def handle(context: ProcessorContext,
record: ConsumerRecord[Array[Byte], Array[Byte]],
exception: Exception): DeserializationHandlerResponse = {
val headers = record.headers().toArray ++ Array[Header](
new RecordHeader("processing-time", ???),
new RecordHeader("hexa-datetime", ???),
new RecordHeader("error-message", ???),
...
)
val producerRecord = new ProducerRecord(topic, /*same key, value and ts,*/ headers.asJava)
producer.send(producerRecord, /* Producer Callback */ )
DeserializationHandlerResponse.CONTINUE
}
Take
Away
69. 69@loicmdivad @PubSapientEng 69@loicmdivad @PubSapientEng
Dead Letter Queue Pattern
You can provide your own implementation of
DeserializationExceptionHandler.
This lets you use the Producer API to write a
corrupted record directly to a quarantine topic.
Then you can manually analyse your corrupted
records
⚠Warning: This approach have side effects that are
invisible to the Kafka Streams runtime.
70. 70@loicmdivad @PubSapientEng 70@loicmdivad @PubSapientEng
Dead Letter Queue Pattern
You can provide your own implementation of
DeserializationExceptionHandler.
This lets you use the Producer API to write a
corrupted record directly to a quarantine topic.
Then you can manually analyse your corrupted
records
⚠Warning: This approach have side effects that are
invisible to the Kafka Streams runtime.
Take
Away
73. 73@loicmdivad @PubSapientEng 73@loicmdivad @PubSapientEng
Related Post
Kafka Connect Deep Dive – Error Handling and
Dead Letter Queues - by Robin Moffatt
Building Reliable Reprocessing and Dead Letter
Queues with Apache Kafka - by Ning Xia
Handling bad messages using Kafka's Streams API -
answer by Matthias J. Sax
74. 74@loicmdivad @PubSapientEng 74@loicmdivad @PubSapientEng
Conclusion
When using Kafka, deserialization is the
responsibility of the clients.
These internal errors are not easy to catch
When it’s possible, use Avro + Schema Registry
When it’s not possible, Kafka Streams applies
techniques to deal with serde errors:
- DLQ: By extending a ExceptionHandler
- Sentinel Value: By extending a Deserializer
76. 76@loicmdivad @PubSapientEng 76@loicmdivad @PubSapientEng
Images
Photo by rawpixel on Unsplash
Photo by João Marcelo Martins on Unsplash
Photo by Jordane Mathieu on Unsplash
Photo by Brooke Lark on Unsplash
Photo by Jakub Kapusnak on Unsplash
Photo by Melissa Walker Horn on Unsplash
Photo by Aneta Pawlik on Unsplash