Всем привет, меня зовут Николай Голубев, я — техлид из компании HFLabs. Эта статья написана по мотивам моего выступления на конференции Saint HighLoad++.

Мы с командой развиваем крупнейший в России клиентский MDM (Master Data Management). Это центральная система, в которую попадают все данные предприятия о клиентах. Там они приводятся к единому формату, дедуплицируются, сливаются, и мы получаем золотую, эталонную запись о клиенте, которую можно использовать во всех системах.

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

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

Требования

Постановка задачи

Сначала немного контекста. Мы имели дело с on-premise MDM, монолитом на Java+Spring Boot, заказчик — банк ВТБ.

Сама база — это клиентская база Postgres примерно из 200 таблиц общим объемом более 1Тб, RPS до 1000 в пике на чтение и запись.

Нужно было реализовать резервирование базы данных между двумя дата-центрами и при этом автоматически переключаться в случае аварии.

Наверняка у вас сразу возникает вопрос, почему не взять что-то стандартное, поднять standby сервер, взять штатную репликацию Postgres и использовать, скажем, Patroni для переключения в случае аварии?

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

Основные требования

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

Вот какими были основные вводные:

  1. Хотелось организовать надежную работу с геораспределенными ДЦ.

  2. Должна была быть возможность независимо обновлять Postgres, ОС и другие настройки.

    При этом репликация должна работать на прикладном уровне и не зависеть от нюансов совместимости версии Postgres.

  3. Нужно было предусмотреть возможность долгого техобслуживания stand-in-базы (>1 ч.). 

    Изменения не должны копиться на источнике, чтобы бесконтрольно не рос WAL (Write Ahead Log) и не закончилось место для базы. Stand-in базой дальше мы будем называть резервную базу.

  4. Нужно было отключить репликацию транкейтов и DDL-инструкций, чтобы, если кто-то случайно почистил не ту табличку, это не привело к потере данных в резервной базе.

  5. Требовалось обеспечить минимальный лаг репликации при большой нагрузке (< 1с.).

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

  6. Переключение в случае аварии должно было происходить быстро (< 30 с.).

    В случае аварии у заказчика были проблемы с Patroni, он мог переключаться довольно долго и подвисать. Хотелось устранить эту проблему и обеспечить более быстрое переключение. 

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

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

Архитектура решения

Общая схема

На схеме мы видим два рабочих ЦОДа и кворумный ЦОД, который мы обсудим чуть дальше. В каждом из них развернуто по несколько экземпляров нашего приложения. Это нужно для отказоустойчивости и горизонтального масштабирования. Все приложения хранят данные в единой базе Postgres. В левом дата-центре активный экземпляр, в правом — stand-in. Данные между ними перетекают через Kafka с использованием коннекторов. 

Kafka — сердце всей этой схемы, которое обеспечивает надежное распределенное хранение, изменение и передачу данных между дата-центрами. Он развернут в кластерном режиме, есть брокеры и экземпляры ZooKeeper в обоих дата-центрах, а также несколько экземпляров ZooKeeper в 3-м дата-центре для обеспечения стабильного консенсуса. 

Debezium-коннектор отвечает за то, чтобы захватить данные в активной базе и передать их Kafka.

Sink-коннектор читает эти данные из Kafka и записывает в stand-in Postgres.

Debezium мы используем абсолютно стандартный, а Sink-коннектор написали сами. Эти коннекторы работают независимо каждый в своем дата-центре и поддерживают кластерный режим, состоящий из нескольких нод для отказоустойчивости. 

Консоль управления / DR оркестратор

Чтобы управлять всем этим, мы написали свою консоль управления.

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

DR-оркестратор может быть как отдельным сервисом, так и встроен внутрь консоли. Мы пошли именно по второму пути. 

Компоненты

Мы с вами посмотрели на общую схему, теперь давайте пройдёмся по детальным настройкам и нюансам этих компонентов.

Debezium-коннектор

Debezium — это open-source платформа для того, чтобы передавать данные из источников в realtime-режиме. Она реализует CDC (Change Data Capture) паттерн и поддерживает множество различных баз данных в виде источника.

Мы в реальном времени захватываем данные в источнике и передаём брокеру. Другие системы могут также в реальном времени эти изменения использовать, подписываться на них и обрабатывать.

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

В чем плюс Kafka Connect? Он позволяет из коробки поддерживать кластерный режим и масштабировать работу коннекторов, а также обеспечивать их отказоустойчивость. Плюс, у него есть REST API, через который можно на лету создавать, удалять и  перезапускать коннекторы. Нам это очень пригодилось.

