Skip to content
Learning Platform
Intermediate
30 minutes
wal-management heartbeat replication-slots production

WAL Bloat и Heartbeat

WAL bloat - это одна из самых частых production-проблем при работе с Debezium и PostgreSQL. Когда replication slot не продвигается, WAL накапливается бесконечно, заполняя диск и приводя базу данных в read-only режим.

Critical: Без правильной защиты abandoned slot может заполнить диск за дни или даже часы на высоконагруженных системах. Это не теоретическая угроза - это происходит регулярно в production.

Проблема WAL Bloat

В уроке о replication slots мы изучили, как slots удерживают WAL. Теперь рассмотрим, почему это становится проблемой.

Сценарий: Low-Traffic Table

Low-Traffic Table Scenario
Orders Table
Replication Slot
WAL Segments
Disk Space
CDC события (1000/час)restart_lsn продвигаетсяСтарые сегменты удаляютсяСтабильно 1GB--- Low-traffic режим ---Нет изменений 7 днейrestart_lsn НЕ МЕНЯЕТСЯ!+500 MB/день накапливаетсяДень 14: Disk full!
Database Read-Only
Production outage
Manual Recovery
Часы downtime

Механизм проблемы:

  1. Slot создается при первом запуске Debezium
  2. restart_lsn показывает последнюю обработанную позицию
  3. PostgreSQL не удаляет WAL сегменты после этой позиции
  4. Если таблица не меняется - slot не продвигается
  5. WAL накапливается от всех транзакций в БД, не только мониторируемых таблиц

Последствия

ЭтапСимптомыВлияние
Warningpg_wal занимает более 50% дискаАлерты мониторинга
CriticalДиск заполнен на более 90%Производительность деградирует
OutageДиск заполнен на 100%PostgreSQL переходит в read-only
RecoveryТребуется ручное вмешательствоDowntime, потенциальная потеря данных

Многослойная защита от WAL Bloat

Правильная защита требует нескольких уровней. Один слой может отказать - многослойность обеспечивает resilience.

Многослойная защита от WAL Bloat

Один слой может отказать - многослойность обеспечивает resilience

Угрозы
Abandoned SlotLow-Traffic TableSlow Consumer
Layer 1: max_slot_wal_keep_size
PostgreSQL parameter - жесткий лимит WAL на slot
Layer 2: Heartbeat Events
Debezium config - принудительное продвижение slot
Layer 3: Monitoring & Alerting
pg_replication_slots - алерты на lag и wal_status
Layer 4: Runbooks & Procedures
Операционные процедуры - чеклисты реагирования
Protected

Layer 1: max_slot_wal_keep_size

Это обязательный параметр PostgreSQL для production CDC. Он устанавливает жесткий лимит WAL retention на каждый slot.

-- Установка лимита (требует перезапуска или reload)
ALTER SYSTEM SET max_slot_wal_keep_size = '10GB';
SELECT pg_reload_conf();

-- Проверка текущего значения
SHOW max_slot_wal_keep_size;

Как работает

WAL Retentionwal_statusПоведение
меньше limitreservedWAL сохраняется, всё нормально
больше limitunreservedWAL может быть удален
WAL удаленlostSlot инвалидирован, требуется resnapshot

Выбор значения

-- Рекомендации по размеру
max_slot_wal_keep_size = '10GB'   -- Небольшие БД (менее 100GB data)
max_slot_wal_keep_size = '50GB'   -- Средние БД (100GB-1TB)
max_slot_wal_keep_size = '100GB'  -- Большие БД (более 1TB)

Формула расчета:

Recommended limit =
    (WAL generation rate per hour) x
    (Maximum acceptable connector downtime in hours) x
    1.5 (safety margin)

Example:
    50 MB/hour x 24 hours x 1.5 = 1.8 GB
    -> Set to 5GB for safety margin

Компромисс: Слишком маленький лимит - slot инвалидируется при кратких проблемах (придется делать полный resnapshot). Слишком большой - не защитит от disk exhaustion.

