Представьте задачу: взять рабочий высоконагруженный государственный сервис с огромной БД под 50 терабайт, тесно интегрированной с бизнес-процессами и используемой тысячами пользователей, и практически безболезненно перенести его с Oracle на Postgres Pro. Те, кто знает цену таких решений, сразу предполагают масштаб бедствия: простои, падения производительности, потерянные данные… Но всё же миграция состоялась, и мы с ОТР расскажем как.

Проект импортозамещения, в рамках которого проводились работы, стартовал в 2024 году и включал миграцию трёх систем разного профиля. К ноябрю настал черёд самой сложной: высоконагруженной, с огромной базой данных и высокой частотой обновлений.
Оптимизация данных от 52 ТБ к 40 ТБ и стратегии этапов
Изначально база данных Oracle весила внушительные 52 ТБ. Первым делом провели инвентаризацию. «В топе у нас вылезли логи, исторические данные», — рассказывает Ирина Токарева. По согласованию с заказчиком, эти данные решили не переносить в Postgres Pro, а оставить в Oracle, переместив их после миграции на более медленные диски и другие серверы. Таким образом, к началу миграции подошли с объёмом в 40 ТБ.
Тестовая миграция этих 40 ТБ показала, что полный перенос займёт 5 дней — непозволительная роскошь для критически важной системы. Стало очевидно: единственный путь — поэтапная миграция.
Стратегия выглядела так:
Миграция этапами.
Инструмент для «большого» переноса — стандартная утилита ora2pg.
Синхронизация изменений (diff). Между этапами накатывается разница данных, наработанная пользователями в промышленной среде (PROD).
Технологическое окно. Каждый этап укладывался в окно с 20:00 пятницы до 02:00 понедельника.
Обработка на стороне Postgres Pro. Чтобы минимизировать влияние на PROD Oracle, вся логика обработки diff'а должна была выполняться на стороне Postgres Pro.
Изначально планировали два этапа, но жизнь внесла коррективы. Условием успешной миграции считалось снятие полного бэкапа и создание реплики. Так, два этапа превратились в три:
1-й и 2-й этапы — перенос самых больших таблиц, их индексов и констрейнтов (суммарно 30 ТБ);
3-й этап — всё остальное (10 ТБ).
Бэкап и создание реплики решили не откладывать на самый конец, а сделали между вторым и третьим этапами. Бэкап занял почти сутки, подтвердив правильность этого решения. В дальнейшем для синхронизации бэкапа и реплики использовали только WAL-логи.
Эволюция обработчика Diff
Ключевым элементом миграции стал механизм накатки изменений (diff), который претерпел четыре итерации.
Общая схема:
Oracle. На исходных таблицах (Source) создавался триггер на операции INSERT, UPDATE, DELETE.
Change Tracking Table (Oracle). Триггер записывал изменения в специальную таблицу отслеживания, состоящую из трёх полей: ID (PK самой трекинговой таблицы), KEY (PK исходной таблицы), EVENT (тип операции: I, U, D).
Postgres Pro. Исходная таблица Oracle и трекинговая таблица были доступны в Postgres Pro как Foreign Tables (через FDW).
Обработчик (Postgres Pro). Самописная процедура на стороне Postgres Pro, обёрнутая в джоб с использованием pg_scheduler.
Модель 1: прямолинейный подход
Обработчик считывал цепочку изменений из трекинговой таблицы, получал по этим ключам актуальные данные из Source-таблицы Oracle и записывал их в Target-таблицу Postgres Pro. После этого обработанные записи удалялись из трекинговой таблицы.
Проблема: почти 70% транзакций завершались ошибкой ORA-08177: can’t serialize access for this transaction при попытке удалить записи из трекинговой таблицы Oracle со стороны Postgres. На тот момент решение не нашли.
Модель 2: отказ от очистки и «потерянные» данные
Решили не удалять записи из трекинговой таблицы в рамках основной транзакции. Вместо этого ввели таблицу-плейсхолдер (sync_left_position) на стороне Postgres Pro, куда записывался ID последней обработанной записи из трекинговой таблицы.
Проблема: количественные расхождения данных. Причины:
очень широкий кеш последовательности (sequence cache) на стороне Oracle для ID трекинговой таблицы (до 12 000);
специфика приложения — оно могло «захватить» ID из последовательности, но зафиксировать транзакцию позже.
В результате обработчик считывал диапазон ID (например, с 1 по 1010), а позже в трекинговой таблице появлялись записи с ID 1008, 1009 с операциями DELETE/UPDATE, которые уже «проскочили» мимо обработчика.
Модель 3: логирование и асинхронная очистка
Усложнили схему:
все обработанные записи (ID из трекинговой таблицы и PK исходной таблицы) логировались в отдельную таблицу на стороне Postgres Pro;
второй обработчик (Feedback) работал автономно и асинхронно. Он читал лог, брал порцию ключей и удалял соответствующие записи из трекинговой таблицы Oracle и из самого лога.
Проблема: скорости всё ещё не хватало для таблиц с интенсивным изменением (до 100 миллионов изменений в сутки). «Даже если мы в пятницу останавливали триггер, у нас генерился такой дифф, который мы не успевали разгрести за два выходных», — вспоминает Ирина.
Модель 4: вводим параллелизм
Единственным выходом стало распараллеливание.
Каждый процесс обработки получил свой плейсхолдер для отслеживания позиции. В процедуру добавили параметры: P_Divider (число параллельных процессов) и P_Piece (обрабатываемый «кусок» данных, определяемый по остатку от деления ключа).
Автоматизировали создание джобов в pg_scheduler для запуска параллельных обработчиков.
Масштабирование удалось. Подготовили скрипты для горизонтального масштабирования.
Проблема: обработчики стали оказывать негативное влияние на PROD Oracle. Пользователи начали жаловаться.
Для решения проблем ограничили работы по накатке diff'а окном низкой нагрузки (с 16:00 до 02:00 ночи). Для самых «горячих» таблиц параллельность довели до 64 потоков.
Что можно было сделать иначе?
Уже находясь на третьей модели, команда нашла решение проблемы ORA-08177 из первой модели. Ошибка лечилась простой настройкой Foreign Data Wrapper: ALTER SERVER oracle_fdw OPTIONS (SET isolation_level 'read_committed'); (по умолчанию serializable). Это позволило бы остаться на первой, более простой модели.
Также была заготовка для пакетной обработки (batching) — DELETE ... WHERE key = ANY($1) и INSERT ... SELECT ... WHERE key = ANY($1). Это могло бы ускорить процесс, но имело ограничение со стороны Oracle: конструкция IN поддерживала не более 1000 записей, тогда как в PROD обрабатывались пачки по 100 000. Однако от распараллеливания уйти бы всё равно не удалось.
Сюрпризы от NUMA
Миграция завершилась успешно, но «приключения» только начинались. Стабилизация системы заняла около недели.
1. NUMA-архитектура и производительность
В понедельник утром после миграции пришлось срочно менять сервер под мастер-базу с 16-сокетного на 8-сокетный (оба с NUMA-архитектурой). PostgreSQL, как оказалось, не очень хорошо работает с NUMA, и на 16-сокетной машине это усугублялось, приводя к «совершенно драматической производительности». Переезд на 8-сокетный сервер резко улучшил ситуацию, так как взаимодействие между NUMA-нодами происходило в рамках одной материнской платы, а не через отдельную шину, как на 16-сокетном.
Проблемы с NUMA проявлялись и на этапе миграции (низкая скорость переливки). Тогда подключилась команда Postgres Professional и по рекомендации для стабилизации работы остались на двух NUMA-нодах, что помогло. Перед выходом в PROD задействовали все NUMA-ноды. Нагрузочное тестирование не выявило всех проблем, которые «выстрелили» в реальной эксплуатации.
Отставание реплики также было связано с NUMA на 16-сокетном сервере реплики. Ликвидировали отставание, поменяв сервер реплики на 8-сокетный.
2. Foreign Keys (FK)
Запросы с проверкой FK стали одними из самых тяжёлых. Проверка FK (SELECT 1 FROM X WHERE id = Y FOR NO KEY UPDATE) блокировала строки и при большом количестве параллельных транзакций генерировала мультитранзакции, включающие все активные ID транзакций, что создавало огромный оверхед.
Опять же по рекомендации Postgres Professional, на особо популярных таблицах от FK пришлось отказаться. Позже выяснилось, что это поведение является багом, который был отдан в доработку.
3. HOT Update и счётчики
Проблема возникла с таблицей, используемой как счётчик для генерации номеров платёжных поручений. При интенсивном обновлении одной и той же строки (до 300–500 раз в секунду) узким местом стал HOT Update. Внутренняя очистка генерировала очень длинные цепочки «мёртвых» строк.
С помощью команды Postgres Professional решили создать индекс на одно из изменяемых полей. Это помогло: мы отключили внутристраничную очистку, так как одним из условий её работы является как раз отсутствие индексов на обновляемых полях.
4. Внутренний мониторинг
На период стабилизации отключили мониторинг pgpro_stats_statements в пользу ванильного pg_stats_statements из-за очень больших накладных расходов.
Выводы и уроки
Несмотря на сложности перехода, миграция завершилась успешно. После двух недель стабилизации производительность Postgres Pro не уступала Oracle по значимым метрикам, а заказчик остался доволен результатами.





