· 14 мин чтения

Миграция Oracle → PostgreSQL без простоя: CDC-схема, которая реально работает

Меня зовут Семёнов Евгений Сергеевич, директор АйТи Фреш. За последние два года к нам приходили пять клиентов с одной и той же задачей: уйти с Oracle Database на PostgreSQL, но так, чтобы бизнес ни на минуту не встал. Ни одна из миграций не была простой, и в каждой мы наступали на грабли, о которых в документации написано полтора абзаца. Эту статью собрал как выжимку: что работает, что нет и где разработчики обычно проигрывают неделю-две на пустом месте.

Три стратегии миграции и почему все три больно

Формально существуют три подхода. Перед любым проектом полезно честно признаться самому себе, какой именно вы тянете.

В девяти случаях из десяти оптимум — онлайн через CDC без dual-write. Дальше разберёмся, как его собрать правильно.

Архитектура: Debezium LogMiner → Kafka → JDBC Sink

Схема, которая реально едет в проде:

  1. Debezium Source-коннектор с адаптером LogMiner читает redo-логи Oracle и превращает каждую DML-операцию в событие в Kafka.
  2. Kafka хранит события в топиках, по одному на таблицу. Порядок внутри топика гарантирован через partition key.
  3. Debezium JDBC Sink читает из топиков и пишет в PostgreSQL через JDBC в режиме upsert.

Стандартный стек, который мы разворачиваем у клиентов:

# docker-compose.yml — минимальный набор
services:
  zookeeper: { image: confluentinc/cp-zookeeper:7.5.0 }
  kafka:     { image: confluentinc/cp-kafka:7.5.0 }
  connect:   { image: debezium/connect:2.5 }
  schema-registry: { image: apicurio/apicurio-registry:2.5 }
  kafka-ui:  { image: provectuslabs/kafka-ui:latest }
  connect-ui:{ image: landoop/kafka-connect-ui:0.9.7 }

На этапе выбора Debezium против Oracle GoldenGate поколения ответа нет: GoldenGate — быстрее, стабильнее и дороже на порядок. Для компаний, которые специально уходят с Oracle из-за стоимости, брать GoldenGate — это крокодил, съевший собственный хвост. Плюс санкции 2022 года заблокировали Oracle-лицензии в России, поэтому в нашем сегменте MSB альтернативы Debezium фактически нет.

Схема данных: Liquibase и мини-таблица преобразований

Перед запуском CDC нужно подготовить схему в PostgreSQL. Ручное копирование DDL — мучение на таблицах от 50 штук. Мы используем Liquibase с контекстами: DDL-чейнджсеты применяются сразу, DML отключены до момента синхронизации, чтобы не вступали в конфликт с потоком событий.

Ключевые преобразования типов Oracle → PostgreSQL:

OraclePostgreSQLКомментарий
NUMBER(1,0)BOOLEANЕсли фактически используется 0/1 как флаг
NUMBER(precision,0)INTEGER / BIGINTПо разрядности
NUMBER(p,s)NUMERIC(p,s)Для денег и дробных
VARCHAR2(n)VARCHAR(n)Но кодировка UTF-8, не cp1251
CLOBTEXTРазмер не ограничен
DATETIMESTAMP(0)Oracle DATE хранит время, date в PostgreSQL — нет
BLOBBYTEAДо 1 ГБ, или Large Object для больших
RAW(16)UUID / BYTEAЕсли это UUID — конвертируйте

Отдельная боль — последовательности. Oracle SEQUENCE и PostgreSQL SEQUENCE отличаются поведением NEXTVAL. Мы всегда смотрим текущее значение Oracle-последовательности и сдвигаем Postgres на запас:

-- На момент старта приложения
SELECT setval('users_id_seq', (SELECT MAX(id) FROM users) + 1000, false);

Запас в 1000 нужен на случай, если CDC ещё догоняет хвост и приложение уже пишет.

Конфиг Source: LogMiner и его ограничения

Минимальный рабочий конфиг Debezium Source для Oracle (в реальных проектах длиннее в 2-3 раза, но суть та же):

