Миграция Oracle → PostgreSQL без простоя: CDC-схема, которая реально работает
Меня зовут Семёнов Евгений Сергеевич, директор АйТи Фреш. За последние два года к нам приходили пять клиентов с одной и той же задачей: уйти с Oracle Database на PostgreSQL, но так, чтобы бизнес ни на минуту не встал. Ни одна из миграций не была простой, и в каждой мы наступали на грабли, о которых в документации написано полтора абзаца. Эту статью собрал как выжимку: что работает, что нет и где разработчики обычно проигрывают неделю-две на пустом месте.
Три стратегии миграции и почему все три больно
Формально существуют три подхода. Перед любым проектом полезно честно признаться самому себе, какой именно вы тянете.
- Офлайн-миграция. Останавливаете приложение в пятницу вечером, сливаете Oracle в дамп через
ora2pgили expdp+парсер, грузите в PostgreSQL, к понедельнику стартуете. Работает до 100 ГБ и простаивающих ночью систем. Для продовой ERP или биллинга — нереально. - Online через CDC. Настраиваете реплику с Oracle на PostgreSQL, она бегает с лагом несколько секунд, переключаете приложение на PostgreSQL и выключаете Oracle. Промежуточный лаг даёт риск потери данных при обрыве — поэтому нужен честный monitoring и контроль положения коммитов.
- Dual-write + CDC. Приложение пишет одновременно в Oracle и PostgreSQL, CDC синхронизирует гарантированно, откат — мгновенный. Но код переписывается в двух местах, транзакционности между БД нет, любая рассинхронизация заставляет останавливать продакшен и лечить руками. Видел проект, где на dual-write ушло три месяца и ничего не заработало — вернулись к CDC-only.
В девяти случаях из десяти оптимум — онлайн через CDC без dual-write. Дальше разберёмся, как его собрать правильно.
Архитектура: Debezium LogMiner → Kafka → JDBC Sink
Схема, которая реально едет в проде:
- Debezium Source-коннектор с адаптером LogMiner читает redo-логи Oracle и превращает каждую DML-операцию в событие в Kafka.
- Kafka хранит события в топиках, по одному на таблицу. Порядок внутри топика гарантирован через partition key.
- 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:
| Oracle | PostgreSQL | Комментарий |
|---|---|---|
| NUMBER(1,0) | BOOLEAN | Если фактически используется 0/1 как флаг |
| NUMBER(precision,0) | INTEGER / BIGINT | По разрядности |
| NUMBER(p,s) | NUMERIC(p,s) | Для денег и дробных |
| VARCHAR2(n) | VARCHAR(n) | Но кодировка UTF-8, не cp1251 |
| CLOB | TEXT | Размер не ограничен |
| DATE | TIMESTAMP(0) | Oracle DATE хранит время, date в PostgreSQL — нет |
| BLOB | BYTEA | До 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, о которые спотыкаются все:
- Имена объектов строго до 30 символов. В Oracle 12c+ лимит вырос до 128, но LogMiner до сих пор молча игнорирует длинные имена. Ищете в логах connect:
WARN Schema 'X' object name too long— значит таблица потеряна. - Неподдерживаемые типы. XMLTYPE, SDO_GEOMETRY, AnyData — всё это LogMiner не реплицирует. Решение: конвертировать в CLOB или изменить архитектуру.
- ROWID не реплицируется. Если приложение использует ROWID как ключ (такие уроды встречаются) — понадобится SQL-миграция логики.
- Supplemental logging обязателен. Без
ALTER DATABASE ADD SUPPLEMENTAL LOG DATA (ALL) COLUMNSLogMiner не увидит значения неизменённых колонок в UPDATE, и upsert на стороне PG сломается.
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"
}
}
Что делают трансформации:
ExtractNewRecordStateразворачивает сложную структуру Debezium (before/after/op/source) в плоскую запись, пригодную для INSERT.RegexRouterпереименовывает Kafka-топики в имена таблиц: изerp.ERPUSER.ORDERSделаетordersв PG.- При DELETE режим
rewriteпомечает запись tombstone, SinkConnector выполнитDELETE FROM orders WHERE id=?.
Составные ключи: "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-2. Инфраструктура: развёртывание Kafka-кластера (3 брокера, NVMe, сеть 10G), PostgreSQL HA через Patroni, мониторинг в Zabbix и Grafana.
- Недели 3-4. Подготовка схемы через Liquibase, DDL-чейнджсеты прогнаны в dev. Написан скрипт валидации: сравнение
COUNT(*)по всем таблицам Oracle и PG. - Неделя 5. Debezium на staging: поток 6 000 событий/сек, стартовая синхронизация 320 ГБ заняла 3:40.
- Недели 6-10. Миграция микросервисов от простых к сложным. Каждый сервис запускался в новом namespace с подключением к PG, feature toggle в конфиге, трафик переключался через балансировщик. Первым пошёл сервис уведомлений (тесто внутри), последним — биллинг-ядро.
- Неделя 11. Чистка: отключение Oracle (оставлен в readonly на 30 дней для отката), удаление Debezium-коннекторов, финальный дамп.
Результат: миграция прошла без инцидентов, клиент отказался от Oracle на отметке 11 недель. Стоимость проекта — 2.1 млн рублей, экономия в первый год 2.7 млн (4.8 лицензия минус 0.9 поддержка PostgreSQL и железо). Окупилось за 10 месяцев.
Где мы наступили на грабли
Честный список проблем, которые стоили от дня до недели:
- DELETE-шторм замедлил лаг. Один из сервисов раз в сутки делал массовый DELETE на 2 млн строк. Debezium Source выплёвывал 2 млн событий, Sink давился JDBC-операциями по одной строке. Решение: заменили DELETE на soft-delete (update флага
is_deleted), лаг упал с 90 минут до 8 секунд. - Таблица без PK. Лог-таблица на 400 млн строк не имела первичного ключа. В режиме upsert Debezium отказывался писать. Добавили сурогатный PK через
ALTER TABLE ADD COLUMN id BIGSERIAL PRIMARY KEY— час работы. - Connection pool PG зависал. При пиковых нагрузках Debezium открывал более 200 соединений и ждал транзакций. PGBouncer нагнул приложение. Выставили
max.retries: 3,connection.pool.max: 20иretry.backoff.ms: 5000. - Имена в верхнем регистре. Oracle хранит имена UPPERCASE, PG по умолчанию нижний. Решили через
transforms.route: применялиtoLower. - Часовые пояса. Oracle хранит TIMESTAMP без TZ, приложение считало что это Europe/Moscow. PG по умолчанию UTC. Настроили
database.time_zone: Europe/Moscowв Sink.
Чек-лист перед продакшеном
- Supplemental logging включён на Oracle уровня DATABASE или TABLE.
- Пользователь Debezium имеет права
SELECT ANY TRANSACTION,LOGMINING,SELECT ON V_$ARCHIVED_LOG. - Kafka retention минимум 7 дней, чтобы пережить откат коннектора.
- Все последовательности PostgreSQL сдвинуты с запасом.
- Скрипт валидации
COUNT(*)и выборочных SHA256-сравнений строк работает автоматически. - Мониторинг Debezium в Grafana с алертом на lag > 60 секунд.
- Документ «Как откатиться за 15 минут» с конкретными шагами, проверенный на staging.
- Oracle остаётся живым минимум 30 дней после переключения.
Мигрируем с 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 даёт ещё более безопасный откат, но удваивает сложность.