Введение в потоковую передачу событий с помощью Kafka и Kafdrop

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

В этой статье исследуется фундаментальный строительный блок - потоковая передача событий. Возглавляет обвинение Apache Kafka - де-факто стандарт платформ потоковой передачи событий, который мы будем наблюдать через Kafdrop - многофункциональный веб-интерфейс.

Краткое введение

Платформы потоковой передачи событий относятся к более широкому классу ориентированного на сообщения промежуточного программного обеспечения (MoM) и аналогичны традиционным очередям сообщений и темам, но предлагают более надежные временные гарантии и, как правило, повышение производительности на порядок за счет неизменяемости структурированного журнала. Проще говоря, операции записи в основном ограничиваются последовательным добавлением, что делает их быстрыми.

В то время как сообщения в традиционной очереди сообщений (MQ) имеют тенденцию быть произвольно упорядоченными и обычно независимыми друг от друга, события (или записи) в потоке имеют тенденцию быть строго упорядоченными, часто хронологически или причинно. Кроме того, поток сохраняет свои записи, тогда как MQ отбрасывает сообщение, как только оно будет прочитано. По этой причине потоковая передача событий, как правило, лучше подходит для архитектур, основанных на событиях, включая источники событий, согласованность в конечном итоге и концепции CQRS. (Конечно, есть и очереди сообщений FIFO, но различия между очередями FIFO и полноценными платформами потоковой передачи событий весьма существенны и не ограничиваются только упорядочением.)

Платформы потоковой передачи событий - сравнительно недавняя парадигма в более широкой области MoM. Доступно лишь несколько основных реализаций по сравнению с сотнями брокеров в стиле MQ, некоторые из которых восходят к 1980-м годам (например, Tuxedo). По сравнению с установленными стандартами, такими как AMQP, MQTT, XMPP и JMS, в области потоковой передачи нет эквивалентных стандартов. Платформы потоковой передачи событий являются активной областью непрерывных исследований и экспериментов. Несмотря на это, потоковые платформы - это не просто нишевая концепция или академическая идея с несколькими эзотерическими вариантами использования; они могут быть эффективно применены к широкому спектру сценариев обмена сообщениями и событий, регулярно вытесняя свои более традиционные аналоги.

Обзор архитектуры

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

Kafka - это распределенная система, состоящая из нескольких ключевых компонентов:

  • Узлы-посредники: отвечают за большую часть операций ввода-вывода и устойчивую работу в кластере. Посредники размещают файлы журнала только для добавления, которые составляют разделы темы, размещенные в кластере. Разделы могут реплицироваться между несколькими брокерами как для горизонтальной масштабируемости, так и для повышения надежности - это называется репликами. Узел-брокер может выступать в качестве лидера для одних реплик, в то же время являясь последователем для других. Одиночный узел брокера также будет выбран в качестве контроллера кластера, отвечающего за внутреннее управление состояниями разделов. Это включает в себя арбитраж ролей лидер-последователь для любого данного раздела.
  • Узлы ZooKeeper. Внутри Kafka нужен способ управления общим статусом контроллера в кластере. Если контроллер выпадает по какой-либо причине, существует протокол для выбора другого контроллера из набора оставшихся брокеров. Фактическая механика выбора контроллера, биения сердца и т. Д. В значительной степени реализована в ZooKeeper. ZooKeeper также действует как своего рода репозиторий конфигурации, поддерживая метаданные кластера, состояния лидер-последователь, квоты, информацию о пользователях, списки управления доступом и другие вспомогательные элементы. Из-за лежащего в основе протокола сплетен и консенсуса количество узлов ZooKeeper должно быть нечетным.
  • Производители: клиентские приложения, отвечающие за добавление записей в темы Kafka. Из-за того, что Kafka имеет структуру журналов и возможность делиться темами в нескольких потребительских экосистемах, только производители могут изменять данные в базовых файлах журналов. Фактический ввод-вывод выполняется узлами-брокерами от имени клиентов-производителей. Любое количество производителей может публиковать в одной и той же теме, выбирая разделы, используемые для сохранения записей.
  • Потребители: клиентские приложения, которые читают по темам. Любое количество потребителей может читать по одной и той же теме; однако, в зависимости от конфигурации и группировки потребителей, существуют правила, управляющие распределением записей среди потребителей.