{
  "name": "erp-oracle-source",
  "config": {
    "connector.class": "io.debezium.connector.oracle.OracleConnector",
    "database.hostname": "oracle-db.internal",
    "database.port": "1521",
    "database.user": "c##dbz_user",
    "database.password": "${file:/run/secrets/oracle-pass:password}",
    "database.dbname": "ORCL",
    "database.connection.adapter": "logminer",
    "log.mining.strategy": "online_catalog",
    "log.mining.batch.size.default": "20000",
    "table.include.list": "ERPUSER\\.ORDERS,ERPUSER\\.CUSTOMERS",
    "topic.prefix": "erp",
    "snapshot.mode": "initial",
    "key.converter": "io.apicurio.registry.utils.converter.AvroConverter",
    "value.converter": "io.apicurio.registry.utils.converter.AvroConverter",
    "value.converter.apicurio.registry.url": "http://schema-registry:8080/apis/registry/v2"
  }
}

LogMiner читает redo-логи Oracle и восстанавливает SQL-команды, которые вызвали изменения. Это дорого по CPU на стороне Oracle — мы нагружали прод-сервер на 18–22% только за счёт LogMiner, и клиенту пришлось временно добавить vCPU.

Альтернатива — XStream, но он требует Golden Gate-лицензии, которой у вас, очевидно, нет, раз вы уходите с Oracle.

Ограничения LogMiner, о которые спотыкаются все:

Avro вместо JSON — обязательная оптимизация

По умолчанию Debezium пишет в Kafka JSON с embedded schema. На одной из миграций мы в первые сутки получили Kafka-кластер, который еле переваривал входящий поток: 3 ГБ в минуту, retention в 7 дней требует 30 ТБ дисков, что у клиента вызвало инфаркт.

Переключение на Avro через Apicurio Schema Registry дало сжатие в 12 раз. Схема хранится в реестре, в каждом сообщении — только ссылка и данные. Итог: 250 МБ вместо 3 ГБ, кластер на 3 ТБ вместо 30 ТБ.

"key.converter": "io.apicurio.registry.utils.converter.AvroConverter",
"key.converter.apicurio.registry.url": "http://schema-registry:8080/apis/registry/v2",
"key.converter.apicurio.registry.auto-register": "true",
"key.converter.apicurio.registry.find-latest": "true",
"value.converter": "io.apicurio.registry.utils.converter.AvroConverter",
"value.converter.apicurio.registry.url": "http://schema-registry:8080/apis/registry/v2",
"value.converter.apicurio.registry.auto-register": "true",
"value.converter.apicurio.registry.find-latest": "true"

Второй бонус — типизация. Реестр знает, что amount это decimal(18,2), и JDBC Sink выставит правильный тип в INSERT. На JSON всё уходит строками, и PG ругается на каждый коммит.

JDBC Sink: upsert, transforms и составные ключи

Типовой конфиг для записи в PostgreSQL:

{
  "name": "erp-pg-sink",
  "config": {
    "connector.class": "io.debezium.connector.jdbc.JdbcSinkConnector",
    "topics": "erp.ERPUSER.ORDERS,erp.ERPUSER.CUSTOMERS",
    "connection.url": "jdbc:postgresql://pg-primary:5432/erp",
    "connection.username": "erp_app",
    "insert.mode": "upsert",
    "delete.enabled": "true",
    "primary.key.mode": "record_key",
    "primary.key.fields": "ID",
    "schema.evolution": "basic",
    "database.time_zone": "Europe/Moscow",
    "transforms": "unwrap,route",
    "transforms.unwrap.type": "io.debezium.transforms.ExtractNewRecordState",
    "transforms.unwrap.delete.handling.mode": "rewrite",
    "transforms.route.type": "org.apache.kafka.connect.transforms.RegexRouter",
    "transforms.route.regex": "erp\\.ERPUSER\\.(.*)",
    "transforms.route.replacement": "$1"
  }
}

Что делают трансформации:

Составные ключи: "primary.key.fields": "COMPANY_ID,ORDER_ID". Работает, но требует тщательной проверки — Debezium не всегда правильно восстанавливает порядок полей.

Реальный кейс: миграция биллинга за 11 недель

