Status

Current state: "Under Discussion"

Discussion thread: here

JIRA: here

Please keep the discussion on the mailing list rather than commenting on the wiki (wiki discussions get unwieldy fast).

Motivation

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.

Popular Patterns to solve this

At the moment of writing this KIP, there are two famous patterns to handle without increasing message.max.bytes

1. Message Chunking (Splitting and Reassembly message)

Break large messages into smaller chunks, send them sequentially, and reassemble on the consumer side.

2. Reference-Based Messaging

Store the large payload externally and send only a reference (e.g URI, databases key) in the Kafka message.

PatternProsCons
ChunkingNo external storage required Complex client logic to split and reassemble messages
Reference-BasedMinimizes Kafka loadExternal system dependency

This KIP is proposing

  1. A serializer in Apache Kafka that implements Reference-Based Messaging as this is the simplest one.
  2. A notion of a Composable  serializer where the client can apply a list of serializers before applying a large message serializer

Public Interfaces

1. Composable Serializer/Deserializer

In 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 proposing a composable serializer, that still implements the Kafka Serializer interface, but allows concatenating several serializers to perform what we are looking for here. 

2. LargeMessageSerializer

A configurable Kafka serializer that:

3. LargeMessageDeserializer

A configurable Kafka deserializer that performs:

Proposed Changes

1. Composable Serializer/Deserializer

Configuration

KeyDescriptionDefaultRequired
value.serializersList 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[]>`.NoneYes
value.deserializersList of deserializer classes 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[]>`.NoneYes

2. LargeMessageSerializer

Configuration

KeyDescriptionDefaultRequired
large.message.blob.store.classImplementation of org.apache.kafka.common.serialization.largemessage.blobstore.BlobStore .NoneYes
large.message.blob.store.timeout.msTimeout for blob store operations. This can't exceed max.block.ms for Kafka producers or max.poll.interval.ms in Kafka consumer.10000No
large.message.blob.store.retry.countNumber of retries for blob store operations.5No
large.message.blob.store.retry.max.backoff.msBackoff time between retries for blob store operations.10000No
large.message.blob.store.retry.delay.backoff.msDelay time between retries back off for blob store operations.100No
large.message.blob.store.blob.id.generatorImplementation 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.DefaultBlobIDGeneratorNo
large.message.threshold.bytes The maximum size of the message is considered large.1MB (Default of max.message.bytes)No

3. LargeMessageDeserializer

Configuration

KeyDescriptionDefaultRequired
large.message.blob.store.classImplementation of org.apache.kafka.common.serialization.largemessage.blobstore.BlobStore .NoneYes
large.message.blob.store.timeout.msTimeout for blob store operations. This can't exceed max.block.ms for Kafka producers or max.poll.interval.ms in Kafka consumer.10000No
large.message.blob.store.retry.countNumber of retries for blob store operations.5No
large.message.blob.store.retry.max.backoff.msBackoff time between retries for blob store operations.10000No
large.message.blob.store.retry.delay.backoff.msDelay time between retries back off for blob store operations.100No
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.FALSENo




4. BlobStore

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);
}

5. BlobIdGenerator

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);
}

6. LargeBlobMessage

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);
}

7. BlobResponse

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;
    }
}

Example

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);

Compatibility, Deprecation, and Migration Plan

Rejected Alternatives