Aurora PostgreSQL

Для Aurora используйте DB Cluster Parameter Group:

Parameter: rds.logical_replication
Value: 1

Parameter: max_slot_wal_keep_size
Value: 10737418240 (в байтах = 10GB)
Проверка знаний
Replication slot удерживает WAL от ВСЕХ транзакций в базе данных, а не только от мониторируемых таблиц. Почему это делает low-traffic таблицы особенно опасными для WAL bloat?
Ответ
Slot продвигается только при обработке событий от мониторируемых таблиц. Если таблица не меняется, slot замирает на старой позиции. При этом WAL от других активных таблиц продолжает накапливаться — slot не позволяет PostgreSQL удалить эти сегменты. Heartbeat решает проблему, генерируя synthetic events для продвижения slot.

Layer 2: Heartbeat Configuration

Heartbeat заставляет slot продвигаться даже когда мониторируемые таблицы не меняются.

Heartbeat Flow

pg_logical_emit_message() - рекомендуемый подход для PostgreSQL 14+

Debezium
Timer 10s
SELECT
pg_logical_
emit_message()
записывает
WAL
logical msg
читает
Slot
restart_lsn++
Kafka
heartbeat topic
Slot продвигается
даже без изменений в таблицах
Старый WAL удаляется
disk space стабилен

pg_logical_emit_message() vs Heartbeat Table

PostgreSQL 14+ предоставляет функцию pg_logical_emit_message() - это рекомендуемый подход.

Подходpg_logical_emit_message()Heartbeat Table
PostgreSQL версия14+Любая
Требует таблицуНетДа
Нагрузка на БДМинимальнаяUPDATE каждые N секунд
БлокировкиНетВозможны на таблице
НастройкаОдна строка конфигаСоздать таблицу + права

Конфигурация с pg_logical_emit_message (рекомендуется)

{
  "name": "inventory-connector",
  "config": {
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "postgres",
    "database.port": "5432",
    "database.user": "debezium_user",
    "database.password": "secret",
    "database.dbname": "inventory",
    "topic.prefix": "inventory",
    "table.include.list": "public.orders,public.customers",
    "plugin.name": "pgoutput",

    "heartbeat.interval.ms": "10000",
    "heartbeat.action.query": "SELECT pg_logical_emit_message(false, 'heartbeat', now()::varchar)"
  }
}

Параметры heartbeat:

ПараметрЗначениеОписание
heartbeat.interval.ms10000Интервал между heartbeat (10 сек)
heartbeat.action.querySQL запросВыполняется для продвижения slot
heartbeat.topics.prefix__debezium-heartbeatKafka topic для heartbeat events

10 секунд - это рекомендуемое значение, оптимизированное для Aurora failover detection. Меньший интервал создает лишнюю нагрузку, больший - увеличивает время обнаружения проблем.

Конфигурация с heartbeat table (PostgreSQL 13 и ниже)

-- Создание heartbeat таблицы
CREATE TABLE IF NOT EXISTS public.heartbeat (
    id INTEGER PRIMARY KEY,
    ts TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);

-- Начальная запись
INSERT INTO public.heartbeat (id, ts)
VALUES (1, NOW())
ON CONFLICT (id) DO NOTHING;

-- Права для Debezium user
GRANT SELECT, UPDATE ON public.heartbeat TO debezium_user;
{
  "heartbeat.interval.ms": "10000",
  "heartbeat.action.query": "UPDATE public.heartbeat SET ts = NOW() WHERE id = 1",
  "table.include.list": "public.orders,public.customers,public.heartbeat"
}

Layer 3: Мониторинг Slot Health

Comprehensive Monitoring Query

