Мертвые события убивают микросервисы: Как Debezium и Outbox-паттерн спасут вашу систему от несогласованности

Мертвые события убивают микросервисы: Как Debezium и Outbox-паттерн спасут вашу систему от несогласованности

Ненадежная отправка сообщений — частая причина ошибок в микросервисной архитектуре. Паттерн Outbox решает эту проблему, а Debezium делает его реализацию невероятно простой.

Проблема, которую решаем

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

Решение

Популярный подход для решения этой проблемы — паттерн Outbox. Приложение записывает события, которые необходимо отправить при изменении данных, в таблицу outbox в рамках транзакции, в которой осуществляются и основные операции. Далее мы, используя подход Change Data Capture (CDC), отслеживаем изменения в таблице outbox и отправляем события в брокер. На просторах сети можно найти множество реализаций CDC, готовых к промышленному применению. Одна из таких реализаций — Debezium.

Debezium

Debezium — распределенная платформа с открытым исходным кодом (лицензия Apache 2.0), реализующая CDC. Она представляет собой набор коннекторов для нескольких популярных баз данных — PostgreSQL, MySQL, MongoDB и других. Debezium слушает лог изменений в БД (WAL в Postgres, binlog в MySQL), извлекает их, преобразует в события и публикует сообщения в Kafka.

Есть несколько вариантов развертывания Debezium:

  • С помощью Kafka Connect — наиболее распространенный вариант. Коннектор отслеживает изменения в соответствующих БД и/или таблицах. Потом Kafka Connect обрабатывает данные от коннекторов и передает их в Kafka.
  • Отдельный сервер Debezium — это готовое приложение, которое передает изменения в БД в различные системы обмена сообщениями.
  • Embedded движок — библиотека движка подключается к java-приложению, и события нужно обрабатывать непосредственно в приложении. Это наиболее гибкое и расширяемое решение, но требует дополнительных усилий со стороны разработки.

Пример настройки Debezium

Запустим Debezium Kafka Connect на локальном компьютере с помощью docker-compose. Полный пример доступен по ссылке.

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

CREATE TABLE IF NOT EXISTS outbox ( id BIGSERIAL PRIMARY KEY, aggregate_type TEXT, aggregate_id TEXT, event_type TEXT, payload TEXT, created_at TIMESTAMP DEFAULT now() );

Теперь нам нужно настроить Debezium Kafka Connect, чтобы он отслеживал изменения в таблице outbox и отправлял сообщения в Kafka. Сначала создадим свойства коннектора в которых будут указаны таблицы для подключения Debezium и топики Kafka, куда отправлять сообщения об изменениях:

{ "name": "outbox-connector", "config": { "connector.class": "io.debezium.connector.postgresql.PostgresConnector", "tasks.max": "1", "database.hostname": "postgres", "database.port": "5432", "database.user": "outbox", "database.password": "outbox", "database.dbname": "outbox_example", "database.server.name": "dbserver1", "table.include.list": "public.outbox", "topic.prefix": "debezium.outbox", "snapshot.mode": "initial", "plugin.name": "pgoutput", "publication.name": "outbox_publication", "slot.name": "outbox_slot", "topic.creation.enabled": "true", "topic.creation.default.replication.factor": "1", "topic.creation.default.partitions": "1", "key.converter": "org.apache.kafka.connect.json.JsonConverter", "key.converter.schemas.enable": "false", "value.converter": "org.apache.kafka.connect.json.JsonConverter", "value.converter.schemas.enable": "false" } }

Здесь database.* — настройки базы данных, table.include.list — список таблиц, которые будут отслеживаться, topic.prefix — префикс для топиков Kafka, в которые будут отправляться сообщения.

Теперь сконфигурируем docker-compose.yml, чтобы запустить всю инфраструктуру локально. Первую часть файла, которая конфигурирует Postgres и Kafka, не буду здесь приводить. Стоит только обратить внимание на конфигурацию Postgres, а именно command: ["-c", "wal_level=logical", "-c", "max_replication_slots=10", "-c", "max_wal_senders=10"] — здесь обязательно нужно включить wal_level=logical, иначе debezium не сможет работать.

Конфигурация коннектора очень проста:

kafka-connect: image: debezium/connect:2.6 hostname: kafka-connect container_name: kafka-connect ports: - "8083:8083" environment: BOOTSTRAP_SERVERS: 'broker:29092' GROUP_ID: 1 CONFIG_STORAGE_TOPIC: 'connect_configs' OFFSET_STORAGE_TOPIC: 'connect_offsets' STATUS_STORAGE_TOPIC: 'connect_statuses' depends_on: - postgres - broker - schema-registry

Для инициализации коннектора нужно отправить запрос на http://localhost:8083/connectors с телом запроса, который мы описывали выше. В нашем случае используется отдельный контейнер, который поднимается после старта debezium и отправляет запрос на создание коннектора:

connect-init: image: curlimages/curl:8.5.0 depends_on: - kafka-connect entrypoint: > /bin/sh -c ' until curl -sS -f http://kafka-connect:8083/connectors; do echo "Waiting for Kafka Connect to be ready..." sleep 5 done echo "Kafka Connect is ready!" curl -X POST -H "Content-Type: application/json" --data @/data/register-connector.json http://kafka-connect:8083/connectors/ echo "Debezium connector registered!" ' volumes: - ./register-connector.json:/data/register-connector.json

Теперь поднимем наши контейнеры:

docker compose up -d

И проверим как работает коннектор. Добавим несколько записей в таблицу outbox:

INSERT INTO outbox ( aggregate_type, aggregate_id, event_type, payload ) VALUES ('user', '1', 'CREATED', 'username=John Dow'), ('user', '1', 'UPDATED', 'username=John Doe'), ('user', '2', 'DELETED', '');

Зайдем в kafka-control-center и проверим, что сообщения появились в топике debezium.outbox.public.outbox. Если все пройдет хорошо, то можно увидеть сообщения примерно такого содержания:

{ "before": null, "after": { "id": 2, "aggregate_type": "user", "aggregate_id": "1", "event_type": "UPDATED", "payload": "username=John Doe", "created_at": 1756487834455127 }, "source": { "version": "2.6.2.Final", "connector": "postgresql", "name": "debezium.outbox", "ts_ms": 1756487834456, "snapshot": "false", "db": "outbox_example", "sequence": "[null,\"26728032\"]", "ts_us": 1756487834456611, "ts_ns": 1756487834456611000, "schema": "public", "table": "outbox", "txId": 740, "lsn": 26728032, "xmin": null }, "op": "c", "ts_ms": 1756487834874, "ts_us": 1756487834874939, "ts_ns": 1756487834874939412, "transaction": null }

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

Преимущества

  • Простота настройки и гибкость — Debezium не требует глубоких знаний для настройки, особенно для начала разработки. Если работали с Kafka Connect, то настройка не будет представлять ничего сложного. Есть возможность подключения к системе в различных вариантах, в том числе и без Kafka.
  • Интеграция с Kafka — изначально платформа строилась вокруг экосистемы Kafka, поэтому отлично интегрирована с ней.
  • Масштабируемость и надежность — Debezium обладает множеством преимуществ, которые предоставляет экосистема Kafka, среди них надежность и масштабируемость.
  • Отсутствие блокировки транзакции и минимальные накладные расходы на БД — коннектор читает изменения из журнала транзакций, поэтому не создается дополнительного влияния на текущую работу БД.
  • Поддержка множества баз данных — Debezium поддерживает из коробки интеграцию с множеством популярных баз данных, как реляционных, так и NoSQL.

Недостатки

Как и у любой технологии, у Debezium есть и свои недостатки, к таким можно отнести:

  • Зависимость от журнала транзакций — если журнал будет поврежден или удален, то данные невозможно будет отправить.
  • Необходимость настройки на стороне БД — очень часто требуется настроить БД, например уровень журнала и/или привилегии для пользователя. Это добавляет дополнительную сложность администрирования.
  • Невозможность ретрансляции старых событий — нельзя повторно отправить события, произошедшие до запуска debezium.
  • Задержки чтения — в любом случае есть небольшая задержка между записью в таблицу и последующей публикацией события.
  • Интеграция с Kafka — для того чтобы использовать на полную, нужно поддерживать инфраструктуру Kafka, но не во всех приложениях это нужно.

Итог

Debezium — отличная реализация паттерна CDC. Для знакомых с экосистемой Kafka его настройка не составит труда. Но его применение должно учитывать особенности проекта и требования к безопасности.

Подписывайся на мой канал в telegram

1
Начать дискуссию