Для того, чтобы понять лучше, как работает Debezium, рассмотрим, как CDC устроен внутри Postgres.

Есть исходные таблицы, любые изменения Postgres пишет через write-ahead log, в котором они хранятся в бинарном формате. Затем эти бинарные данные можно при помощи Output plugin представить в виде стандартного формата JSON либо Avro и отдать куда-то вовне. Publication позволяет настроить подписку либо на все таблицы, либо на часть, а Replication slot обеспечивает надежное чтение без потерь и пропусков данных. Он хранит последние изменения и таким образом говорит Postgres, что эти изменения прочитал, можно почистить WAL.

Чтобы Debezium мог работать мы должны в базе поднять уровень логического декодирования, создать отдельного пользователя либо дать права нашему пользователю на создание replication slot и стриминг данных из него. 

Как выглядит сообщение Debezium:

В качестве ключа используются колонки primary key по умолчанию, а в теле сообщения указывается операция, которая была выполнена (create/update/delete), источник (схема и таблица) и состояние до и после.

Пример — JSON

Мы видим before, after (состояние до и после), source (источник), op (операция).

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

Тут есть один неприятный нюанс. Те, кто много работал с Postgres, наверняка знают, что там есть механизм TOAST, который хранит тяжелые колонки вне основной строки. При обновлении строк в такой таблице WAL лог не дублирует значения этих тяжелых колонок, если они не менялись. Debezium в этом случае в сообщение запишет такой placeholder:

Здесь мы видим в поле metadata значение __debezium_unavailable_value.

Если такие тяжелые колонки есть, нужно быть готовым обработать их в Sink-коннекторе и не потерять данные.

Настройки Debezium

Какие настройки Debezium мы используем:

По умолчанию Debezium пишет изменения по каждой таблице в отдельный топик. Этой настройкой мы задаем префикс названия топиков. 

Мы используем JSON-формат. При этом мы не пишем JSON-схему, потому что она может занимать больше места в сообщении, чем сами данные, а схему можем в Sink-коннекторе всегда получить из Stand-in базы.

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

Отключаем snapshot данных при создании коннектора Debezium, чтобы при создании он не сохранял все строки из таблицы в Kafka. Для больших таблиц это практически нереально. Об инициализации мы поговорим отдельно дальше. 

Отключаем операции truncate, как и было указано в требованиях.

Здесь мы указываем название слота и публикации.

Здесь задаётся публикация не всех таблиц, а только указанных в фильтре.

По умолчанию создаются топики с 4 партициями, чтобы их можно было обрабатывать параллельно в Sink-коннекторе. 

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

А это настройки различных типов данных. 

С такими настройками мы используем Debezium-коннектор, теперь перейдём к Sink-коннектору.

Sink-коннектор

Напомню, Sink-коннектор берет изменения из Kafka и применяет их к Stand-in-базе.

Есть как минимум два готовых JDBC Sink-коннектора:

  1. Довольно древний от создателей Kafka.

При этом у него своеобразная лицензия и нужна дополнительная конвертация в Debezium формат.

2. Более свежий от создателей Debezium.

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

Для решения двух этих проблем мы написали свой коннектор. За основу был взят первый коннектор, в него мы добавили нужное и выкинули лишнее.

Самописный Sink 

Чем нам помог самописный Sink-коннектор:

  1. Передача схемы данных больше не нужна.

Теперь обрабатываются только DML-инструкции. Структура полей и описание таблиц берутся прямо из Stand-in базы. 

2. Контроль над запросами и ошибками.

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

3. Теперь toast-плейсхолдер поддерживается.

4. Была внедрена At least once семантика. 

Это возможность повторно обрабатывать одни и те же сообщения за счет того, что добавляются инструкции on conflict. Таким образом, если к нам приедет один и тот же update или insert, ничего не сломается.

5. Была добавлена возможность параллельной обработки изменений.

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

Настройки Sink

Настройки Sink-коннектора гораздо проще. Тут мы указываем координаты базы, название топиков, JSON-формат. Из интересного только количество параллельных задач, которые определяют степень параллельности обработки изменений.

Есть еще блок настроек, который конфигурируется не для каждого коннектора, а именно в конфиге Kafka Connect Cluster.

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

Две следующие настройки уже служат для консьюмера. 

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

Последние настройки — это названия служебных топиков, в которых Kafka Connect хранит свои настройки, и включение аутентификации для авторизации. 

Про коннекторы мы поговорили, перейдем к консоли управления.

