Вторая статья цикла «CDC Pipeline в домашней лаборатории». В первой мы сделали Telegram-бота для парсинга банковских скриншотов. Теперь подготовим PostgreSQL к тому, чтобы Debezium мог захватывать изменения в реальном времени.
Зачем я это пишу
Честно: в первую очередь это мои заметки. На работе я поддерживаю CDC-пайплайны, но разбираться в чужой конфигурации — не то же самое, что понимать систему изнутри. Поэтому строю с нуля дома.
Во вторую очередь — портфолио. Если буду менять работу, эти статьи можно приложить к резюме: «вот, смотрите, я не просто кнопки нажимаю, а понимаю что делаю».
И в третью — мотивация не бросить. Если хотя бы десять человек прочитают и кому-то пригодится, значит не зря потратил выходные.
Так что если вы тоже настраиваете CDC для себя или учитесь — добро пожаловать, разберёмся вместе.
Зачем эта статья (технически)
У меня есть PostgreSQL с таблицей транзакций. Budget Parser из первой статьи уже пишет туда данные. Следующий шаг — научить Debezium читать изменения из WAL и отправлять их в Kafka.
Но просто так Debezium к PostgreSQL не подключится. Нужно:
Включить wal_level = logical
Создать publication для нужных таблиц
Настроить пользователя с правами на репликацию
Открыть сетевой доступ
Эта статья — пошаговая настройка всего перечисленного.
Почему не polling?
Можно не заморачиваться с CDC, а просто опрашивать таблицу по updated_at:
SELECT * FROM transactions WHERE updated_at > :last_check;
Проблемы:
Нагрузка на базу — постоянные запросы, даже когда ничего не изменилось
DELETE невидим — запись исчезла, как узнать какая?
Нужен индекс на updated_at + колонка в каждой таблице
CDC работает иначе: PostgreSQL сам сообщает об изменениях через механизм логической репликации. Никаких лишних запросов, все операции (INSERT/UPDATE/DELETE) видны.
Что такое WAL и зачем он нужен
WAL (Write-Ahead Log) — журнал, куда PostgreSQL записывает все изменения до того, как применит их к данным. Если сервер упадёт — при старте он прочитает WAL и восстановит консистентное состояние.
Но WAL можно использовать не только для recovery. Его можно декодировать и получать информацию об изменениях в читаемом формате.
Три уровня wal_level
Уровень | Что записывается | Для чего |
minimal | Только для crash recovery | Максимальная производительность |
replica | + данные для физической репликации | Standby-серверы, read replicas |
logical | + информация о конкретных изменениях в строках | CDC, Debezium |
Ключевое отличие:
replica: WAL содержит «Изменён блок X на странице Y» — физический уровень
logical: WAL содержит «INSERT в таблицу users: id=1, name='Иван'» — логический уровень с данными
Именно logical нужен для CDC. Debezium подключается к PostgreSQL, читает WAL и преобразует изменения в события для Kafka.
🔍 Как WAL гарантирует сохранность данных
Почему сначала WAL, а потом данные?
Запись в WAL — последовательная. Диск просто дописывает в конец файла. Это быстро.
Запись страницы данных — случайная. Надо найти нужное место, перезаписать. Это в 10-100 раз медленнее.
Если сервер упадёт между этими операциями — при старте PostgreSQL прочитает WAL и «доиграет» незаписанные изменения. Это называется recovery:
database system was not properly shut down; automatic recovery in progress
redo starts at 0/40D04D0
redo done at 0/40D14E0
LSN (Log Sequence Number) — адрес записи в WAL, как номер страницы и строки в книге:
LSN = 0/19B3A58
│ └── смещение внутри сегмента
└──── номер сегмента
Debezium использует LSN чтобы знать «где я остановился». При рестарте он продолжит с сохранённой позиции — ничего не потеряется.
Влияние на производительность
Переход с replica на logical увеличивает объём WAL. В моём home lab с парой сотен транзакций в месяц это незаметно. В production с миллионами записей — стоит замерить.
Практика: настройка PostgreSQL
Инфраструктура
Использую LXC-контейнер в Proxmox:
ID: 302
Hostname: pg-source
IP: 192.168.0.151
RAM: 2GB
Disk: 16GB
Команда создания контейнера (для Proxmox)
pct create 302 local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst \ --hostname pg-source \ --memory 2048 \ --cores 2 \ --rootfs local-lvm:16 \ --net0 name=eth0,bridge=vmbr0,ip=192.168.0.151/24,gw=192.168.0.1 \ --nameserver 8.8.8.8 \ --unprivileged 1 \ --features nesting=1 pct start 302 pct enter 302
Шаг 1: Установка PostgreSQL 16
apt update && apt upgrade -y apt install -y postgresql-16 postgresql-contrib-16
Шаг 2: Включаем logical replication
Редактируем /etc/postgresql/16/main/postgresql.conf:
cat >> /etc/postgresql/16/main/postgresql.conf << 'EOF' # === CDC / Logical Replication === wal_level = logical # Записывать логические изменения max_replication_slots = 4 # Количество слотов репликации max_wal_senders = 4 # Параллельные подключения для репликации EOF
Что означают параметры:
Параметр | Зачем |
wal_level = logical | Включает запись логических изменений в WAL |
max_replication_slots | Сколько «подписчиков» может читать WAL. Слот — это закладка: PostgreSQL не удалит WAL-сегменты, пока слот их не прочитает |
max_wal_senders | Сколько клиентов могут одновременно читать WAL |
Важно: нужен именно restart, не reload:
systemctl restart postgresql
В чём разница:
reload — PostgreSQL перечитывает конфиг (pg_hba.conf, некоторые параметры), но процесс не перезапускается
restart — полный перезапуск процесса, применяются все параметры
wal_level — параметр, который читается только при старте. Поэтому reload его не изменит.
Проверяем:
sudo -u postgres psql -c "SHOW wal_level;" wal_level ----------- logical
Шаг 3: Создаём базу и таблицу
sudo -u postgres psql CREATE DATABASE budget_db; \c budget_db -- Схема для изоляции CREATE SCHEMA finance; -- Таблица транзакций CREATE TABLE finance.parsed_transactions ( id SERIAL PRIMARY KEY, transaction_date DATE, merchant VARCHAR(255), amount NUMERIC(12,2) NOT NULL, transaction_type VARCHAR(20) DEFAULT 'expense', category VARCHAR(100), balance_after NUMERIC(12,2), source_image_hash VARCHAR(64), is_verified BOOLEAN DEFAULT FALSE, verified_at TIMESTAMP, original_amount NUMERIC(12,2), original_merchant VARCHAR(255), parsed_at TIMESTAMP DEFAULT NOW() ); -- Индексы CREATE INDEX idx_parsed_tx_date ON finance.parsed_transactions(transaction_date); CREATE INDEX idx_parsed_tx_verified ON finance.parsed_transactions(is_verified); COMMENT ON TABLE finance.parsed_transactions IS 'Транзакции из банковских скриншотов (Budget Parser)';
Почему одна таблица?
Для изучения CDC достаточно одной таблицы — все принципы (INSERT/UPDATE/DELETE, replication slot, publication) работают одинаково. В будущем добавлю таблицы категорий и бюджетов — это позволит посмотреть, как CDC обрабатывает связанные данные.
Шаг 4: Создаём пользователей
Принцип минимальных привилегий: каждый делает только своё.
-- Пользователь для Debezium (CDC) — только чтение + репликация
CREATE USER debezium WITH REPLICATION PASSWORD 'debezium_pass'; GRANT CONNECT ON DATABASE budget_db TO debezium; GRANT USAGE ON SCHEMA finance TO debezium; GRANT SELECT ON ALL TABLES IN SCHEMA finance TO debezium; ALTER DEFAULT PRIVILEGES IN SCHEMA finance GRANT SELECT ON TABLES TO debezium; -- Пользователь для Budget Parser — запись данных CREATE USER parser WITH PASSWORD 'parser_pass'; GRANT CONNECT ON DATABASE budget_db TO parser; GRANT USAGE ON SCHEMA finance TO parser; GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA finance TO parser; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA finance TO parser; ALTER DEFAULT PRIVILEGES IN SCHEMA finance GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO parser; ALTER DEFAULT PRIVILEGES IN SCHEMA finance GRANT USAGE, SELECT ON SEQUENCES TO parser;
Пользователь | Права | Назначение |
debezium | REPLICATION + SELECT | Читает WAL и данные таблиц |
parser | INSERT/UPDATE/DELETE | Budget Parser пишет транзакции |
Шаг 5: Настраиваем сетевой доступ
PostgreSQL по умолчанию слушает только localhost. Debezium будет подключаться с другого хоста.
# Слушать на всех интерфейсах echo "listen_addresses = '*'" >> /etc/postgresql/16/main/postgresql.conf # Разрешить подключения из локальной сети cat >> /etc/postgresql/16/main/pg_hba.conf << 'EOF' # CDC Pipeline - local network access host budget_db debezium 192.168.0.0/24 scram-sha-256 host budget_db parser 192.168.0.0/24 scram-sha-256 host replication debezium 192.168.0.0/24 scram-sha-256 EOF systemctl restart postgresql
Что означают строки в pg_hba.conf:
Строка | Смысл |
host budget_db debezium 192.168.0.0/24 scram-sha-256 | debezium может подключаться к budget_db из сети 192.168.0.x с паролем |
host replication debezium ... | debezium может использовать протокол репликации (для чтения WAL) |
Проверяем:
psql -h 192.168.0.151 -U debezium -d budget_db -c "SELECT 1;"
Шаг 6: Создаём Publication
Publication говорит PostgreSQL: «Отслеживай изменения в этих таблицах».
sudo -u postgres psql -d budget_db CREATE PUBLICATION finance_publication FOR TABLE finance.parsed_transactions; Проверяем: SELECT * FROM pg_publication_tables; pubname | schemaname | tablename -----------------------+------------+----------------------- finance_publication | finance | parsed_transactions
Проверяем связку с Budget Parser
Теперь убедимся, что всё работает вместе. На контейнере с Budget Parser (192.168.0.150) в .env:
DB_HOST=192.168.0.151 DB_PORT=5432 DB_NAME=budget_db DB_USER=parser DB_PASSWORD=parser_pass
Смотрим текущие данные и позицию в WAL:
SELECT id, transaction_date, merchant, amount FROM finance.parsed_transactions ORDER BY parsed_at DESC; id | transaction_date | merchant | amount ----+------------------+---------------------------+--------- 4 | 2026-01-08 | YM*DEEPHOST | -313.95 3 | 2026-01-08 | VITA apteka 26 Samara RUS | -575.00 2 | 2026-01-08 | Красное&Белое | -440.83 1 | 2026-01-08 | SAMARA_SAMARSK_OBL_TPP | -37.00 SELECT pg_current_wal_lsn(); pg_current_wal_lsn -------------------- 0/19B3A58
Отправляем ещё один скриншот боту... и проверяем снова:
SELECT id, transaction_date, merchant, amount FROM finance.parsed_transactions ORDER BY parsed_at DESC; id | transaction_date | merchant | amount ----+------------------+---------------------------+--------- 5 | 2026-01-08 | SAMARA_SAMARSK_OBL_TPP | -37.00 -- новая! 4 | 2026-01-08 | YM*DEEPHOST | -313.95 3 | 2026-01-08 | VITA apteka 26 Samara RUS | -575.00 2 | 2026-01-08 | Красное&Белое | -440.83 1 | 2026-01-08 | SAMARA_SAMARSK_OBL_TPP | -37.00 SELECT pg_current_wal_lsn(); pg_current_wal_lsn -------------------- 0/19B48D8
LSN изменился: 0/19B3A58 → 0/19B48D8. Каждая новая транзакция двигает позицию в WAL. Когда подключим Debezium — он начнёт читать с определённого LSN и не пропустит ни одного изменения.
Аутентификация: peer vs password
Момент, который может запутать:
# Работает без пароля (peer auth) sudo -u postgres psql # Требует пароль (сетевое подключение) psql -h 192.168.0.151 -U debezium -d budget_db
Peer auth: PostgreSQL смотрит на системного пользователя. Если это postgres — пускает как суперюзера базы.
Password auth: При сетевом подключении нужен пароль. Это то, что мы настроили в pg_hba.conf.
Типичные грабли
Настройка прошла без серьёзных проблем. Но вот типичные грабли, о которых стоит знать заранее:
1. wal_level не применился
systemctl reload postgresql не меняет wal_level. Нужен именно restart.
2. pg_hba.conf: порядок строк важен
PostgreSQL читает правила сверху вниз и применяет первое совпавшее. Если выше есть host all all 0.0.0.0/0 reject — ваши правила не сработают.
3. Replication slot не удаляется
Когда подключите Debezium — он создаст replication slot. Если Debezium упадёт, слот останется. PostgreSQL не будет удалять WAL-сегменты, пока слот их не прочитает. Диск может переполниться.
-- Посмотреть слоты SELECT slot_name, active FROM pg_replication_slots; -- Удалить неактивный слот SELECT pg_drop_replication_slot('debezium_slot');
4. Publication не включает новые таблицы
Если создали publication для конкретных таблиц — новые туда не попадут автоматически:
-- Добавить вручную ALTER PUBLICATION finance_publication ADD TABLE finance.new_table; -- Или сразу для всей схемы CREATE PUBLICATION finance_publication FOR TABLES IN SCHEMA finance;
Полезные команды
# Проверить WAL-параметры sudo -u postgres psql -c "SHOW wal_level; SHOW max_replication_slots;" # Посмотреть публикации sudo -u postgres psql -d budget_db -c "SELECT * FROM pg_publication_tables;" # Replication slots (появится после Debezium) sudo -u postgres psql -d budget_db -c "SELECT * FROM pg_replication_slots;" # Текущая позиция в WAL sudo -u postgres psql -c "SELECT pg_current_wal_lsn();" # Статус PostgreSQL systemctl status postgresql
Итог
PostgreSQL готов к CDC:
Что | Статус |
wal_level = logical | ✅ |
Publication создана | ✅ |
Пользователь с REPLICATION | ✅ |
Сетевой доступ | ✅ |
Budget Parser подключён | ✅ |
Текущая архитектура:

Жёлтая зона — первая статья. Синяя — эта статья. В следующей подключим Debezium и Kafka.
Что дальше
В следующей статье поднимем Kafka и Debezium Connect. Debezium подключится к нашему PostgreSQL, создаст replication slot и начнёт захватывать изменения в реальном времени.
# | Статья | Статус |
1 | ✅ Опубликовано | |
2 | PostgreSQL для CDC | ✅ Эта статья |
3 | Kafka + Debezium | ⏳ Следующая |
4 | HDFS + Hive | 📋 Планируется |
5 | Мониторинг в Grafana | 📋 Планируется |
Ссылки
Расскажите про свой опыт с CDC: кто ещё настраивает это для себя или в проде? Какие грабли встречались? Интересно узнать, что я не один такой, кто возится с этим на домашнем сервере.
