PostgreSQL CookbookUse casesШов CDC: эстафета в kafka-cookbook
0 / 63 (0%)

Шов CDC: эстафета в kafka-cookbook

Меню Brew меняется само по себе: бариста правит цену эспрессо, маркетинг публикует новую статью в блог, у клиента меняется телефон. И каждое такое изменение должно доехать до соседнего мира — до kafka-cookbook, где тот же самый кофейный Brew живёт уже как поток событий в Kafka, кормит Elasticsearch и строит поиск по меню. Вопрос финала всего курса один: как отдать наш поток изменений Kafka-курсу, не переписывая ничего на той стороне?

Ответ — Change Data Capture. Мы не пишем своего relay (это был путь 09-03), не заводим триггеров. Мы настраиваем у Postgres логическую репликацию ровно на нужных таблицах, а Debezium из kafka-cookbook подключается к ней и читает поток сам. Postgres становится источником истины, а WAL — нашей шиной событий. Этот юнит собирает шов и доказывает, что он держит нагрузку.

CDC вместо relay: WAL — уже журнал изменений

В 09-03 мы вручную клали событие строкой в outbox и вычитывали его relay'ем. CDC заходит с другой стороны: изменение уже записано — в WAL, журнал упреждающей записи, которым Postgres и так гарантирует durability. Логическое декодирование разбирает WAL обратно в логические INSERT/UPDATE/DELETE по таблицам — и отдаёт их потребителю. Своего кода доставки писать не нужно вообще: relay'ем работает сам Postgres + декодер на стороне потребителя.

Чтобы поток был адресным, а не «весь сервер целиком», CDC опирается на две настройки: PUBLICATION (какие таблицы стримим) и REPLICA IDENTITY (сколько данных о старой строке писать в WAL на UPDATE/DELETE). Обе мы выставляем явно.

Три источника и их REPLICA IDENTITY FULL

В CDC-эстафету едут три базовые таблицы: drinks (меню), articles (блог), customers (справочник клиентов). На них в схеме Brew уже стоит REPLICA IDENTITY FULL — и это не косметика.

По умолчанию (REPLICA IDENTITY DEFAULT) на UPDATE/DELETE Postgres пишет в WAL только первичный ключ старой строки — этого хватает, чтобы физическая реплика нашла строку. Но Debezium на той стороне строит из потока полноценные события «было → стало», и для UPDATE/DELETE ему нужен before-image — старое состояние строки целиком. С DEFAULT он увидит в before-image один id и не сможет восстановить, что именно изменилось. REPLICA IDENTITY FULL говорит Postgres писать в WAL всю старую строку — тогда before-image содержит все столбцы.

REPLICA IDENTITY DEFAULTREPLICA IDENTITY FULL
В WAL на UPDATE/DELETEтолько PK старой строкився старая строка
before-image для Debeziumодин idвсе столбцы (у drinks — 9)
Хватает физической репликедада
Хватает CDC «было → стало»нетда
Цена в WALминимальнаярастёт на горячих/широких строках
В нашей схеме Brewdrinks, articles, customers

PUBLICATION: явный список вместо автосоздания

Поток адресуют публикацией:

sql
CREATE PUBLICATION dbz_publication FOR TABLE drinks, articles, customers;

Мы перечисляем таблицы руками, а не включаем publication.autocreate на стороне Debezium. Так в одном месте видно ровно то, что уходит в поток: три таблицы, ни больше ни меньше. Убрать таблицу из стрима — это ALTER PUBLICATION dbz_publication DROP TABLE <name>, добавить — ADD TABLE. Никакой магии «само подцепит всё, что нашлось».

Доказательство шва через test_decoding

Собрать конфигурацию мало — надо показать, что before-image действительно несёт всю строку. Для этого демо создаёт временный логический слот репликации с выходным плагином test_decoding, делает один UPDATE напитка, вычитывает изменения слота через pg_logical_slot_get_changes и считает, сколько столбцов попало в сегмент old-key (это и есть before-image в формате test_decoding). Под REPLICA IDENTITY FULL там оказываются все 9 столбцов drinks, а не один id. Слот после проверки сразу сносится.

test_decoding — это отладочный плагин, который печатает изменения текстом; Debezium в бою использует свой декодер, а не этот. Нам он нужен ровно чтобы глазами увидеть before-image и убедиться, что FULL работает.

Шов целиком: от UPDATE до Kafka

Собрав части, видно весь путь одного изменения — от записи в Brew до Debezium на стороне kafka-cookbook, без единой строки нашего кода доставки:

plaintext
Сквозной шов: один UPDATE в Brew доезжает до kafka-cookbook
 
  Postgres (этот курс)
    UPDATE drinks
       │  изменение записано

    WAL — журнал упреждающей записи (durability пишет его и так)
       │  logical decoding разбирает WAL обратно в INSERT/UPDATE/DELETE
       │  REPLICA IDENTITY FULL → before-image несёт всю старую строку

    PUBLICATION dbz_publication (drinks, articles, customers)
       │  через логический слот репликации

  kafka-cookbook (следующий курс)
    Debezium → Kafka → Elasticsearch
       db/init.sql байт-совместим — схему на той стороне не переписывают

Своего relay (как в 09-03) тут нет: relay'ем работает сам Postgres + декодер Debezium. Наша задача — отдать корректный поток, и две настройки (PUBLICATION + REPLICA IDENTITY FULL) её решают.

Что показывает наш код