-- Полный мониторинг replication slots
SELECT
    slot_name,
    plugin,
    database,
    active,
    restart_lsn,
    confirmed_flush_lsn,
    -- WAL retention metrics
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal,
    pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS retained_bytes,
    -- Slot health status (PostgreSQL 14+)
    wal_status,
    safe_wal_size,
    -- Health assessment
    CASE
        WHEN NOT active AND pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) > 1073741824
        THEN 'CRITICAL: Inactive slot retaining more than 1GB'
        WHEN wal_status = 'lost'
        THEN 'CRITICAL: Slot invalidated'
        WHEN wal_status != 'reserved'
        THEN 'WARNING: WAL status degraded'
        WHEN NOT active
        THEN 'WARNING: Inactive slot'
        ELSE 'OK'
    END AS health_status
FROM pg_replication_slots
WHERE slot_type = 'logical'
ORDER BY retained_bytes DESC NULLS LAST;

Alert Thresholds

# Grafana alert rules
- name: Replication Slot WAL Retention
  condition: retained_bytes more than 1073741824  # 1 GB
  for: 5m
  severity: warning
  annotations:
    summary: "Slot retaining more than 1GB WAL"

- name: Replication Slot WAL Critical
  condition: retained_bytes more than 5368709120  # 5 GB
  for: 10m
  severity: critical
  annotations:
    summary: "URGENT: Slot retaining more than 5GB WAL"

- name: Slot Invalidated
  condition: wal_status == 'lost'
  for: 1m
  severity: critical
  annotations:
    summary: "Slot invalidated - requires resnapshot"

wal_status Values

ЗначениеОписаниеДействие
reservedWAL зарезервирован, всё в порядкеМониторить размер
extendedWAL превысил max_wal_size, но slot держитСледить внимательно
unreservedWAL может быть удален в любой моментНемедленная проверка коннектора
lostWAL уже удален, slot невосстановимТребуется полный resnapshot
Проверка знаний
Почему heartbeat через pg_logical_emit_message() предпочтительнее heartbeat table для PostgreSQL 14+?
Ответ
pg_logical_emit_message() создает запись напрямую в WAL без необходимости таблицы, UPDATE-операций и блокировок. Heartbeat table требует создания таблицы, настройки прав, добавления в table.include.list, и каждый UPDATE создает дополнительную нагрузку. pg_logical_emit_message() — минимальный overhead с максимальным эффектом.

Layer 4: Operational Runbooks

Runbook: Обнаружен High WAL Retention

Триггер: Alert “Slot retaining more than 1GB WAL”

Шаги:

  1. Проверить статус коннектора:
curl -s http://localhost:8083/connectors/inventory-connector/status | jq '.connector.state, .tasks[0].state'
  1. Если RUNNING но lag растет:
# Проверить логи на ошибки
docker logs connect 2>&1 | tail -100 | grep -i error
  1. Если FAILED:
# Перезапустить task
curl -X POST http://localhost:8083/connectors/inventory-connector/tasks/0/restart
  1. Если коннектор не существует:
# Проверить список коннекторов
curl -s http://localhost:8083/connectors | jq
# Если slot orphaned - эскалировать к владельцу

Runbook: Slot Invalidated (wal_status = ‘lost’)

Триггер: Alert “Slot invalidated”

Это означает: WAL, необходимый для slot, был удален. Продолжение CDC невозможно без действий.

Шаги:

  1. Остановить коннектор:
curl -X PUT http://localhost:8083/connectors/inventory-connector/pause
  1. Удалить orphaned slot:
SELECT pg_drop_replication_slot('debezium_inventory');
  1. Перезапустить коннектор (выполнит initial snapshot):
curl -X PUT http://localhost:8083/connectors/inventory-connector/resume
  1. Мониторить snapshot progress:
# Проверять пока SnapshotCompleted = true
curl -s http://localhost:8083/connectors/inventory-connector/status | jq
  1. Post-mortem: Почему slot остался без присмотра?

Runbook: Подозрение на Abandoned Slot

Триггер: Inactive slot с растущим lag

-- Найти подозрительные slots
SELECT
    slot_name,
    active,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots
