Елизавета Акманова

Ведущий аналитик

Хабр, всем привет! С вами Лиза Акманова, ведущий аналитик ГК «Юзтех».

Представьте идеальный мир: аналитик пишет ясное техническое задание, разработчик его реализует и всё работает безупречно. Но когда в проекте появляется Kafka, эта идиллическая картина часто рассыпается. Почему? Потому что существуют технические нюансы, которые аналитик может не заметить, а разработчик не озвучить. Давайте вместе разберемся с этими “граблями” и научимся их обходить.

Грабли №1: Предположение о гарантированном порядке событий

Многие аналитики исходят из того, что события в системе всегда обрабатываются в строгом порядке их поступления. Однако в Kafka это правило работает иначе. Здесь есть фундаментальное ограничение: события гарантированно упорядочены только в рамках одной партиции.

Как на самом деле работает распределение данных

По умолчанию сообщения распределяются по принципу round-robin: первое сообщение в первую партицию, второе во вторую, и так по кругу. Отличное решение для равномерного распределения нагрузки, но катастрофическое для сохранения порядка связанных событий.

Реальный пример из практики

Рассмотрим ситуацию с онлайн-платежами. Пользователь совершает оплату заказа, но потом передумывает и запрашивает возврат. В идеале система должна сначала обработать платеж, и только после этого возврат. Но в реальности может произойти вот что:

Событие платежа попадает в перегруженную партицию, где уже скопилась очередь сообщений. Оно ждет своей очереди на обработку. Тем временем событие возврата направляется в свободную партицию и мгновенно доходит до системы-получателя. Обработав возврат, система не находит соответствующего платежа и игнорирует операцию. Когда же доходит очередь до исходного платежа, деньги успешно списываются. Результат: клиент теряет деньги, а компания репутацию.

Решение: правильное партицирование

Ключ к решению этой проблемы – в правильном выборе ключа партицирования. Аналитику необходимо обязательно обсудить с разработчиками, по какому принципу будут распределяться сообщения между партициями. Для операций, где критически важен порядок обработки, все связанные события должны попадать в одну партицию.

Как пример реализации: все события в рамках одного заказа должны использовать order_id в качестве ключа партицирования. Это гарантирует, что платеж и возврат, относящиеся к одному заказу, будут обрабатываться последовательно.

Грабли №2: Когда одно сообщение приходит дважды

Существует ложное предположение, что каждое событие доставляется и обрабатывается ровно один раз. К сожалению, в распределенных системах это не так. В Kafka можно настроить механизмы доставки "at-least-once" (минимум один раз), что означает: в случае сбоев сообщения могут доставляться повторно. 

Что такое идемпотентность и почему она важна

Идемпотентность — это свойство операции, при котором ее многократное выполнение дает тот же результат, что и однократное. Проще говоря, если вы выполнили операцию один раз, а затем выполнили ее еще десять раз — в системе должно остаться так, как будто вы сделали это только один раз. Аналог в математике: умножение на 0. Сколько бы раз вы одно и то же число не умножали на 0, результат как и в первый раз. Чаще всего я слышу про идемпотентность в части проектирования API, но и при проектировании брокеров об этом также нужно помнить.

Реальный пример: двойное списание платежа

Представьте ситуацию: пользователь оплачивает заказ, но из-за проблем с сетью его браузер отправляет запрос дважды. Оба сообщения "оплата заказа №123" попадают в Kafka и обрабатываются. Если система не идемпотентна, происходит двойное списание денег и пользователь платит дважды за один товар.

Другой пример, реже встречается, но имеет место быть. Консьюмер может прочитать сообщение, обработать его, но не сказать об этом Kafka (задержка по сети, отвалилось соединение, что угодно). При рестарте такого сервиса по логике он запросит номер последнего прочитанного сообщения, и тут появляется проблема: Kafka даст позицию уже обработанного события. 

Как решить проблему дублей

Решение заключается в том, чтобы принимать дубли как нормальную часть работы распределенной системы и учиться их корректно обрабатывать:

  • Всегда ищите в данных уникальные идентификаторы сообщений (message_id, event_id)

  • Реализуйте механизм проверки на дубли перед выполнением операций

  • Используйте идемпотентные операции там, где это возможно

!!!Важное предостережение!!!

Следует помнить о производительности: каждая проверка на дубли требует дополнительных запросов к базе данных или кешу. Поэтому важно находить баланс — проверять только те операции, где дубли действительно критичны. Например, для финансовых транзакций проверка необходима, а для сбора статистики, возможно, избыточна.

Грабли №3: «Потерянные события: они просто исчезли?»