Консоль управления

Это простое веб-приложение, которое мы сами написали. Здесь в основном различные интеграции, опрос всех компонентов, вызовы API Kafka Connect, Postgres и самой Kafka.

Сверху мы видим четыре ноды нашего приложения, по центру — два Postgres (активный и Stand-in), а снизу — Kafka Connect кластеры. Если провести по центру линию, то слева — первый дата-центр, справа — второй. При этом данные идут слева направо. 

Здесь мы видим Debezium-коннектор в нижнем Kafka Connect кластере и Replication slot на активной базе — это захват данных.

Во втором дата-центре крутится Sink-коннектор из четырех задач, который применяет эти данные к Stand-in-базе. Тут же мы видим delta (отставание данных). Сейчас оно нулевое и все в порядке. Если что-то идет не так, мы это сразу подсвечиваем, администратор может быстро увидеть, что есть проблемы, и начать разбираться.

Итого, вот главные функции консоли:

  1. Визуальный мониторинг всей схемы.

  2. Управление коннекторами.

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

3. Сбор метрик для построения графиков и алертов.

Консоль публикует метрики в формате Prometheus, которые можно добавить на дашборды, настроить алерты.

4. Ручное (штатное) переключение активной БД.

5. Disaster recovery (аварийное переключение).

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

При нажатии коннекторы меняются местами.

Теперь справа Debezium, а слева Sink, правый Postgres становится активным. 

Алгоритм переключения базы данных

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

  1. Остановить запись в активную БД.

  2. Проверить, что все изменения дошли до Stand-in-базы.

  3. Актуализировать значения sequences.

  4. Поменять местами коннекторы (Debezium <-> Sink). 

  5. Дождаться активации Debezium.

  6. Сменить номер активной БД.

  7. Включить запись в БД.

  8. Удалить асинхронно replication slot со старой, активной БД, чтобы он не держал WAL.

Здесь, пожалуй, стоит остановиться на двух пунктах поподробнее.

Проверить, что все изменения дошли до Stand-in-базы.

Как проверить, что все изменения дошли? Они летят через Kafka, поэтому мы можем обратиться к ней через Admin API, получить последние прочитанные Sink-коннектором офсеты, а также получить для каждого топика последние записанные офсеты.

Их нужно просто сравнить. 

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

Актуализировать значения sequences.

Отдельный пункт, который доставляет неудобства, это sequence. К сожалению, они не реплицируются как таблицы, поэтому с ними приходится работать отдельно.

При ручном переключении старый active доступен, поэтому мы можем просто сходить туда, взять все сиквенсы, их последние значения и скопировать в Stand-in-базу. Но если у нас аварийное переключение и старая база недоступна, то мы опираемся на данные.

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

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

  1. Disaster recovery

Сам алгоритм точно такой же, только нам нужно определить, что произошла авария. Как это сделать? 

Напомню, что у нас в каждом ЦОДе есть консоль управления, в которую встроен DR-оркестратор. Он  мониторит состояние активной базы и Stand-in-базы. Если есть какая-то проблема, и мы не можем, например, из консоли достучаться до активного Postgres, то это еще не повод переживать. Мы опрашиваем все ноды приложения и смотрим, что считают они. Если из них доступ есть, то значит это какая-то кратковременная проблема и не стоит реагировать.

Но представим, что первый дата-центр полностью вышел из строя, там случилась экстренная ситуация. Что делать в таком случае?

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

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

Подытожим алгоритм детекции аварии:

  1. Проверяем доступность активной БД из консоли.

  2. Проверяем согласованность мнений нод приложения:

  • Опрашиваем все ноды.

  • Считаем количество статусов DB_UNAVAILABLE. 

  • Количество больше кворума (параметр).

    3. Проверяем в цикле. Если проблема 5 раз подряд, то фиксируем аварию.

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

4. Аварийное переключение (если фиксируем аварию):

  • Проверяем доступность Stand-in-базы. 

  • Проверяем дельту последнего изменения.

  • Получаем распределенную блокировку.

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

Чтобы избежать этого, мы получаем распределенную блокировку, делаем это, опять же, через Kafka: создаем отдельного Kafka consumer, чтобы не вводить дополнительные сущности; подписываемся на топик с одной партицией; ждем успешного назначения партиции при помощи ConsumerRebalanceListener.onPartitionsAssigned — если нам назначили её, значит, мы лидер в этом процессе и можем выполнять переключение; затем — выполняем переключение.

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

  1. Адаптация приложения

