ZIO Kafka Documentation

repository·master·Indexed 18 days ago

https://github.com/zio/zio-kafka

A purely functional, streams-based Kafka client for ZIO. It provides high-level APIs for producing, consuming, and managing Kafka resources, integrating with ZIO and zio-streams for high-throughput, declarative event streaming. Features include parallel partition processing, a ZIO-based Admin API, and a dedicated testkit for testing purposes.

Tokens
43.1K
Snippets
122
Records
150
Agent score
63%

What's inside zio-kafka

  1. Overview of ZIO Kafka Features

    master

    ZIO Kafka is a ZIO-native client for Apache Kafka that provides a high-level, purely functional, and streams-based interface. It wraps the Java Kafka client to offer declarative concurrency via zio-streams.

    Key Capabilities

    Consuming:

    • Two API Styles: Streaming API and ZIO workflow-based API.
    • High Throughput: Supports parallel partition processing, batched processing, and configurable per-partition pre-fetching with back-pressure.
    • Reliability: The only async Kafka consumer (as far as known) that prevents duplicates after a rebalance. Supports automatic or manual starting offsets and external commits.
    • Error Handling: Retries after authentication/authorization errors and detects silent authorization failures (e.g., when a READ ACL is revoked at runtime).
    • Observability: Exposes metrics and a diagnostics API.

    Producing:

    • Two API Styles: Streaming API and ZIO workflow-based API.
    • High Throughput: Supports batching for maximum performance.
    • Customization: Supports custom serialization and optional broker acknowledgements.
    • Reliability: Optional retries after authentication/authorization errors.
    • Observability: Exposes metrics.

    Admin API:

    • Exposes all Java Admin client methods through a ZIO-based interface.
    • Provides safer operations by waiting for the operation to complete before returning.
  2. Commit offsets efficiently in ZIO Streams

    master

    When consuming records, you must commit the offset of a CommittableRecord to ensure it isn't re-consumed after a crash or restart.

    While you can commit offsets individually using record.offset.commit, this is inefficient for high-volume streams. Instead, use ZStream#aggregateAsyncWithin combined with the Consumer.collectOffsets sink to aggregate offsets into an OffsetBatch and commit them in batches at a fixed interval.

    consumer
      .plainStream(Subscription.topics(KAFKA_TOPIC), Serde.int, Serde.string)
      .map(_.offset)
      .aggregateAsyncWithin(Consumer.collectOffsets, Schedule.fixed(100.millis))
      .mapZIO(_.commit)
      .runDrain
  3. Performance characteristics of zio-kafka

    master

    zio-kafka programs often achieve higher throughput than programs using the standard java-kafka client directly because zio-kafka processes partitions in parallel by default.

    Key Performance Notes:

    • Parallelism: zio-kafka processes partitions in parallel, whereas the default java-kafka client does not.
    • Throughput Threshold: As of late 2024, zio-kafka is estimated to consume faster than the java-kafka client when processing takes more than approximately 1.2ms per 1000 records. The exact threshold depends on various factors.
    • Latency vs. Throughput: If your application requires absolute minimum latency and you do not need the ZStream-based API, using the Java-based Kafka client directly may be preferable.
  4. Understand Serialization and Deserialization (Serdes) in ZIO Kafka

    master

    ZIO Kafka handles the conversion between raw byte arrays and high-level Scala types for both Kafka keys and values.

    • Producer: Requires a Serializer for both keys and values to convert types to byte arrays.
    • Consumer: Requires a Deserializer for both keys and values to convert byte arrays to types.
    • Serde: A Serde[K, V] is a combined object that provides both a Serializer and a Deserializer for a specific key and value type.

    Common pre-defined serdes are available in the Serdes object, such as Serdes.byteArray, Serdes.string, and Serdes.long.

  5. Avoid chunk-breaking operators in ZIO Kafka streams

    master

    ZIO Kafka optimizes throughput by grouping records fetched from a broker into chunks. Using operators like mapZIO or tap can break this structure, resulting in a stream where every chunk contains only a single element, significantly reducing performance.

    To identify chunk-breaking operators in Scaladoc, look for the warning: This combinator destroys the chunking structure.

    Important Note on Execution Order: When moving from chunk-breaking operators to chunk-preserving ones, the evaluation order changes:

    • Chunk-breaking (mapZIO(f).mapZIO(g)): f(a), g(a), f(b), g(b)
    • Chunk-preserving (Alternatives): f(a), f(b), g(a), g(b)

    If g(a) fails in the chunk-preserving model, f(b) will have already been executed. Ensure this change in semantics is acceptable for your application.

  6. Avoid non-deterministic partition assignment with overlapping subscriptions

    master
    If your streams use overlapping subscriptions (e.g., Subscription.topics("topic1", "topic2") and Subscription.topics("topic2", "topic3")), the assignment of partitions for the overlapping topic (topic2) is non-deterministic. You cannot guarantee which stream will receive the records for the shared topic.
  7. How Serde works for Kafka data

    master

    Kafka stores records as raw bytes. To work with typed data, ZIO Kafka uses the Serde[R, A] type, which combines a Serializer and a Deserializer for type A within an environment R.

    trait Serde[-R, A] {
      def deserialize(data: Array[Byte]): RIO[R, A]
      def serialize(value: A)           : RIO[R, Array[Byte]]
    }

    The Serde companion object provides built-in serializers/deserializers for primitive types:

    • Serde.long
    • Serde.int
    • Serde.short
    • Serde.float
    • Serde.double
    • Serde.boolean
    • Serde.string
    • Serde.byteArray
    • Serde.byteBuffer
    • Serde.uuid
  8. Create custom Serde instances using combinators

    master

    In zio-kafka, all serializers and deserializers are instances of the Serde trait. You can create complex Serde instances by transforming existing ones using two primary combinators:

    • inmap: Performs pure transformations from the source type to the target type and back. Use this when the transformation does not require side effects or error handling that involves ZIO.
    • inmapZIO: Performs effectful transformations. This is useful when parsing might fail and you want to encode those failures as ZIO errors (e.g., using ZIO.fromEither).
    // Example: Pure transformation using inmap
    import java.time.Instant
    val instantSerde: Serde[Any, Instant] =
      Serde.long.inmap[Instant](Instant.ofEpochMilli)(_.toEpochMilli)
    
    // Example: Effectful transformation using inmapZIO
    // Useful for JSON parsing where errors must be handled in ZIO
    val eventSerde: Serde[Any, Event] =
      Serde.string.inmapZIO[Any, Event](s =>
        ZIO.fromEither(s.fromJson[Event]).mapError(e => new RuntimeException(e))
      )(r => ZIO.succeed(r.toJson))
  9. Understand producer benchmark methodologies

    master

    Producer benchmarks test various patterns of record production. Common patterns include:

    • Batch Production: Producing batches of records (e.g., 30 batches of 500 small records).
      • produceChunkSeq: Sequential production.
      • produceChunkSeqAsync: Sequential production where the client does not wait for acknowledgements.
      • produceChunkPar: Production using 4 concurrent fibers.
    • Single Record Production: Producing individual records (e.g., 100 small records) with lingering disabled.
      • produceSingleRecordSeq: Sequential production.
      • produceSingleRecordSeqAsync: Sequential production without waiting for acknowledgements.
      • produceSingleRecordPar: Production using 4 concurrent fibers.
  10. Manage memory usage for high partition counts

    master

    When consuming from a large number of partitions, the heap usage increases because each partition maintains a record queue.

    A rough estimate for required heap is: average record size * number of partitions * max(partitionPreFetchBufferLimit, max.poll.records)

    To reduce memory pressure:

    1. Decrease partitionPreFetchBufferLimit or max.poll.records.
    2. Use a custom FetchStrategy, such as ManyPartitionsQueueSizeBasedFetchStrategy (available since version 2.8.1).
  11. Replace restartStreamOnRebalancing with rebalanceSafeCommits

    master

    The restartStreamOnRebalancing mode is no longer supported in zio-kafka 3. This mode previously ended all streams during a rebalance, even if their specific partitions were not revoked.

    In zio-kafka 3, transactional consuming no longer requires this mode. If your goal is to prevent duplicate processing, use rebalanceSafeCommits instead.

  12. Understand consumer benchmark methodologies

    master

    The consumer benchmarks in this project are designed to represent a 'worst-case' scenario for zio-kafka to ensure fair comparison with standard Kafka clients. Because these benchmarks only count received records without any processing, they do not leverage zio-kafka's ability to process records in parallel, which can make the throughput appear lower than in real-world usage.

    All consumer benchmarks send approximately 50,000 records of 512 bytes each per run.

    Key benchmark types:

    • throughput: Uses plainStream with a topic subscription; offsets are not committed.
    • throughputWithCommits: Uses plainStream with a topic subscription; offsets are committed.
    • manual variants: Use partition assignment instead of topic subscription.