Темы, разделы, записи и смещения

Раздел - это полностью упорядоченная последовательность записей, которая лежит в основе Kafka. Запись имеет идентификатор - 64-битное целочисленное смещение и временную метку с точностью до миллисекунды. Также он может иметь ключ и значение; оба являются байтовыми массивами, и оба необязательны. Термин «полностью заказанный» просто означает, что для любого данного производителя записи будут записаны в том порядке, в котором они были отправлены приложением. Если запись P была опубликована до Q, тогда P будет предшествовать Q в разделе. (Предполагается, что P и Q совместно используют раздел.) Более того, они будут прочитаны в одном и том же порядке всеми потребителями; P всегда будет прочитан перед Q для всех возможных потребителей. Эта гарантия заказа жизненно важна в подавляющем большинстве случаев использования; опубликованные записи, как правило, соответствуют некоторым реальным событиям, и часто важно сохранить временную шкалу этих событий.

Примечание. Кафка использует термин «запись», где другие могут использовать «сообщение» или «событие». В этой статье мы будем использовать эти термины как синонимы, в зависимости от контекста. Точно так же вы можете увидеть термин «поток» как общий заменитель слова «тема».

Не существует признанного упорядочивания между производителями; если два (или более) производителя выпускают записи одновременно, эти записи могут материализоваться в произвольном порядке. При этом этот порядок по-прежнему будет соблюдаться единообразно для всех потребителей.

Смещение записи однозначно идентифицирует ее в разделе. Смещение представляет собой строго монотонно увеличивающееся целое число в разреженном адресном пространстве, что означает, что каждое последующее смещение всегда выше, чем его предшественник, и между соседними смещениями могут быть различные промежутки. Разрывы могут появиться законно, если уплотнение включено или в результате транзакций; нам не нужно вдаваться в подробности на этом этапе, достаточно сказать, что смещения не обязательно должны быть смежными.

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

На схеме ниже показано, как выглядит перегородка изнутри.

start of partition
+--------+-----------------+
|0..00000|First record     |
+--------+-----------------+
|0..00001|Second record    |
+--------+-----------------+
|0..00002|Third record     |
+--------+-----------------+
|0..00003|Fourth record    |
+--------+-----------------+
|0..00007|Fifth record     |
+--------+-----------------+
|0..00008|Sixth record     |
+--------+-----------------+
|0..00010|Seventh record   |
+--------+-----------------+
            ...
+--------+-----------------+
|0..56789|Last record      |
+--------+-----------------+
       end of partition

Начальное смещение, также называемое отметкой низкого уровня, является первым сообщением, которое будет представлено потребителю. Из-за ограниченного удержания Кафки это не обязательно первое опубликованное сообщение. Записи могут быть удалены в зависимости от времени и / или размера раздела. В этом случае метка минимального уровня будет отображаться смещением вперед, а записи до метки минимального уровня будут усечены.

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

Тема - это логическая композиция разделов. Тема может иметь один или несколько разделов, и раздел должен быть частью только одной темы. Темы имеют фундаментальное значение для Kafka, обеспечивая как параллелизм, так и балансировку нагрузки.

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

Пример: публикация сообщений

Давайте применим часть этой теории на практике. Мы собираемся развернуть пару контейнеров Docker - один для Kafka, другой для Kafdrop. Но вместо того, чтобы запускать их по отдельности, мы будем использовать Docker Compose.

Создайте docker-compose.yaml файл в выбранном вами каталоге, содержащий следующее:

