История одного провала — Raft консенсус не хотите?

Всегда нужно уметь признавать свои ошибки. Вот и я недавно жутко оплашался. Так сильно, что аж стыдно было. Именно поэтому и появился данный пост.

Итак, сегодня не будет разглагольствования, просто сразу перейдем к делу. У меня была простая задача по обработке потока данных. Задачу можно сформировать так — есть датчики. Они генерируют упорядоченный набор событий. Нужно разработать горизонтально масштабируемую систему, которая будет обрабатывать события, сохраняя исходный порядок. Усложнение — каждый сигнал нужно обработать один раз.

Рассмотрим пример. Пускай в системе есть два датчика — А и B. Тогда эти датчики будут генерировать события A1, A2, A3, … , AN и B1, B2, … BN. Мы не имеем право обработать, например, сначала событие А2, а затем А1. Или, сначала B3, а затем B1. При этом, порядок обработки событий от разных датчиков не важен. То есть, можно сначала обработать, событие B2, а затем событие А1.

Какое наивное решение у этой задачи? Давайте обрабатывать поток событий в один поток на одном сервере приложения. Тогда порядок не будет нарушен, так как обработка данных происходит сериализованно. Конечно же, это решение не подходит под требование — горизонтальной масштабируемой системы. Поэтому идем дальше.

Мне почему-то в голову пришла идея, за которую мне стыдно и по сей день.

  • В системе есть Лидер. Выбор лидера реализуется каким-нибудь Консулом — https://www.consul.io/docs/guides/leader-election.html
  • Каждая Node — обработчик сигналов, должна зарегистрироваться у Лидера.
  • Лидер, используя Consistent hashing по типам датчиков, назначает каждой зарегистрированной Node-обработчику набор типов датчиков, которые нужно обрабатывать.
  • При смерте текущего Лидера, происходит выбор нового Лидера, и все Ноды повторно регистрируются.
  • Когда в кластер добавляется новая Нода, или уходит одна из существующих, Лидер перепосылает всем действующим Нодам новые наборы Событий, которые нужно обрабатывать.

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

К счастью, моё решение было благополучно выкинуто, и была использована Кафка, которая замечательно решает мою задачу.

  • Создадим топик, в который будут поступать все события ото всех датчиков. У топика нужно создать много Партиций (например, в два раза больше, чем максимально число Node-обработчиков).
  • Будем записывать события в данный топик, распределяя их по Партициям в зависимости от хэша.
  • Поднимем нужное количество консьюмеров. Важный факт — если консьюмеров будет меньше, чем количество Партиций, то Кафка просто решит эту задачу: некоторым Консьюмерам будет назначено несколько Партиций. Если консьюмеров будет больше, чем количество Партиций, то просто часть Консьюмеров будут стоять без дела, ожидая. когда какой-нибудь консьюмер сломается.
  • Если мы хотим high availability, то просто зафиксируем нужное количество Реплик для данного топика.
  • Если появляетя новая Node-обработчик, или пропадает одна из существующих Node, то просто Кафка выполняет Partitions Rebalance, и никто ничего не замечает.

Второе решение бьёт моё просто по всем фронтам. Оно стоит в миллион раз дешевле. Оно в десятки раз надежнее. Оно пишется в 10 строк на любом популярных языке.

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

Категории: Программирование