Current state: "Under Discussion"
Discussion thread: here [Change the link from the KIP proposal email archive to your own email thread]
JIRA: here
Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).
Kafka has a limit for message size which limits some use cases where they might have messages that are larger than message.max.bytes even after enabling compression on the producer side or after applying serialization formats like Apache Avro or Protocol Buffers to reduce payload size. Increasing message size indefinitely is not a viable solution as it can lead to performance degradation, memory issues, and instability of the broker. And by looking at some of the enterprise/cloud offerings of Kafka you can see that on average they can offer 8MB to 10MB as max message size.
At the moment of writing this KIP, there are two famous patterns to handle without increasing message.max.bytes
Break large messages into smaller chunks, send them sequentially, and reassemble on the consumer side.
message.max.bytes.Store the large payload externally and send only a reference (e.g URI, databases key) in the Kafka message.
s3://bucket/key) via Kafka.| Pattern | Pros | Cons |
|---|---|---|
| Chunking | No external storage required | Complex client logic to split and reassemble messages |
| Reference-Based | Minimizes Kafka load | External system dependency |
Composable serializer where the client can apply a list of serializers before applying a large message serializerIn some use-cases we might need a way to be able to chain of Kafka serializer/deserializer before applying large message serializer for example need to apply schema or special format like avro or protobuf. We could just create a single LargeMessageSerializer that implemented the Kafka Serializer interface, but we would need to create the different versions (AvroSerializer, ProtobufSerializer) with their support for the schema.
Instead the KIP is proposing idea of having a composable serializer, that still implements the Kafka Serializer interface, but allows concatenating several serializers to perform what we are looking for here.
A configurable Kafka serializer that:
large-message: true headerA configurable Kafka deserializer that performs:
large-message: true header| Key | Description | Default | Required |
| value.serializers | List of serializers class that implements the `org.apache.kafka.common.serialization.Serializer` interface in order. The first serializer is `Serializer<T>` while the remaining serializers must be `Serializer<byte[]>`. | None | Yes |
| value.deserializers | List of deserializes class that implements the `org.apache.kafka.common.serialization.Deserializer` interface in order. The last deserializer must be `Deserializer<T>` while the rest must be `Deserializer<byte[]>`. | None | Yes |
| Key | Description | Default | Required |
|---|---|---|---|
large.message.blob.store.class | Implementation of org.apache.kafka.common.serialization.largemessage.blobstore.BlobStore . | None | Yes |
large.message.blob.store.timeout.ms | Timeout for blob store operations. This can't exceed max.block.ms for Kafka producers or max.poll.interval.ms in Kafka consumer. | 10000 | No |
large.message.blob.store.retry.count | Number of retries for blob store operations. | 5 | No |
large.message.blob.store.retry.max.backoff.ms | Backoff time between retries for blob store operations. | 10000 | No |
large.message.blob.store.retry.delay.backoff.ms | Delay time between retries back off for blob store operations. | 100 | No |
large.message.blob.store.blob.id.generator | Implementation of org.apache.kafka.common.serialziation.largemessage.blobstore.BlobIdGenerator interface. This provides a way for the serializer to generate an identifier for the message in the store. | org.apache.kafka.common.serialization.largemessage.blobstore.BlobIdGenerator.DefaultBlobIDGenerator | No |
large.message.threshold.bytes | The maximum size of the message is considered large. | 1MB (Default of max.message.bytes) | No |
| Key | Description | Default | Required |
|---|---|---|---|
large.message.blob.store.class | Implementation of org.apache.kafka.common.serialization.largemessage.blobstore.BlobStore . | None | Yes |
large.message.blob.store.timeout.ms | Timeout for blob store operations. This can't exceed max.block.ms for Kafka producers or max.poll.interval.ms in Kafka consumer. | 10000 | No |
large.message.blob.store.retry.count | Number of retries for blob store operations. | 5 | No |
large.message.blob.store.retry.max.backoff.ms | Backoff time between retries for blob store operations. | 10000 | No |
large.message.blob.store.retry.delay.backoff.ms | Delay time between retries back off for blob store operations. | 100 | No |
| Implementation of org.apache.kafka.common.serialziation.largemessage.blobstore.BlobIdGenerator iinterface. This provides a way for the serializer to generate an identifier for the message in the store. | org.apache.kafka.common.serialization.largemessage.blobstore.BlobIdGenerator.DefaultBlobIDGenerator | No |
| `large.message.skip.not.found.error` | Skip not found error when the blob is not found in the store. This allows the deserializer to skip not-found messages and return empty bytes instead. This is important when external store ttl is smaller than kafka retention. | FALSE | No |
public abstract class BlobStore implements Configurable, Closeable {
@Override
public void configure(Map<String, ?> configs) {
// configure
}
/**
* Upload data into object store.
*
* @param data data that will be uploaded into the object.
* @return {@link BlobResponse}.
*/
public abstract BlobResponse putObject(String topic, byte[] data);
/**
* Download object from object store.
*
* @param blobPath id of the object in blobstore
* @return content of the object as bytes.
*/
public abstract byte[] getObject(String blobPath);
} |
public interface BlobIdGenerator extends Configurable {
BlobIdGenerator DEFAULT_BLOB_ID_GENERATOR = new BlobIdGenerator() {
@Override
public void configure(Map<String, ?> configs) {
// Nothing to do with config
}
@Override
public String id(byte[] data) {
return UUID.randomUUID().toString();
}
};
/**
* generate blob id.
*/
String id(byte[] data);
} |
public interface LargeMessageFormatter<T> extends Configurable {
LargeMessageFormatter JSON = new LargeMessageFormatter() {
// implement a default json one
// format is {
// "full-blob-path": "<full-path-to-access-blob-as-string>",
// "blob-store-class": "<used-blob-store-class-path-to-upload-blob>",
// "blob-id-generator-class": "<class-used-to-generate-blob-id>",
// "large-message-formatter-class": "<formatter-class-path>"
// }
};
/**
* build message bytes from blob store response and data.
*/
byte[] messageBytes(byte[] data, BlobResponse response);
/**
* parse data into LargeBlobMessage.
*/
T largeBlobMessage(byte[] data);
} |
public class BlobResponse {
public final int responseCode;
public final String fullBlobPath;
public final BlobStoreException blobStoreException;
/**
* Construct blob response with response code and blob id.
*/
public BlobResponse(int responseCode, String fullBlobPath) {
this(responseCode, fullBlobPath, null);
}
/**
* Construct blob response with response code, blob id and exception.
*/
public BlobResponse(int responseCode, String fullBlobPath, BlobStoreException blobStoreException) {
this.responseCode = responseCode;
this.fullBlobPath = fullBlobPath;
this.blobStoreException = blobStoreException;
}
} |
Map<String, Object> producerConfig = new HashMap<>();
producerConfig.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
producerConfig.put("value.serializers",
"org.apache.kafka.common.serialization.DoubleSerializer,org.apache.kafka.common.serialization.LargeMessageSerializer");
producerConfig.put("large.message.blob.store.class", "CustomS3Store")
producerConfig.put("bootstrap.servers", "localhost:9092");
KafkaProducer<String, Double> producer = new KafkaProducer<>(producerConfig); |