Аналитики иногда предполагают, что “данные попали в Kafka, значит мы их точно обработаем”. К сожалению, это не так. В Kafka существует политика автоматической очистки данных (retention.ms), которая удаляет сообщения по истечении установленного срока.

Реальный пример: потеря данных из-за "скукоживания" партиций

В одном из проектов мы столкнулись с неприятной ситуацией… Мне очень понравилась формулировка нашего разработчика, что Kafka неожиданно "скукожилась" до одной работающей партиции из-за проблем с кластером. Оставшаяся партиция не справлялась с нагрузкой, сообщения начали накапливаться, и к моменту восстановления кластера срок хранения для значительной части данных уже истёк.

Мы обнаружили проблему лишь когда половина ожидаемых сообщений так и не была доставлена и они были безвозвратно удалены системой. Мониторинг, судя по всему, не был должным образом настроен и не показал критических ошибок, так как Kafka формально работала исправно, просто в соответствии со своими настройками очистила "устаревшие" данные.

Как решить проблему потери данных

Решение заключается в тщательном планировании политик хранения данных:

  • Всегда согласовывайте требования к хранению данных с DevOps и архитекторами не только при положительном и штатном сценарии, но и на случай падений и отказов

  • Устанавливайте retention.ms с запасом, учитывая возможные сбои и периоды простоя систем

  • Настройте мониторинг lag (отставания) консьюмеров и количества активных партиций

  • Для критически важных данных используйте длительные сроки хранения или архивацию

Грабли №4: “Схема данных? Нет, не слышали”

Все мы знаем, что данные меняются: добавляются новые параметры, удаляются старые, обязательность также не постоянна. Как версионировать API уже все знают, а как версионировать… топик? Или партицию? Что делать?

Вполне себе может быть ситуация, когда в production-топике одновременно оказались сообщения в старой и новой версии схем. Или еще лучше: web отправлял данные в одном формате, mobile в другом, а backend должен уметь корректно обрабатывать и те, и другие данные.

Как решить проблему управления схемами

Решение заключается во внедрении централизованного контроля версий схем, а именно Schema Registry. Это отдельный сервис, который выполняет роль “библиотеки договоренностей” для ваших данных в Kafka. Когда разные сервисы (продюсеры и консьюмеры) общаются через Kafka, Schema Registry гарантирует, что все они говорят на “одном языке”. Он хранит официальные версии форматов данных (схем) и не позволяет отправить в Kafka сообщения, которые получатели не смогут понять.

Как это работает на практике

  • Продюсер перед отправкой сообщения в Kafka спрашивает у Schema Registry: “Я хочу отправить данные вот в таком формате, это разрешено?”

  • Schema Registry проверяет схему на совместимость с предыдущими версиями. Если все в порядке, он регистрирует новую версию и разрешает отправку.

  • Консьюмер, получая сообщение, может обратиться к Schema Registry, чтобы точно понять, в каком формате ему пришли данные.

Грабли №5: «Консьюмер отстал, и мы этого не заметили»

Мы подобный кейс уже разбирали в граблях #3, но сейчас давайте подробнее. К сожалению, консьюмеры могут незаметно отставать на часы или даже дни, и вы будете работать с устаревшей информацией, принимая решения на основе вчерашних данных.

Что такое Lag и почему он важен

Consumer Lag — это разница между последним сообщением в партиции Kafka и последним обработанным сообщением. Проще говоря, это количество сообщений, которые уже поступили, но еще не обработаны. Крайне важный параметр, который надо мониторить с особой осторожностью. Как именно? 

  • Настройте мониторинг Lag для всех критичных консьюмеров

  • Установите пороговые значения — при каком Lag нужно бить тревогу

  • Создайте дашборды с визуализацией Lag в реальном времени

  • Автоматизируйте алерты при превышении допустимых значений

Выводы

Работа с Kafka напоминает сборку сложного механизма: если не проверить каждую деталь, система даст сбой в самый неподходящий момент. Делюсь с вами чек-листом из 5-ти основных пунктов, который поможет избежать катастрофы.

  1. Всегда уточняйте ключ партицирования – это гарантирует порядок обработки связанных событий.

  2. Заранее предполагайте дубли – идемпотентность ваших операций спасет от двойных списаний и повторных действий.

  3. Контролируйте схему данных – это предотвратит ситуацию, когда сервисы перестанут понимать друг друга после обновлений.

  4. Согласуйте политики хранения – так вы избежите безвозвратной потери данных при сбоях.

  5. Мониторьте lag консьюмеров – это единственный способ быть уверенным, что ваша аналитика работает с актуальными данными.

Этот чек-лист можно назвать практическим инструментом для построения надежных ИТ решений. Следуя пунктам, вы превращаетесь из наблюдателя в архитектора системы, где данные работают на бизнес, а не создают скрытые проблемы.