version: "2"
services:
  kafdrop:
    image: obsidiandynamics/kafdrop
    restart: "no"
    ports:
      - "9000:9000"
    environment:
      KAFKA_BROKERCONNECT: "kafka:29092"
    depends_on:
      - "kafka"
  kafka:
    image: obsidiandynamics/kafka
    restart: "no"
    ports:
      - "2181:2181"
      - "9092:9092"
    environment:
      KAFKA_LISTENERS: "INTERNAL://:29092,EXTERNAL://:9092"
      KAFKA_ADVERTISED_LISTENERS: "INTERNAL://kafka:29092,EXTERNAL://localhost:9092"
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT"
      KAFKA_INTER_BROKER_LISTENER_NAME: "INTERNAL"

Примечание. Мы используем изображение obsidiandynamics/kafka для удобства, поскольку оно аккуратно объединяет Kafka и ZooKeeper в одно изображение. При желании вы можете заменить это изображениями из Confluent или Wurstmeister, но тогда вам придется все это правильно подключить. Изображение obsidiandynamics/kafka делает все это за вас, поэтому его настоятельно рекомендуется новичкам (и ленивым профессионалам).

Затем начните с docker-compose up. После загрузки перейдите к localhost: 9000 в своем браузере. Вы должны увидеть экран посадки Kafdrop.

Вы должны увидеть наш кластер с одним брокером. Начало многообещающее, но нет тем. Не проблема; давайте создадим тему и опубликуем несколько сообщений с помощью инструментов командной строки Kafka. Для удобства у нас уже есть образ Kafka, работающий как часть нашего docker-compose стека, поэтому мы можем использовать его для использования встроенных инструментов CLI.

docker exec -it kafka-kafdrop_kafka_1 bash

Это приведет вас в оболочку Bash. Инструменты находятся в каталоге /opt/kafka/bin, поэтому давайте cd в него:

cd /opt/kafka/bin

Создайте тему с именем streams-intro с 3 разделами:

./kafka-topics.sh --bootstrap-server localhost:9092 \
    --create --partitions 3 --replication-factor 1 \
    --topic streams-intro

Вернувшись к Kafdrop, теперь мы должны увидеть новую тему в списке.

Время публиковать материал. Мы собираемся использовать инструмент kafka-console-producer:

./kafka-console-producer.sh --broker-list localhost:9092 \
    --topic streams-intro --property "parse.key=true" \
    --property "key.separator=:"

Примечание. kafka-topics использует аргумент --bootstrap-server для настройки списка брокеров Kafka, а kafka-console-producer использует аргумент --broker-list для той же цели. Кроме того, --property аргументы в основном недокументированы; будьте готовы к поиску в Google.

Записи разделяются новой строкой. Части ключа и значения разделяются двоеточиями, как указано в свойстве key.separator. В качестве примера введите следующее (подойдет копипаст):

foo:first message
foo:second message
bar:first message
foo:third message
bar:second message

Когда закончите, нажмите CTRL+D. Затем вернитесь к Kafdrop и щелкните тему streams-intro. Вы увидите обзор темы вместе с подробной разбивкой основных разделов:

Давайте сделаем паузу и проанализируем, что было сделано. Мы создали тему с тремя разделами. Затем мы опубликовали пять записей с использованием двух уникальных ключей - foo и bar. Kafka использует ключи для сопоставления записей с разделами, так что все записи с одним и тем же ключом всегда будут отображаться в одном разделе. Удобно, но также важно, поскольку позволяет издателю определять точный порядок записей. Мы обсудим хеширование ключей и назначение разделов более подробно позже; тем временем расслабьтесь и наслаждайтесь поездкой.

Если посмотреть на таблицу разделов, то у раздела № 0 первое и последнее смещение равно нулю и двум соответственно. В разделе №2 они равны нулю и трем, в то время как раздел №1 кажется пустым. Нажатие на # 0 в веб-интерфейсе Kafdrop отправляет нас в программу просмотра тем:

Мы видим две записи, опубликованные под ключом bar. Обратите внимание, они совершенно не связаны с foo записями. Помимо сопоставления в рамках одной темы, нет ничего, что связывало бы записи по разделам.

Примечание. Если вам интересно, стрелка слева от сообщения позволяет развернуть и красиво распечатать сообщения в кодировке JSON. Поскольку в наших примерах JSON не использовался, печатать нечего.

