Загрузка
Все статьи

Kafka vs RabbitMQ — выбор брокера сообщений для Kotlin-микросервисов

При росте микросервисной архитектуры встал вопрос: Kafka или RabbitMQ? Оба брокера используются в Kotlin-экосистеме, но выбор между ними — это не про «какой лучше», а про «какая проблема у вас». Рассказываю на опыте реального проекта, где мы переписали messaging-слой с RabbitMQ на Kafka и что из этого вышло.

Полтора года назад я присоединился к проекту — Kotlin-микросервисы на Ktor, в проде около 20 сервисов. Messaging-слой был целиком на RabbitMQ. Всё работало, деплоились, жизнь была хороша. Пока нагрузка не выросла в 5 раз.

Начались проблемы: consumer lag, дублирование сообщений, потеря порядка. Я честно пытался починить RabbitMQ — пересматривал prefetch count, игрался с ack mode, ставил quorum queues. Но на определённом моменте стало ясно: мы упираемся в архитектурные ограничения брокера, а не в конфигурацию.

В этой статье я расскажу, чем Kafka и RabbitMQ отличаются на практике (не из документации, а из боли), покажу реальный код миграции на kafka-clients и дам честное сравнение — без холивара.

Как у нас был устроен RabbitMQ

Архитектура классическая: service A шлёт сообщения в exchange, routing key определяет очередь, service B потребляет. Для RPC использовали reply-to. Всё это обёрнуто в удобную обёртку на Kotlin:

class RabbitPublisher(
    private val channel: Channel,
    private val objectMapper: ObjectMapper
) {
    fun publish(event: OrderCreatedEvent, routingKey: String) {
        val message = objectMapper.writeValueAsBytes(event)
        channel.basicPublish(
            "orders-exchange",
            routingKey,
            AMQP.BasicProperties.Builder()
                .contentType("application/json")
                .deliveryMode(2) // persistent
                .build(),
            message
        )
    }
}

Выглядело нормально. Но когда количество сообщений перевалило за 50k/мин, начали сыпаться тревоги: consumer lag рос, prefetch был либо слишком маленький (тормозило), либо слишком большой (OOM на consumers). А ещё мы столкнулись с тем, что порядок сообщений внутри одной очереди сохранялся, но при fanout на несколько очередей — уже нет.

Где RabbitMQ начал нас подводить

Три конкретные проблемы, которые стали триггером для миграции.

Consumer lag при пиковых нагрузках. У нас есть биллинг-сервис, который обрабатывает платежные события. В конце месяца — всплеск в 10x. RabbitMQ consumer не успевал, очередь росла. Мы добавляли инстансов, но ramp-up времени consumer'а составлял 30-40 секунд, а за это время в очереди накапливались миллионы сообщений.

Нет rebalancing'а как такового. В Kafka, если вы добавляете consumer в group, партиции перераспределяются. В RabbitMQ если у вас одна очередь и несколько consumer'ов — они делят сообщения round-robin, но нет концепции «эта партиция за этим consumer'ом навсегда». Это значит, что при добавлении consumer'а нет локальности кэша.

Replay сообщений. Для investigative purposes нам нужно было переигрывать события за определённый период. В RabbitMQ это боль — нужно либо дублировать в отдельную «archive» очередь, либо использовать shovel plugin. В Kafka это просто: consumer.seek(offset).

Миграция на Kafka: код

Мы решили мигрировать поэтапно: сначала продюсеры, потом консьюмеры. Для Kafka используем официальную библиотеку kafka-clients — она стабильно работает с Kotlin без обёрток.

Producer

class KafkaEventPublisher(
    private val producer: KafkaProducer
) {
    fun publish(event: OrderCreatedEvent) {
        val key = event.orderId.toString()
        val value = objectMapper.writeValueAsBytes(event)
        val record = ProducerRecord(
            "order-events",
            key,
            value
        )
        producer.send(record) { meta, ex ->
            ex?.let {
                logger.error("Failed to publish event", it)
                producer.send(ProducerRecord("order-events-dlq", key, value))
            }
        }
    }
}