WHERE slot_type = 'logical'
  AND active = false
  AND pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) > 104857600; -- more than 100 MB

Шаги:

  1. НЕ удалять сразу! Slot может принадлежать временно остановленному коннектору

  2. Найти владельца:

# Есть ли коннектор с таким slot_name?
curl -s http://localhost:8083/connectors | jq
  1. Если коннектор существует и PAUSED:

    • Связаться с командой-владельцем
    • Выяснить причину паузы и ETA восстановления
  2. Если коннектор не существует:

    • Подтвердить с командой, что коннектор удален навсегда
    • Документировать решение
    • Удалить slot: SELECT pg_drop_replication_slot('slot_name');

Lab: Configure Heartbeat and Verify Slot Advancement

Цель

Настроить heartbeat и убедиться, что slot продвигается даже при отсутствии изменений в мониторируемых таблицах.

Предварительные требования

  • Запущенный lab environment (docker-compose up -d)
  • Коннектор inventory-connector развернут

Шаги

1. Проверить текущую позицию slot:

docker exec -it postgres psql -U postgres -d inventory -c "
SELECT
    slot_name,
    restart_lsn,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots
WHERE slot_name LIKE 'debezium%';
"

2. Обновить коннектор с heartbeat:

curl -X PUT http://localhost:8083/connectors/inventory-connector/config \
  -H "Content-Type: application/json" \
  -d '{
    "connector.class": "io.debezium.connector.postgresql.PostgresConnector",
    "database.hostname": "postgres",
    "database.port": "5432",
    "database.user": "postgres",
    "database.password": "postgres",
    "database.dbname": "inventory",
    "topic.prefix": "inventory",
    "table.include.list": "public.orders,public.customers",
    "plugin.name": "pgoutput",
    "heartbeat.interval.ms": "10000",
    "heartbeat.action.query": "SELECT pg_logical_emit_message(false, '\''heartbeat'\'', now()::varchar)"
  }'

3. Подождать 30 секунд (3 heartbeat cycles):

sleep 30

4. Проверить, что slot продвинулся:

docker exec -it postgres psql -U postgres -d inventory -c "
SELECT
    slot_name,
    restart_lsn,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots
WHERE slot_name LIKE 'debezium%';
"

5. Проверить heartbeat events в Kafka:

docker exec -it kafka kafka-console-consumer \
  --bootstrap-server localhost:9092 \
  --topic __debezium-heartbeat.inventory \
  --from-beginning \
  --max-messages 3 \
  --property print.timestamp=true

Ожидаемый результат

  • restart_lsn должен обновиться (сравнить со Шагом 1)
  • lag должен быть минимальным (менее 1 MB)
  • В Kafka topic видны heartbeat events каждые 10 секунд

Ключевые выводы

  1. WAL bloat - реальная угроза: Без защиты slot заполнит диск, вызвав database outage

  2. Многослойная защита обязательна:

    • Layer 1: max_slot_wal_keep_size (жесткий лимит)
    • Layer 2: Heartbeat (активное продвижение)
    • Layer 3: Мониторинг (раннее обнаружение)
    • Layer 4: Runbooks (готовность к реагированию)
  3. pg_logical_emit_message() - рекомендуемый подход для PostgreSQL 14+

  4. Heartbeat каждые 10 секунд - оптимально для production

  5. Abandoned slot - не просто inactive: active=false может означать временную паузу, всегда проверяйте владельца

  6. wal_status=‘lost’ означает point of no return - требуется полный resnapshot

Production Insight: Настройте мониторинг WAL retention ДО первого инцидента. Большинство production outages от WAL bloat происходят потому, что команда узнала о проблеме только когда диск заполнился.

Check Your Understanding

Score: 0 of 0
Conceptual
Question 1 of 5. Что является корневой причиной WAL bloat при использовании Debezium с PostgreSQL?

Finished the lesson?

Mark it as complete to track your progress