Чтобы схема максимально хорошо работала, нужно поддержать работу двух независимых баз данных, которые находятся в active режиме. Для этого мы в нашем приложении добавляем еще один Datasource.

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

Поддержать в Spring это можно очень просто. Есть базовый класс AbstractRoutingDataSource, от которого мы наследуемся, остается только указать название активного Datasource.

Также понадобится read-only режим. Поскольку во время переключения нужно остановить запись, чтобы убедиться, что все данные из одной базы в другую дошли, мы на короткий промежуток времени останавливаем запись, а клиенты повторяют запросы к системе. Это происходит быстро, поэтому в итоге после пары попыток они сохраняют данные уже в Stand-in-базу, как нам и нужно.

Сделать read-only можно за счет следующих опций, которые можно выставить либо на сессию, либо на транзакцию.

Управлять этими режимами и настройками можно при помощи любого геораспределенного, отказоустойчивого key-value хранилища — это etcd либо ZooKeeper. Также можно использовать саму Kafka, как это делает Debezium.

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

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

Миграция схемы БД 

  1. Мигрируем обе базы перед обновлением приложения.

Сначала Stand-in-базу, меняем структуру таблиц, чтобы она была готова к новым данным, а потом основную базу.  

2. Миграции должны быть обратно совместимыми.

Но это вы уже и так соблюдаете, если у вас есть несколько нод приложения.

3. Внутри приложения (liquibase, flyway) прогоняем для обоих datasources.

4. Внешняя — запускаем по очереди для каждой БД.

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

Пришло время поговорить об эксплуатации и некоторых граблях, на которые мы наступили, заодно поделимся советами.

Эксплуатация

Инициализация

  1. Добавляем настройки в обеих базах. 

  2. Активируем Debezium connector для active pg. 

  3. Делаем полный дамп схемы из active. 

pg_dump -h <pg1 host> -U <user> -n <schema> -F c -f <file> <db name>

4. Делаем импорт дампа в Stand-in. 

pg_restore -h <pg2_host> -d <db_name> -U <sys user> <file>

5. Активируем Sink connector для Stand-in pg.

В настройках Debezium-коннектора мы отключаем snapshot, чтобы все данные из огромных таблиц не писались в Kafka. Поэтому мы активируем Debezium без снапшота, делаем полный дамп из активной базы и заливаем его в Stand-in. После этого активируем уже Sink-коннектор, который сначала применяет всю дельту, которая скопилась с момента пункта 2, а потом начинает применять все новые изменения. Поскольку у нас at-least-one семантика, то ошибок в случае обработки повторной записи не будет.

Сверка

Нужно как-то сверять данные, чтобы убедиться в консистентности двух баз. Для этого можно использовать полную сверку. Здесь, как минимум, есть 3 метода:

  1. count(*) — для append-only таблиц.

  2. Семплирование — N случайно взятых записей 

  3. Чек-сумма по всей таблице*
    Пример хранимой процедуры для расчета чек-суммы есть в полной версии презентации. Можете посмотреть, если интересно.

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

Для быстрой сверки мы исходим из того, что:

  • Изменения не теряются по пути, они все записываются в Kafka.

  • Достаточно проверять только дельту по времени последнего изменения в Stand-in и активной базах, либо сравнивать максимальные идентификаторы по основным таблицам и понимать, что все данные дошли.

  • Обязателен индекс на целевой колонке.

Мониторинг 

Что нужно мониторить:

  • Активность консоли, коннекторов (тасок) и Kafka.

  • Лаг отставания для топиков в Kafka.

  • Лаг отставания replication slot в Postgres.

  • Размер WAL и свободное место для баз.

  • Дельту сверки данных.

Грабли и советы

На какие грабли мы наступили и какие в связи с этим есть советы:

  1. Большие сообщения — это ограничение всей схемы (producer.max.request.size и message.max.bytes).

В Kafka есть ограничение на максимальный размер сообщения. Хоть Debezium и пишет каждое изменение в отдельном сообщении, но все равно, если у вас есть очень большие таблицы, то обязательно тестируйте, смотрите реальный максимальный размер ваших сообщений и подгоняйте настройки под этот размер. Иначе, если сообщение не пройдёт, то схема остановится.

2. Используйте больше одной партиции для топиков, чтобы их можно было обрабатывать параллельно.

3. Не забывайте про retention, исходя из того, насколько вы планируете останавливать Stand-in базу (log.retention.hours и log.retention.bytes). 

4. Включите автосоздание топиков, чтобы для каждой таблички каждый раз не создавать вручную.

