Вторая статья цикла «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/19B48D8LSN изменился: 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_dbPeer 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: кто ещё настраивает это для себя или в проде? Какие грабли встречались? Интересно узнать, что я не один такой, кто возится с этим на домашнем сервере.
