Выгружать таблицу целиком каждую ночь просто, пока в ней тысячи строк. Когда строк сотни миллионов, полная выгрузка нагружает источник и не укладывается в окно. Тогда переходят на инкрементальную загрузку: забирать только то, что изменилось.
Полная загрузка
Выгрузить таблицу целиком и заменить копию в хранилище.
- Плюсы: просто, видны удаления (строки просто исчезают), нет накопленных ошибок.
- Минусы: долго и тяжело для источника на больших объёмах.
Подходит для справочников и небольших таблиц. Даже при инкрементальной загрузке полезно периодически делать полную сверку.
Инкремент по водяному знаку
Таблица источника имеет колонку updated_at, которая меняется при каждом изменении строки. Загрузка запоминает водяной знак (watermark): до какого updated_at данные уже забраны, и в следующий раз берёт updated_at > watermark.
Подводные камни:
- Граница: несколько строк с одинаковым
updated_at, часть из них была вставлена уже после выгрузки, но с тем же временем. Строгое>их потеряет. Безопаснее брать>=или окно с перекрытием и дедуплицировать по ключу при загрузке (upsert). - Долгие транзакции: строка получила
updated_at = 10:00, но транзакция закоммитилась в 10:05, после того как выгрузка в 10:02 сдвинула водяной знак. Строка потеряна. Лечится перекрытием окна (брать с запасом в несколько минут или часов). - Удаления не видны: удалённой строки в источнике просто нет, по
updated_atеё не найти. Решения: мягкое удаление (deleted_at) в источнике, периодическая сверка ключей, log-based CDC. - Колонку обновляют не все: если какой-то код меняет строку без обновления
updated_at, изменение потеряется. Надёжнее триггер в базе. - Нужен индекс по
updated_at, иначе инкремент сканирует всю таблицу.
Log-based CDC
CDC (change data capture) захватывает изменения из журнала транзакций базы: WAL в PostgreSQL (логическая репликация), binlog в MySQL. Самый популярный инструмент: Debezium, работает как коннектор Kafka Connect и пишет события изменений в Kafka.
Событие Debezium содержит:
op:c(create),u(update),d(delete),r(read: строка из начального снимка);beforeиafter: состояние строки до и после (дляdпустоеafter);source: метаданные: таблица, позиция в журнале (LSN), время транзакции.
Плюсы log-based CDC:
- Видны все изменения, включая удаления и промежуточные состояния.
- Минимальная нагрузка на источник: читается журнал, а не таблицы.
- Задержка секунды.
- Не нужна колонка
updated_atи дисциплина разработчиков.
Минусы и что учитывать:
- Нужен доступ к журналу и настройка базы (в PostgreSQL
wal_level = logical, слот репликации, публикация). - Слот репликации держит WAL, пока коннектор не прочитал: если коннектор встал надолго, диск источника переполнится. Это обязательно мониторят.
- Начальная загрузка: снимок существующих данных (события
r), затем поток изменений. - Изменения схемы источника нужно обрабатывать.
- Порядок гарантирован в пределах ключа (события строки идут в одну партицию Kafka по первичному ключу).
Применение изменений в хранилище
События нужно применить к копии таблицы:
- Последнее состояние: по каждому ключу берём событие с самой поздней позицией в журнале; если это удаление, строку удаляем (или помечаем). Делается через
MERGEили в ClickHouse через ReplacingMergeTree с флагом удаления. - История: все события сохраняются как журнал изменений, из которого можно построить SCD 2.
- Идемпотентность: повторная доставка того же события не должна ломать результат. Порядок определяется позицией в журнале (LSN), а не временем получения.
Outbox
Сервису нужно и записать изменение в свою базу, и отправить событие в Kafka. Если делать это двумя операциями, одна может не пройти: заказ сохранён, а событие потеряно (или наоборот). Паттерн transactional outbox: в той же транзакции, что и бизнес-изменение, сервис пишет событие в таблицу outbox. Отдельный процесс (часто тот же Debezium) читает outbox и публикует события. Атомарность обеспечивает транзакция базы.
Частые ошибки
- Строгое
updated_at > watermarkбез перекрытия: потери на границе и из-за долгих транзакций. - Инкремент без обработки удалений.
- Слот репликации без мониторинга: переполненный диск боевой базы.
- Применение событий CDC в порядке получения, а не в порядке журнала.
- Двойная запись «в базу и в Kafka» без outbox.
Что спросят на собесе
- Полная загрузка против инкрементальной, когда что.
- Как работает загрузка по
updated_atи где она теряет данные. - Что такое log-based CDC, Debezium и формат его событий.
- Как применить поток изменений к таблице в хранилище с учётом удалений.
- Паттерн outbox и зачем он нужен.