5. Будьте готовы к плейсхолдеру __debezium_unavailable_value или используйте ReselectColumnsPostProcessor 

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

6. Проверьте наличие первичных ключей на всех таблицах (default identity), потому что по умолчанию они используются в качестве ключа сообщения.

7. Имейте в виду, что Debezium подхватывает новые таблицы только после рестарта.

ALTER PUBLICATION cdi_dbz_publication ADD TABLE <new_table>

Если используете публикацию не всех таблиц с фильтрацией, то есть нюанс: Debezium подхватывает новые таблицы только после рестарта. Он перезапускается, смотрит, какие есть новые таблицы и добавляет их в публикацию. Чтобы таблицы подхватывались сразу, можно в миграции использовать ALTER PUBLICATION и добавлять их руками.

8. После создания Debezium-коннектора проверяйте активацию replication slot.

SELECT active FROM pg_replication_slots 
WHERE slot_name = cdi_debezium 

Во время тестирования мы выявили проблему: при переключении Debezium отвечал, что он активен, а на самом деле слот еще не успел активироваться и мы теряли часть изменений при переключении.

9. Debezium может не успевать сохранять изменения в Kafka.

Поскольку Debezium работает по-прежнему в однопоточном режиме, хоть он пишет в Kafka, все равно у него есть предел. В нашем случае это было ~ 50 тыс. строк в секунду. Если изменений больше, то начнет расти WAL и возникнут проблемы. Мы дошли до этого предела, когда по неосторожности запустили одновременно несколько очень интенсивных многопоточных батчевых обработок. В принципе, в онлайне сложно упереться в этот предел, отсюда вывод: регулируйте интенсивность батчевых обработок и такой проблемы не возникнет.

Итоги

Как нам удалось реализовать требования к системе — осмотрим по пунктам: 

  1. Надежная работа с геораспределенными дата-центрами:

  • Каждый коннектор работает независимо внутри одного ДЦ с базой.

  • Kafka обеспечивает отказоустойчивое, распределённое хранение изменений и передачу.

    2. Независимое обновление Postgres, ОС и других настроек:

  • Репликация прикладная. 

  • Формат сообщений Debezium не привязан к версии Postgres.

Можно независимо обновлять Postgres, операционку под ним, делать сложные процедуры типа full vacuum и так далее. Поскольку репликация прикладная, то никаких проблем с этим не возникает.

3. Возможно долгое техобслуживание Stand-in-базы:

  • Debezium сразу считывает изменения из активной базы и сохраняет их в Kafka.

  • Kafka — буферная зона, WAL не растет.

Я уже сказал, что можно делать full vacuum, останавливать Sink-коннектор на какое-то время, Debezium сразу считывает изменения из источника и сохраняет их в Kafka. Kafka служит буферной зоной, поэтому WAL всегда нормального размера и место не заканчивается.

4. Отключение репликации транкейтов и DDL:

Debezium реплицирует только изменения в данных (DDL Debezium не передает), транкейты отключены.

5. Минимальный лаг репликации при большой нагрузке:

  • Debezium пишет изменения в топики с несколькими партициями. 

  • Sink-коннектор работает параллельно, можно масштабировать запись.

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

6. Быстрое переключение в случае аварии:

  • Свой алгоритм DR с гибкими параметрами.

  • Kafka позволяет быстро понять, что все данные дошли.

7. Возможность ручного управления и гибкой настройки — отдельная консоль с наглядным UI.

Кому подойдет такая схема

Схема довольно сложная, поэтому стоит пытаться ее повторять, только если у вас:

  1. Критичность системы очень высокая (mission-critical, business-critical).

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

2. Часть особых требований совпадает с нашими.

3. Уже используется Debezium.

Если вы уже используете Debezium, то будет сильно проще. Когда у вас нет множества больших колонок с JSON, текстами и бинарями, то можно взять готовый Debezium Sink-коннектор. А вместо консоли можно написать скрипт и по алгоритму переключения, который описан в статье, переключать базы данных.

Кому не подойдет такая схема

Применение такого решения не подойдёт, если:

  1. Вы не умеете готовить Kafka.

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

2. У вас в команде пока невысокий уровень зрелости эксплуатации (SRE).

Компонентов много, их нужно мониторить и быстро реагировать в случае каких-то инцидентов. Это требует опытной команды эксплуатации и высокого уровня SRE-культуры. Без этого будет тяжело.

Надёжность геораспределённых баз данных остаётся актуальной и важной темой. А чтобы узнать больше полезного, приходите на конференцию развития Highload++ 2026 в Москве!