Без преувеличения можно сказать, что встроенный инструментарий Кафки - это мерзость. В именах аргументов команд нет единообразия, и простой акт публикации сообщений с ключами требует от вас обхода препятствий - передачи непонятных, недокументированных свойств. Удобство использования встроенных инструментов - хорошо известная боль в сообществе Kafka. Это настоящий позор. Это все равно, что покупать Феррари только для того, чтобы поставить его с пластиковыми колпаками ступиц. К счастью, есть альтернативы - как коммерческие, так и с открытым исходным кодом - которые могут заполнить вопиющие пробелы в инструментах и ​​наблюдаемости.

Потребители и группы потребителей

До сих пор мы узнали, что производители отправляют записи в поток; эти записи организованы в красиво упорядоченные разделы. Топология pub-sub Kafka придерживается гибкой модели многоточечный-многоточечный, что означает, что может быть любое количество производителей и потребителей, одновременно взаимодействующих с потоком. В зависимости от фактического контекста решения потоковые топологии также могут быть двухточечными, многоточечными и двухточечными. Пришло время посмотреть, как потребляются записи.

Потребитель - это процесс или поток, который подключается к кластеру Kafka через клиентскую библиотеку. (Один доступен для большинства языков.) Потребитель обычно, но не обязательно, действует как часть всеобъемлющей группы потребителей. Группа определяется свойством group.id. Группы потребителей фактически являются механизмом балансировки нагрузки в Kafka, распределяя назначения разделов примерно равномерно между отдельными экземплярами-потребителями в группе. Когда первый потребитель в группе подписывается на тему, он получит все разделы в этой теме. Когда впоследствии подключается второй потребитель, он получает примерно половину разделов, освобождая первого потребителя от половины его предыдущей нагрузки. Когда потребители уходят (отключение или тайм-аут), процесс идет в обратном порядке - оставшиеся потребители поглощают большее количество разделов.

Таким образом, потребитель перекачивает записи из темы, извлекая долю разделов, назначенных ему Kafka вместе с другими потребителями в своей группе. Что касается балансировки нагрузки, это должно быть довольно просто. Но вот что интересно: использование записи не приводит к ее удалению. Поначалу это может показаться противоречивым, особенно если вы связываете акт потребления с истощением. (Во всяком случае, потребителя следовало бы назвать «читателем», но не будем останавливаться на выборе терминологии.) Простой факт в том, что потребители абсолютно не влияют на тему и ее разделы; тема - это реестр, предназначенный только для добавления, который может быть изменен только производителем или самим Kafka (как часть уплотнения или очистки). Потребители «дешевы», поэтому многие из них могут следить за журналами, не нагружая кластер. Это еще одно различие между потоком событий и традиционной очередью сообщений, и оно очень важно.

Потребитель внутренне поддерживает смещение, которое указывает на следующую запись в разделе, увеличивая смещение при каждом последующем чтении. Когда потребитель впервые подписывается на тему, он может начать либо с головного, либо с конечного конца темы. Это поведение контролируется установкой для свойства auto.offset.reset одного из значений latest, earliest или none. В последнем случае будет сгенерировано исключение, если для группы потребителей не существует предыдущего смещения.

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

Чтобы проиллюстрировать эту концепцию, рассмотрим надуманный сценарий, включающий тему с двумя разделами. На эту тему подписаны две группы потребителей - A и B. Каждая группа состоит из трех экземпляров, потребители имеют имена A1, A2, A3, B1, B2. и B3. На схеме ниже показано, как две группы могут разделять тему и как потребители продвигаются по записям независимо друг от друга.

Partition 0                 Partition 1
               +--------+                  +--------+
               |0..00000|                  |0..00000|
               +--------+                  +--------+
               |0..00001| <= consumer A2   |0..00001|
               +--------+                  +--------+
               |0..00002|                  |0..00002| <= consumer A1
               +--------+                  +--------+
               |0..00003|                  |0..00003| 
               +--------+                  +--------+
                  ...                          ...
               +--------+                  +--------+
               |0..00008| <= consumer B3   |0..00008| <= consumer B2
               +--------+                  +--------+
               |0..00009|                  |0..00009|
               +--------+                  +--------+