Ретроспектива заставила команду по-другому взглянуть на многие моменты:
простой настройкой FDW (isolation_level='read_committed') можно было избежать сложных перепроектирований системы;
важно заранее освобождать проект от архивных и исторических данных;
нагрузочное тестирование должно быть максимально полным и учитывать работу под интенсивными нагрузками;
время на миграцию всегда желательно закладывать с запасом, так как даже тщательно подготовленный проект неожиданно может столкнуться с неявными проблемами.
Созданный инструмент миграции и подходы накатки diff оказались универсальными и могут переиспользоваться для других проектов аналогичного масштаба и сложности.
Комментарии (5)
adrozhzhov
02.07.2025 13:55Нума - штука серьёзная, как влияет на работу под нагрузкой лучше тестировать до переезда.
У нас numa=off сеть ограничивает, если надо больше 100Гб.
zVlad909
02.07.2025 13:55Есть такой замечательный продукт от ИБМ называемый IBM InfoSphere Data Replication.
Этот продукт делает то что описано в статье без шороха и пыли. Без каких либо проблем обозначенных в статье и с временем переключеня на новую БД измеряемым минутами.
Устроен продукт IIDR следущим образом. Для каждой известной БД в нем есть CDC (Change Data Capture) Replication Engine или в просторечии DB agent. DB agent читает журнал изменений исходной (source) DB и преобразует запси изменения данных журнала в SQL стейтменты изменения данных для принимающей (target) DB и посылает эти стейтменты агенту принимающей БД. Агент принимающей БД применяет (apply) стейтменты в примимающей БД.
Таким образом происходит непрерывная синхронизация данных в двух связанных БД. Т.е. выполняется репликация БД.
Миграция данных из продакшн системы выполняется в три простых шага:
Создается конфигурация связи таблиц исхлодной и принимающей БД. IIDR предлагает массу решений связанных с различиями разнородных БД (типы данных, и т.д.), в том числе различиях структур.
Выполняется начальная закрузка данных из исходной БД, синхронизация изменений данных произошедщих в течении времени начальной загрузки, и поддержка сихронизации данных в реальном времени. Все это происходит с IIDR в одном действии ("start mirroring") время выполнения которого зависит исключительно от размера данных, а размер данных может быть каким угодно.
-
В день переключения (cutover day) доступ к исходной БД останавливается. Ожидается завершение синхронизации данных. Как правило это будут секунды, пусть минуты. Открывается достпуп приложения и пользователей к новой БД.
В IIDR есть агенты БД и для Оракл и для Postgre SQL (тотал 37 различных БД).
Я лично участвовал в миграции данных большой БД (полторы тысячи таблиц) из DB2 for z/OS в Oracle, и сейчас сопровождаю репликацию из Oracle в MS SQL.
apcs660
02.07.2025 13:55так IBM же ушел, как они применят? Это же не домашний проект, надо официально проводить
hogstaberg
02.07.2025 13:55Ретроспектива заставила команду по-другому взглянуть на многие моменты:
Вода мокрая
Иногда нужно читать что же за ошибки вам выдаёт перед тем, как проектировать новый велосипед
apcs660
Снимаю шляпу.
Если абстрагироваться, можно было разделить данные по разным базам?
Если база будет расти дальше, то с каким темпом? Упрется в какой то потолок в обозримом будущем?
В одном проекте (репозиторий на основе базы) с ростом хранилища примерно 6 млн документов в день на 25 лет (архив на 50 млрд документов, в основном сканы и пдф), было решено каждый год создавать новое хранилище для активных изменений, старое становилось архивом, только для чтения, а текущее хранилище имело приоритет (перекрывало данные из архива). Программным путем все разруливалось уровнем выше. Примерно как слои в докере, но на уровне репозитория документов (файлнет). Сколько занимала база для каждого года, не могу точно сказать, но документ с метаданными и содержимым был около 100кб ( 2 млрд * 0.1 MB равно 18 терабайт получается, в год, база могла пухнуть и уходить в архив?).