cmd/demo/main.go — это raw-pgx escape-hatch: урок про конфигурацию репликации (PUBLICATION, REPLICA IDENTITY, слоты) и системные функции декодирования — это DDL и pg_*-вызовы, не SQL уровня sqlc, поэтому здесь нет ни query.sql, ни internal/db/. Демо последовательно: накатывает схему Brew, применяет артефакт эстафеты db/init.sql, показывает REPLICA IDENTITY трёх источников, печатает опубликованные таблицы и доказывает before-image через test_decoding. Артефакт db/init.sql — тот самый файл, что байт-совместим с kafka-cookbook и уезжает на её сторону; повторное применение идемпотентно (CREATE TABLE IF NOT EXISTS, повторный ALTER ... REPLICA IDENTITY FULL, DO-блок на публикацию).

Запуск

sh
docker compose up -d
make lecture L=10-use-cases/10-05-the-cdc-seam-handoff T=db-reset
make lecture L=10-use-cases/10-05-the-cdc-seam-handoff

T=run — режим по умолчанию, его можно не писать. Изнутри каталога юнита короче: make db-reset, затем make run. А make test гоняет асмерченный integration-тест: он проверяет, что публикация покрывает ровно три таблицы, что на всех стоит REPLICA IDENTITY FULL и что before-image содержит 9 столбцов (без поднятой песочницы тест делает t.Skip). Юнит требует wal_level=logical — он уже выставлен в корневом docker-compose.yml курса.

plaintext
1) Базовые таблицы на месте, REPLICA IDENTITY FULL на CDC-источниках:
   articles   replica identity: full
   customers  replica identity: full
   drinks     replica identity: full
 
2) Публикация для Debezium (явный список таблиц):
   CREATE PUBLICATION dbz_publication FOR TABLE drinks, articles, customers
   публикует таблицы: articles, customers, drinks
 
3) Проверяем шов логическим декодированием (test_decoding):
   UPDATE drinks #1 → перехвачено изменений в слоте: 3
   before-image (old-key) содержит столбцов: 9 → REPLICA IDENTITY FULL работает
   (без FULL Debezium увидел бы в before-image только id; здесь — всю строку)
 
4) Эстафета: db/init.sql байт-совместим с kafka-cookbook — Debezium
   читает наши таблицы без переписывания схемы. Дальше — Kafka-курс.

Все три источника несут full. Публикация стримит ровно drinks, articles, customers. А ключевая строка — третья: один UPDATE drinks оставил в слоте три изменения (BEGIN/UPDATE/COMMIT), и before-image у UPDATE содержит 9 столбцов — всю строку drinks целиком. Это и есть доказательство, что REPLICA IDENTITY FULL делает свою работу: Debezium получит полное «было», а не огрызок из одного id.

Заборчик

  • Невычитываемый слот держит WAL. Логический слот репликации, который никто не вычитывает, не даёт Postgres удалять сегменты журнала, пока их не подтвердил самый отстающий потребитель — и диск медленно заполняется. Наше демо честно сносит слот в конце, но в проде застрявший слот (умерший Debezium, отключённый consumer) — реальный путь к No space left on device. За слотами надо следить (pg_replication_slots) и подчищать мёртвые — это территория твоего DBA.
  • test_decoding — не то, чем читает Debezium. Это отладочный плагин: печатает изменения текстом для глаз, у Debezium свой декодер. Мы взяли его только чтобы увидеть before-image.
  • REPLICA IDENTITY FULL — это компромисс: за полный before-image платишь WAL. Каждый UPDATE/DELETE теперь пишет в журнал всю старую строку, а не один PK — на горячей таблице с широкими строками это заметный рост объёма WAL и нагрузки на репликацию. На наших трёх справочниках (меню, блог, клиенты) запись редкая, цена копеечная; на high-churn таблице это решение надо взвешивать.
  • Сквозной пайплайн Debezium → Kafka → sinks мы здесь не запускаем. Это уже сторона kafka-cookbook: следующий курс. Наша задача — отдать корректный поток, и она выполнена.

Что забрать с собой

CDC — это способ отдать поток изменений наружу, не написав ни строки доставки: WAL уже журнал изменений, логическое декодирование разбирает его обратно в INSERT/UPDATE/DELETE, а адресуют поток PUBLICATION (какие таблицы) и REPLICA IDENTITY (сколько старой строки писать в WAL). REPLICA IDENTITY FULL кладёт в before-image всю строку — без него потребитель не восстановит UPDATE/DELETE, но и WAL растёт. Это альтернатива transactional outbox из 09-03: там событие писали руками в outbox, здесь источником служит сам журнал БД.

И здесь же закрывается весь курс. Главным героем у нас всё время был SQL: sqlc-юниты держали запросы в центре, а escape-hatch'и (как этот) выходили на уровень DDL, MVCC и системных функций ровно тогда, когда sqlc мешал увидеть суть. Финальный кадр — правило байт-совместимости схемы Brew, которое мы держали с первого модуля: db/init.sql этого юнита дословно совпадает по именам и типам колонок с init.sql на стороне kafka-cookbook (защищено тестом TestInitSQL_ByteCompatTokens), поэтому Debezium читает наши drinks/articles/customers без переписывания схемы. Переименуй здесь хоть одну колонку базовой таблицы — и эстафета рвётся.

Дальше — соседний курс kafka-cookbook (github.com/dsbasko/kafka-cookbook). Он подхватывает ровно этот поток: Debezium слушает нашу dbz_publication, кладёт изменения в Kafka, а оттуда sink'и едут в Elasticsearch и строят поиск по тому же кофейному Brew. Один мир, одна модель данных, два курса — Postgres отдал эстафету, Kafka её принимает. Это был последний юнит. Спасибо, что дошёл до конца.

·Модуль 11

Этот урок ещё впереди

Курс изучается по порядку — чтобы открыть этот шаг, сначала завершите предыдущие. Так контекст накапливается без пропусков.

/ вы пытались открыть
Use cases / Шов CDC: эстафета в kafka-cookbook