producer P1 => |0..00010|                  |0..00010|
               +--------+                  +--------+
                            producer P1 => |0..00011|
                                           +--------+

Посмотрите внимательно, и вы заметите, что чего-то не хватает. Потребителей A3 и B1 нет. Это потому, что Kafka гарантирует, что раздел может быть назначен только не более чем одному потребителю в его группе потребителей. (Мы говорим «максимум», чтобы охватить случай, когда все потребители отключены.) Поскольку в каждой группе есть три потребителя, но только два раздела, один потребитель останется в режиме ожидания - ожидая, пока другой потребитель в своей соответствующей группе уйдет, прежде чем он будет отключен. назначили раздел. Таким образом, группы потребителей представляют собой не только механизм балансировки нагрузки, но и подобный забору контроль исключительности, используемый для создания высокопроизводительных конвейеров без ущерба для безопасности, особенно когда есть требование, что запись может может обрабатываться только одним потоком или процессом в любой момент времени.

Группы потребителей также используются для обеспечения доступности. Периодически извлекая записи из темы, потребитель неявно сигнализирует кластеру о том, что он находится в «работоспособном» состоянии, тем самым продлевая аренду на его разделение. Однако, если потребитель не сможет прочитать снова в течение допустимого срока, он будет считаться неисправным, и его разделы будут переназначены - распределены между оставшимися «здоровыми» потребителями в его группе. Этот крайний срок контролируется свойством клиента max.poll.interval.ms клиента, по умолчанию установленным на пять минут.

Если использовать аналогию с транспортом, тема - это шоссе, а перегородка - это переулок. Запись эквивалентна автомобилю, а количество пассажиров в ней соответствует значению записи. Несколько автомобилей могут безопасно двигаться по одной и той же трассе, если они придерживаются своей полосы движения. Машины, разделяющие одну и ту же линию, едут последовательно, образуя очередь. Теперь предположим, что каждая полоса движения ведет к съезду, направляя свой трафик в какое-то место. Если один съезд заезжает в крен, другие съезды могут по-прежнему плавно двигаться.

Это именно эта метафора переулка, которую Kafka использует для достижения своей сквозной пропускной способности, легко достигающей миллионов записей в секунду на стандартном оборудовании. При создании темы можно выбрать количество разделов - количество дорожек, если хотите. Разделы делятся примерно поровну между отдельными потребителями в группе потребителей с гарантией того, что ни один раздел не будет назначен двум (или более) потребителям одновременно, при условии, что эти потребители являются частью той же группы потребителей. . Ссылаясь на нашу аналогию, автомобиль никогда не окажется на двух съездах одновременно; тем не менее, две полосы движения могут привести к одному и тому же съезду.

Примечание. После создания темы можно изменить размер, увеличив количество разделов. Однако невозможно уменьшить количество разделов без воссоздания темы.

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

Записи с одним и тем же хешем гарантированно занимают один и тот же раздел. Предполагая, что тема с несколькими разделами, записи с другим ключом вероятно окажутся в разных разделах. Однако из-за конфликтов хешей записи с разными хешами также могут оказаться в одном разделе. Такова природа хеширования. Если вы понимаете, как работает хеш-таблица, это ничем не отличается.

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

Подтверждение смещений

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

Сохранение состояния потребителя обратно в кластер Kafka называется фиксацией смещения. Обычно потребитель читает запись (или пакет записей) и фиксирует смещение последней записи плюс один. Если новый потребитель берет на себя тему, он начнет обработку с последнего зафиксированного смещения, поэтому шаг плюс один очень важен. (В противном случае последняя обработанная запись будет обработана во второй раз.)

Интересный факт: Kafka применяет рекурсивный подход к управлению зафиксированными смещениями, элегантно используя себя для сохранения и отслеживания смещений. Когда смещение зафиксировано, Kafka опубликует двоичную запись во внутренней теме __consumer_offsets. Содержимое этого раздела сжимается в фоновом режиме, создавая эффективное хранилище событий, которое постепенно сокращается только до последних известных точек фиксации для любой данной группы потребителей.

