Как организовать сервис для приема, хранения и быстрого возврата данных из Kafka с высокой производительностью
Что проверяют
Умеете ли вы начать с требований и разделить три задачи: быстро читать из Kafka, надёжно писать в хранилище и быстро отдавать данные клиенту. Интервьюер ждёт цифры и компромиссы, а не список технологий. Следом обычно спрашивают, как не потерять и не задвоить сообщения при падении сервиса.
Как отвечать
Сначала уточните: сколько сообщений в секунду и какого размера, как будут читать (по ключу, последнее состояние, диапазон по времени), какая задержка допустима и сколько хранить. От этого зависит выбор хранилища.
Каркас решения:
- параллелизм: консьюмеров в группе полезно не больше, чем партиций в топике, лишние простаивают. Партиции закладывайте с запасом: при добавлении ключи перераспределятся и порядок по ключу нарушится;
- пакетное чтение: в Spring Kafka батч-листенер (
spring.kafka.listener.type=batch), настройкиmax.poll.records,fetch.min.bytes,fetch.max.wait.ms; - пакетная запись: JDBC batch или
COPYв PostgreSQL, Cassandra для записи по ключу на большом объёме, ClickHouse для аналитических выборок; - коммит после записи:
enable.auto.commit=false,AckMode.BATCHилиMANUAL. Это даёт доставку «хотя бы раз», а дубли гасит идемпотентный upsert по ключу; - быстрая отдача: читать не из Kafka, а из подготовленной модели для чтения. По ключу это Redis или Kafka Streams с хранилищем состояния на RocksDB и interactive queries, для последнего состояния по ключу подходит компактируемый топик (
cleanup.policy=compact); - продюсер:
acks=all,enable.idempotence=true,linger.msиbatch.sizeдля укрупнения пачек, сжатиеlz4илиzstd.
Завершают ответ мониторинг лага консьюмеров через Micrometer и Prometheus и нагрузочный тест в Gatling.
Где спотыкаются
Добавляют консьюмеров сверх числа партиций и удивляются, что пропускная способность не растёт.
Пишут в базу по одной записи и коммитят каждый офсет отдельно. Узким местом становятся сетевые круги и транзакции, а не Kafka.
Делают тяжёлую обработку прямо в листенере. Если между вызовами poll() проходит больше max.poll.interval.ms, консьюмера выкидывают из группы, начинается ребалансировка, и сообщения обрабатываются повторно.