Consumer

class OrderEventConsumer(
    private val consumer: KafkaConsumer,
    private val orderService: OrderService
) {
    fun start() {
        consumer.subscribe(listOf("order-events"))
        while (true) {
            val records = consumer.poll(Duration.ofMillis(100))
            for (record in records) {
                val event = objectMapper.readValue(
                    record.value(), OrderCreatedEvent::class.java
                )
                try {
                    orderService.handleEvent(event)
                    consumer.commitSync()
                } catch (e: Exception) {
                    logger.error("Processing failed for ${record.key()}", e)
                    sendToDlq(record)
                }
            }
        }
    }
}

Обратите внимание: мы делаем commitSync() после каждого сообщения. В проде это можно оптимизировать через commitAsync() с batch'ем, но для начала sync-вариант надёжнее — вы не потеряете сообщение при краше, максимум обработаете дважды (idempotency — ваш друг).

Что изменилось после миграции

Consumer lag при пиковых нагрузках. Раньше: 2-3 часа отставания. Сейчас: максимум 2-3 минуты. Kafka позволяет потреблять параллельно по партициям — мы разбили топик order-events на 12 партиций, и 6 consumer'ов в группе спокойно справляются с пиком.

Replay стал тривиальным. Нужно переиграть события за 1 мая? Поднимаем consumer с нужным offset. Раньше это был отдельный механизм с архивной очередью и custom shovel-скриптами.

Порядок сообщений. В Kafka порядок гарантируется внутри одной партиции. Мы используем orderId как ключ партиционирования — значит все события одного заказа обрабатываются в порядке. В RabbitMQ мы имели порядок только в рамках одной очереди, но при fanout на несколько сервисов порядок терялся.

Честное сравнение

Вот таблица, которую я составил по итогам миграции. Не из документации — из реального опыта.

RabbitMQ лучше для: task queues, RPC-паттернов, когда нужен сложный routing (headers exchange, topic с wildcard), когда у вас до 10k сообщений/мин и нет жёстких требований к replay. Если вы строите event-driven архитектуру с сотнями тысяч событий — RabbitMQ будет сопротивляться.

Kafka лучше для: event streaming, когда важен порядок внутри группы, когда нужен replay и long-term storage событий, когда у вас высокие объёмы и нужно горизонтальное масштабирование consumers.

Мы не отказались от RabbitMQ полностью — он остался для RPC и task queues (например, отправка email-уведомлений). Kafka забрал event streaming: доменные события, аудит-лог, интеграционные события между сервисами.

Итог

Если бы я выбирал брокер сегодня — я бы задал себе три вопроса. Нужен ли мне replay? Нужен ли мне порядок? Какой объём сообщений ожидается? Если хотя бы на два ответа «да» — Kafka. Если на все три «нет» — RabbitMQ будет проще и дешевле в эксплуатации. Идеальный вариант, который мы в итоге реализовали — использовать оба: Kafka для event streaming, RabbitMQ для task distribution и RPC. Каждый инструмент решает свою задачу.

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

Micronaut vs Spring Boot — лёгкий фреймворк для Kotlin-микросервисов

Сравниваю Micronaut и Spring Boot для Kotlin-микросервисов: стартовое время, потребление памяти, холодный старт и реальный опыт миграции одного из сервисов.

12 июня 2026 11 мин

Kotlin: sealed interfaces и when-выражения — паттерн-матчинг как в Rust

Как sealed interfaces из Kotlin 1.5+ вместе с when-выражениями дают мощный паттерн-матчинг, похожий на Rust enum+match, и почему мы перешли на ADT для доменных событий.

11 июня 2026 9 мин

Kotlinx Serialization + Protobuf — быстрая сериализация для микросервисов

Разбираюсь с накладными расходами JSON-сериализации при высоких нагрузках и показываю, как интегрировать Protobuf с kotlinx-serialization в микросервисной архитектуре на Kotlin.

10 июня 2026 10 мин