Контроль момента совершения смещения обеспечивает большую гибкость в отношении гарантий доставки, что дает Kafka еще один козырь. Термин «доставка» подразумевает не только чтение записи, но и полный цикл обработки со всеми побочными эффектами. Можно перейти от модели доставки не более одного раза к модели доставки хотя бы один раз, просто переместив операцию фиксации с точки до обработка записи начинается до некоторого момента после завершения обработки. В этой модели, если потребитель выйдет из строя на полпути обработки записи, запись будет перечитана после переназначения раздела.

По умолчанию потребитель Kafka будет автоматически фиксировать смещения каждые 5 секунд, независимо от того, завершил ли потребитель обработку записи. Часто это не то, что вам нужно, так как это может привести к смешанной семантике доставки - в случае отказа потребителя некоторые записи могут быть доставлены дважды, а другие могут не быть доставлены вообще. Чтобы включить фиксацию смещения вручную, установите для свойства enable.auto.commit значение false.

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

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

В сценарии «хотя бы один раз» (и, соответственно, ровно один раз) типичная реализация потребителя фиксирует свое смещение линейно, в тандеме с обработкой записей. То есть прочитать запись, зафиксировать ее (плюс один), прочитать следующую, зафиксировать (плюс один) и так далее. Распространенной тактикой является одновременная обработка пакета записей (где это имеет смысл) с использованием пула потоков и подтверждение последней записи только после завершения всего пакета. Процесс фиксации в Kafka очень эффективен, клиентская библиотека будет асинхронно отправлять запросы на фиксацию в кластер с использованием очереди в памяти, не блокируя потребителя. Клиентское приложение может зарегистрировать дополнительный обратный вызов, уведомив его о подтверждении фиксации кластером.

Группа потребителей - это несколько заниженная концепция, которая имеет решающее значение для универсальности платформы потоковой передачи событий. Просто варьируя сходство потребителей с их группами, можно прийти к совершенно разным топологиям распределения - от тематического поведения pub-sub до модели точка-точка в стиле MQ. Поскольку записи никогда не используются по-настоящему (продвигающееся смещение создает только иллюзию потребления), можно одновременно накладывать разрозненные топологии распределения на один поток событий.

Бесплатные потребители

Группы потребителей не являются обязательными; Потребитель не должен входить в группу потребителей, чтобы получать сообщения из темы. бесплатный потребитель пропускает свойство group.id. Это позволяет ему работать по упрощенным правилам, полностью передавая ответственность за управление потребителями приложению.

Примечание. Использование термина «бесплатно» для обозначения потребителя без охватывающей группы не является частью стандартной номенклатуры Kafka. Поскольку у Кафки нет канонического термина для описания этого, здесь был принят термин «свободный».

Бесплатные потребители не подписываются на тему; вместо этого приложение-потребитель отвечает за ручное присвоение потребителю набора разделов темы, индивидуально определяя начальное смещение для каждой пары разделов темы. Бесплатные потребители не возлагают на Kafka возмещение; приложение должно отслеживать прогресс таких потребителей и сохранять их состояние, когда это необходимо, используя хранилище данных по их выбору. В этом режиме перестают существовать концепции автоматического назначения разделов, перебалансировки, постоянства смещения, эксклюзивности раздела, биения потребителя и обнаружения сбоев, а также других так называемых тонкостей, предоставляемых группам потребителей.