В июле 2025 года к нам обратился оператор связи, работающий в нескольких регионах ЦФО (до 50 административных офисов, ~12 000 абонентов бизнес-сегмента). Биллинг на Oracle 12c Standard, 14 микросервисов на Spring Boot, 620 таблиц, 1.4 ТБ данных. Причина миграции — продление лицензии Oracle обходилось в 4.8 млн рублей в год, и продавец отказался возобновлять поддержку из-за санкций.

Мы работали восьмью руками: два бэкенда, один DBA, один сисадмин (я). Плюс один инженер на стороне клиента.

Последовательность:

  1. Недели 1-2. Инфраструктура: развёртывание Kafka-кластера (3 брокера, NVMe, сеть 10G), PostgreSQL HA через Patroni, мониторинг в Zabbix и Grafana.
  2. Недели 3-4. Подготовка схемы через Liquibase, DDL-чейнджсеты прогнаны в dev. Написан скрипт валидации: сравнение COUNT(*) по всем таблицам Oracle и PG.
  3. Неделя 5. Debezium на staging: поток 6 000 событий/сек, стартовая синхронизация 320 ГБ заняла 3:40.
  4. Недели 6-10. Миграция микросервисов от простых к сложным. Каждый сервис запускался в новом namespace с подключением к PG, feature toggle в конфиге, трафик переключался через балансировщик. Первым пошёл сервис уведомлений (тесто внутри), последним — биллинг-ядро.
  5. Неделя 11. Чистка: отключение Oracle (оставлен в readonly на 30 дней для отката), удаление Debezium-коннекторов, финальный дамп.

Результат: миграция прошла без инцидентов, клиент отказался от Oracle на отметке 11 недель. Стоимость проекта — 2.1 млн рублей, экономия в первый год 2.7 млн (4.8 лицензия минус 0.9 поддержка PostgreSQL и железо). Окупилось за 10 месяцев.

Где мы наступили на грабли

Честный список проблем, которые стоили от дня до недели:

Чек-лист перед продакшеном

Мигрируем с Oracle на PostgreSQL без простоя — от 1.5 млн

Я лично веду проекты миграции баз данных для среднего бизнеса в Москве и области. Debezium + Kafka + PostgreSQL, последовательная перепрошивка микросервисов, контролируемый откат. Типовой проект — 8-14 недель при 8-15 микросервисах и объёме до 2 ТБ. Предварительный аудит инфраструктуры бесплатно, расчёт и план — за 3 рабочих дня.

Телефон: +7 903 729-62-41
Telegram: @ITfresh_Boss
Семёнов Евгений Сергеевич, директор АйТи Фреш

FAQ — миграция Oracle → PostgreSQL

Почему Debezium, а не Oracle GoldenGate?
Debezium — Open Source, бесплатный и проще в развёртывании. GoldenGate стоит дорого и покрыт санкциями с 2022 года. Для большинства бизнес-сценариев Debezium LogMiner покрывает все задачи CDC с лагом в несколько секунд.
Сколько длится миграция микросервисной системы?
Зависит от объёма и количества сервисов. Наш клиент с 14 микросервисами, 620 таблиц и 1.4 ТБ данных в Oracle завершил миграцию за 11 недель, перенося по 1-2 сервиса в неделю. Стартовая синхронизация тяжёлых таблиц занимала 3-5 часов.
Как бороться с большим объёмом JSON в Kafka?
Переключайте Debezium на Avro через Apicurio Schema Registry. Мы получили сжатие в 12 раз: 3 ГБ топика с JSON превратились в 250 МБ Avro. Это критично при нагрузке свыше 50 сообщений в секунду.
Что делать с таблицами без первичного ключа?
Debezium JDBC Sink в режиме upsert требует PK. Варианты: добавить синтетический PK в целевой БД, переключиться в insert mode (без обновлений) или использовать уникальный индекс. Последний вариант рискован при дубликатах.
Как откатиться, если на проде всё сломалось?
Перед переключением приложения на PostgreSQL сохраняете дамп Oracle в текущем состоянии и не отключаете редо-логи минимум неделю. Если что-то пошло не так — переключаете приложение обратно на Oracle. Dual-write даёт ещё более безопасный откат, но удваивает сложность.