Бесплатные потребители не так часто встречаются в дикой природе, как их сгруппированные собратья. Преимущественно существует два варианта использования, когда свободный потребитель является подходящим выбором. Первый - когда вам действительно нужен полный контроль над схемой назначения разделов и / или вам требуется альтернативное место для хранения потребительских смещений. Это очень редко. Излишне говорить, что его также очень сложно реализовать правильно, учитывая множество сценариев, которые необходимо учитывать. Второй, более часто встречающийся вариант использования - это когда у вас есть временный потребитель без сохранения состояния, которому нужно отслеживать тему. Например, вы можете быть заинтересованы в отслеживании темы для идентификации конкретных записей или просто в качестве инструмента отладки. Вам могут быть интересны только те записи, которые были опубликованы, когда ваш потребитель без сохранения состояния был в сети, поэтому такие проблемы, как сохранение смещений и возобновление с последней обработанной записи, совершенно неуместны. Хорошим примером того, как это обычно используется, является веб-интерфейс Kafdrop, который мы уже видели. Когда вы щелкаете тему для просмотра сообщений, Kafdrop создает бесплатного потребителя и назначает ему запрошенный раздел, считывая записи из предоставленных смещений. Переход к другой теме или разделу приведет к сбросу потребителя, отменяя любое предыдущее состояние.

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

+----------+          +----------+
|PRODUCER 1|          |PRODUCER 2|
+-----v----+          +-----v----+
      |                     |
      |                     |
      |                     |
+-----V---------------------V--------------------------------------+
|                            >>> TOPIC >>>                         |
|            +---------------------------------------------------+ |
| PARTITION 0|record 0..00|record 0..01|record 0..02|record 0..03| |
|            +-------------------v-------------------------------+ |
|                                |                                 |
|            +-------------------|-------------------------------+ |
| PARTITION 1|record 0..00|      |     |record 0..02|record 0..03| |
|            +-------------------|-------------v-----------------+ |
|                                |             |                   |
+----------v---------------------|-------------|-------------------+
           |                     |             |       
           |                     |             | 
           |                     |             | 
           |            +--------|-------------|-------------------+
           |            |        |             |                   |
      +----V-----+      | +------V---+  +------V---+  +----------+ |
      |CONSUMER 1|      | |CONSUMER 2|  |CONSUMER 3|  |CONSUMER 4| |
      +----------+      | +----------+  +----------+  +----------+ |
                        |               CONSUMER GROUP             |
                        +------------------------------------------+

Ключевые выводы:

  • Темы подразделяются на разделы, каждая из которых образует независимую, полностью упорядоченную последовательность в более широком, частично упорядоченном потоке.
  • Несколько производителей могут публиковать материалы в теме, выбирая раздел по своему желанию. Это может быть выполнено либо напрямую, указав секционированный индекс, либо косвенно, посредством ключа записи, который детерминированно хеширует согласованный индекс секции. (На схеме выше оба Производитель 1 и Производитель 2 публикуют сообщения в одной теме.)
  • Нагрузка разделов в теме может быть сбалансирована для популяции потребителей в группе потребителей, примерно равномерно распределяя разделы между членами этой группы. (Потребитель 2 и Потребитель 3 получают по одному разделу.)
  • Потребителю в группе не гарантируется распределение по разделам. Если население группы превышает количество разделов, некоторые потребители будут бездействовать до тех пор, пока этот баланс не выровняется или не изменится в пользу другой стороны. (Потребитель 4 остается без разделов.)
  • Разделы могут быть вручную назначены свободным потребителям. При необходимости целая тема может быть назначена одному бесплатному потребителю - это делается путем индивидуального назначения всех разделов. (Потребителю 1 можно свободно назначить любой раздел.)

Ровно-однократная доставка

При противопоставлении семантики доставки "хотя бы один раз" и "не более одного раза" часто задают вопрос: почему у нас не может быть ее ровно один раз?

Не углубляясь в академические подробности, которые включают предположения и доказательства невозможности, достаточно сказать, что семантика «точно один раз» невозможна без сотрудничества с приложением-потребителем. Что это значит на практике?

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

Пример: торговая платформа

Со всей этой теорией, нависшей над нами, как «Монолит Кубрика», было бы неуместно делать выводы, не предлагая читателю практического сценария.

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

Kafka делает этот вариант использования, как и другие подобные, почти тривиальным для реализации. Мы бы создали пару тем: prices для необработанных данных о ценах и orders для любых результирующих заказов. Мы можем быть довольно щедры с подсчетом разделов, поскольку природа данных дает нам широкие возможности для параллелизма.

В источнике фида мы могли бы опубликовать запись для каждой цены по теме prices, привязанную к тикерному коду. Автоматическое назначение разделов Kafka гарантирует, что каждый тикер-код будет обрабатываться (максимум) одним потребителем в его группе. Экземпляры-потребители могут свободно увеличиваться и уменьшаться в соответствии с нагрузкой обработки. Группы потребителей должны иметь содержательные названия, в идеале отражающие цель приложения-потребителя. Хорошим примером может быть trading-strategy.abc для фиктивной торговой стратегии под названием «ABC».

Как только ценовая модель определена потребителем, он может опубликовать другое сообщение - запрос заказа - в теме orders. Мы соберем еще одну группу потребителей - order-execution, которая будет читать приказы и пересылать их брокеру.

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

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

Гибкая архитектура многоточечной связи с многоточечными подписками Kafka сочетает в себе потребление с отслеживанием состояния и семантику широковещательной передачи. Используя отдельные группы потребителей, Kafka позволяет разрозненным приложениям обмениваться темами ввода, обрабатывая события в своем собственном темпе. Для второй торговой стратегии потребуется специальная группа потребителей - trading-strategy.xyz - применяющая свою конкретную бизнес-логику к общему потоку ценообразования, публикуя итоговые заказы в той же orders теме. Таким образом, Kafka позволяет создавать модульные конвейеры обработки событий из дискретных элементов, которые легко повторно использовать и компоновать.

Примечание. Во времена служебных шин и традиционных корпоративных брокеров сообщений, до того, как источники событий стали широко использоваться, вам приходилось выбирать между постоянными очередями сообщений или временными темами широковещательной рассылки. В нашем примере вы, вероятно, создали бы несколько очередей FIFO, используя шаблон разветвление. Поскольку Kafka обобщает темы pub-sub и постоянные очереди сообщений в единую модель, тема из одного источника может обслуживать широкий круг потребителей без дублирования.

В заключение

Платформы потоковой передачи событий являются высокоэффективным строительным блоком при создании модульных, слабосвязанных, управляемых событиями приложений. В мире потоковой передачи событий Kafka укрепила свои позиции в качестве универсального решения с открытым исходным кодом, которое одновременно является удивительно гибким и высокопроизводительным. Параллелизм и параллелизм лежат в основе архитектуры Kafka, формируя частично упорядоченные потоки событий, которые могут быть сбалансированы по нагрузке в масштабируемой потребительской экосистеме. Простая реконфигурация потребителей и входящих в них групп может привести к совершенно разной семантике распределения и обработки событий; смещение точки фиксации смещения может инвертировать гарантию доставки с модели «не более одного раза» на модель «хотя бы один раз».

Конечно, у Кафки есть свои недостатки. Оснастка, мягко говоря, некачественная; большинство практиков Kafka давно отказались от готовых утилит CLI в пользу других инструментов с открытым исходным кодом, таких как Kafdrop, Kafkacat и сторонних коммерческих предложений, таких как Kafka Tool. Широта вариантов конфигурации Kafka ошеломляет, со значениями по умолчанию, пронизанными ошибками, готовыми шокировать ничего не подозревающего начинающего пользователя.

В целом, Kafka представляет собой смену парадигмы в том, как мы проектируем и строим сложные системы. Его преимущества выходят за рамки излишнего, и они затмевают любые мелочи, которые неизбежно будут иметь место в технологии, которая подверглась столь агрессивному внедрению. Что особенно важно, это прокладывает путь к дальнейшему прогрессу в своем пространстве; Apache Pulsar является ярким примером альтернативной платформы, которая улучшила многие недостатки Kafka, но во многом обязана своей предшественнице за то, что заложила краеугольный камень и вывела этот жанр в мейнстрим.

Была ли эта статья вам полезна? Я хотел бы услышать ваши отзывы, так что не сдерживайтесь! Если вас интересует Kafka или трансляция событий, или у вас есть вопросы, подписывайтесь на меня в Twitter. Я также являюсь сопровождающим Kafdrop и автором Effective Kafka.