Миграция БД 40 ТБ с Oracle на PostgreSQL для страховой компании

Клиент и масштаб задачи

«СтрахГарант» — федеральная страховая компания с 42 филиалами, 3.2 миллионами активных полисов и 15 000 обращений в день. Ядро IT-инфраструктуры — Oracle Database Enterprise Edition с базой данных объёмом 52 ТБ.

Предпосылки миграции:

  • Стоимость лицензии Oracle — 48 млн рублей в год (и растёт)
  • Требования регулятора — переход на отечественное или открытое ПО к 2027 году
  • Зависимость от вендора — Oracle-специалисты на рынке дороги и дефицитны

Клиент обратился к нам в октябре 2025 года. После аудита мы определили параметры проекта:

ПараметрЗначение
Исходный объём Oracle52 ТБ
Объём после очистки (логи, архивы)40 ТБ
Количество таблиц1 847
Количество хранимых процедур (PL/SQL)620
Пиковая нагрузка1 998 TPS
Допустимый даунтайм8 часов (выходные)

Целевая СУБД — Postgres Pro Enterprise (российская сборка PostgreSQL с расширенной поддержкой). Наша команда: 2 DBA, 1 backend-разработчик, 1 DevOps-инженер. Срок проекта — 5 месяцев.

Стратегия миграции: три этапа вместо одного

Перенести 40 ТБ за одно окно миграции в 8 часов невозможно — полный перенос при тестировании занимал 5 дней. Мы разработали трёхэтапную стратегию:

  • Этап 1: Перенос крупных исторических таблиц (30 ТБ) — полисы за 2010-2024, архивные обращения, финансовая история. Эти данные не меняются.
  • Этап 2: Перенос оставшихся таблиц (10 ТБ) — активные полисы, текущие обращения, справочники.
  • Этап 3: Финальная синхронизация изменений + переключение приложений.

Между этапами Oracle продолжал работать в production. Все изменения, произошедшие после начала переноса, фиксировались и синхронизировались с PostgreSQL через механизм дифференциальной синхронизации (подробнее ниже).

Для переноса структуры и данных мы использовали ora2pg — стандартный инструмент миграции Oracle → PostgreSQL:

# ora2pg.conf — базовая конфигурация
ORACLE_DSN    dbi:Oracle:host=oracle-prod;sid=INSURE;port=1521
ORACLE_USER   migration_user
ORACLE_PWD    ${ORA_PASS}

SCHEMA        INSURANCE
TYPE          TABLE,COPY,SEQUENCE,VIEW,TRIGGER,FUNCTION,PROCEDURE,PACKAGE

# Параллелизм для ускорения
JOBS          8
ORACLE_COPIES 4

# Конвертация типов
DATA_TYPE     DATE:timestamp,NUMBER:numeric,VARCHAR2:varchar,CLOB:text,BLOB:bytea

# Исключаем архивные логи
EXCLUDE       LOG_%,AUDIT_%,TMP_%

PG_DSN        dbi:Pg:host=pg-prod;port=5432;dbname=insurance
PG_USER       migration_user

ora2pg сконвертировал 1 847 таблиц, 283 представления и 620 хранимых процедур. PL/SQL → PL/pgSQL конвертация потребовала ручной доработки в 34% случаев (217 процедур).

Дифференциальная синхронизация: 4 итерации до успеха

Самая сложная часть проекта — синхронизация изменений между Oracle (работающий production) и PostgreSQL (заполняющаяся новая база). За время переноса 40 ТБ в Oracle происходит до 100 миллионов изменений в сутки.

Архитектура: триггеры на Oracle фиксируют INSERT/UPDATE/DELETE в «Change Tracking Table», Foreign Data Wrapper (FDW) на стороне PostgreSQL читает эти изменения и применяет их.

Модель 1 — прямое чтение (отклонена):

-- PostgreSQL: создание foreign table для доступа к Oracle
CREATE EXTENSION oracle_fdw;

CREATE SERVER oracle_srv
  FOREIGN DATA WRAPPER oracle_fdw
  OPTIONS (dbserver '//oracle-prod:1521/INSURE');

CREATE FOREIGN TABLE change_tracking (
    id          BIGINT,
    table_name  VARCHAR(128),
    row_key     VARCHAR(256),
    event_type  CHAR(1),  -- I/U/D
    created_at  TIMESTAMP
) SERVER oracle_srv
  OPTIONS (schema 'INSURANCE', table 'CHANGE_TRACKING');

Проблема: при удалении обработанных записей из change_tracking возникала ошибка ORA-08177 (snapshot too old) в 70% случаев из-за serializable isolation level по умолчанию.

Модель 2 — позиционное чтение (проблемная): мы отказались от удаления записей и ввели позиционный маркер sync_left_position. Но Oracle Sequence с кешем 12 000 создавал «дыры» в нумерации — записи пропускались.

Модель 3 — логирование обработанных (медленная): обработанные ID записывались в отдельную таблицу, фоновый процесс Feedback очищал change_tracking. Не успевали при 100 млн изменений/сутки.

Модель 4 — параллельная обработка (финальная)

Итоговое решение — распараллеливание через параметры P_Divider и P_Piece:

-- Функция обработки изменений с параллелизмом
CREATE OR REPLACE FUNCTION sync_changes(
    p_table_name VARCHAR,
    p_divider    INT DEFAULT 16,
    p_piece      INT DEFAULT 0,
    p_batch_size INT DEFAULT 5000
) RETURNS BIGINT AS $$
DECLARE
    v_processed BIGINT := 0;
    v_row       RECORD;
BEGIN
    FOR v_row IN
        SELECT ct.id, ct.row_key, ct.event_type
        FROM change_tracking_fdw ct
        WHERE ct.table_name = p_table_name
          AND MOD(ct.id, p_divider) = p_piece
        ORDER BY ct.id
        LIMIT p_batch_size
    LOOP
        CASE v_row.event_type
            WHEN 'I' THEN
                PERFORM upsert_from_oracle(p_table_name, v_row.row_key);
            WHEN 'U' THEN
                PERFORM upsert_from_oracle(p_table_name, v_row.row_key);
            WHEN 'D' THEN
                PERFORM delete_local(p_table_name, v_row.row_key);
        END CASE;
        v_processed := v_processed + 1;
    END LOOP;
    RETURN v_processed;
END;
$$ LANGUAGE plpgsql;

Для самых «горячих» таблиц (активные полисы, текущие обращения) мы довели параллельность до 64 потоков. Обработка велась в окно 16:00-02:00, чтобы не нагружать Oracle в рабочее время.

Планировщик pgpro_scheduler запускал обработчики по расписанию:

-- Регистрация задач в pgpro_scheduler
SELECT pgpro_scheduler.create_job(
    'sync_policies',
    'SELECT sync_changes(''policies'', 64, generate_series(0, 63), 5000)',
    '0 16 * * *'  -- каждый день в 16:00
);

Критическое открытие: isolation level FDW

Через 3 недели мучений с первой моделью синхронизации мы обнаружили, что одна настройка FDW решала проблему с ORA-08177 изначально:

-- Изменение isolation level для FDW соединений
ALTER SERVER oracle_srv
  OPTIONS (SET isolation_level 'read_committed');

По умолчанию oracle_fdw использует SERIALIZABLE isolation, что вызывает конфликты при конкурентном доступе к change_tracking таблице. Переключение на READ COMMITTED устранило 70% ошибок первой модели.

Этот урок стоил нам 2 недели времени. Документация oracle_fdw упоминает эту опцию одной строкой в разделе Advanced Options. Мы создали внутренний чеклист для будущих проектов миграции, чтобы подобные ситуации не повторялись.

Если бы мы знали об этой настройке с самого начала, первая модель синхронизации работала бы корректно, и мы сэкономили бы 2-3 недели проекта. Подробнее об оптимизации FDW — на itfresh.ru.

Проблемы после миграции: NUMA и производительность

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

Проблема NUMA: исходный сервер имел 16-сокетную архитектуру (2 CPU × 8 NUMA-нод). PostgreSQL на таком оборудовании показывал «совершенно драматическую» деградацию — запросы, которые в Oracle выполнялись за 5 мс, в PostgreSQL занимали 40-80 мс.

Причина: PostgreSQL shared buffers размещались на NUMA-ноде 0, а рабочие процессы запускались на нодах 4-15. Каждое обращение к shared memory шло через межпроцессорную шину — +300% латентности.

# Диагностика NUMA
numactl --hardware
# available: 16 nodes (0-15)
# node 0 cpus: 0-7
# node 0 size: 32768 MB
# ...

# Проверка распределения памяти PostgreSQL
numastat -p $(pgrep -f 'postgres.*writer')
# Node 0: 28 GB  (shared_buffers)
# Node 1-15: 0.1 GB each  (рабочая память)

Решение: мы перенесли PostgreSQL на 8-сокетный сервер (1 CPU × 4 NUMA-ноды) и настроили привязку процессов:

# Привязка PostgreSQL к NUMA-нодам 0-1
numactl --cpunodebind=0,1 --membind=0,1 \
    pg_ctl start -D /var/lib/postgresql/data

# postgresql.conf — оптимизация для NUMA
shared_buffers = '64GB'         # На одной NUMA-ноде
huge_pages = on                 # Уменьшение TLB miss
effective_cache_size = '128GB'
work_mem = '256MB'
maintenance_work_mem = '2GB'

После миграции на 8-сокетный сервер латентность вернулась в норму.

Foreign Keys и HOT Update: тонкости PostgreSQL

Два дополнительных сюрприса ждали нас после стабилизации основной производительности.

Проблема 1: Foreign Keys блокируют строки

Проверка FK в PostgreSQL выполняет SELECT 1 FROM parent_table WHERE id = ? FOR NO KEY UPDATE. Это блокирует строку в родительской таблице на время транзакции. Для таблицы-справочника регионов (42 строки, обращения из каждой транзакции) это создавало contention:

-- Мониторинг блокировок
SELECT relation::regclass, mode, count(*)
FROM pg_locks
WHERE relation = 'regions'::regclass
GROUP BY relation, mode;
-- RowShareLock: 847 (одновременно!)

Решение: мы убрали FK на «горячих» справочниках и заменили их проверкой на уровне приложения. Позже эта проблема была классифицирована как известный баг в Postgres Pro и исправлена в патче.

Проблема 2: HOT Update на счётчиках

Таблица-счётчик обращений обновлялась 300-500 раз в секунду (одна строка на филиал). PostgreSQL использует HOT (Heap Only Tuple) update для ускорения — обновлённая строка размещается на той же странице без обновления индексов.

Но HOT update на одной строке создавал длинные цепочки версий, замедляя чтение:

-- Диагностика HOT chains
SELECT schemaname, relname, n_tup_hot_upd, n_live_tup
FROM pg_stat_user_tables
WHERE relname = 'branch_counters';
-- n_tup_hot_upd: 45,000,000  n_live_tup: 42

Решение: создание индекса на изменяемые поля отключает HOT update, но PostgreSQL начинает обновлять индекс при каждом UPDATE. Для счётчика мы изменили архитектуру: вместо обновления одной строки — INSERT в лог-таблицу с периодической агрегацией:

-- Вместо UPDATE branch_counters SET count = count + 1
INSERT INTO counter_log (branch_id, delta, ts)
VALUES (42, 1, now());

-- Агрегация раз в минуту
INSERT INTO branch_counters_agg (branch_id, total_count)
SELECT branch_id, SUM(delta)
FROM counter_log
WHERE ts > now() - interval '1 minute'
GROUP BY branch_id
ON CONFLICT (branch_id)
DO UPDATE SET total_count = branch_counters_agg.total_count + EXCLUDED.total_count;

Мониторинг производительности и стабилизация

Для мониторинга PostgreSQL мы развернули pg_stat_statements (вместо pgpro_stats_statements, который создавал избыточную нагрузку на нашем объёме данных):

# postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.max = 10000
pg_stat_statements.track = all

-- Топ-10 медленных запросов
SELECT
    substring(query, 1, 80) AS query_short,
    calls,
    round(total_exec_time::numeric, 2) AS total_ms,
    round(mean_exec_time::numeric, 2) AS avg_ms,
    round((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 2) AS pct
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;

В первую неделю после переключения мы выявили и оптимизировали 23 запроса, которые в PostgreSQL работали медленнее, чем в Oracle. Основные причины:

ПроблемаКоличество запросовРешение
Отсутствующие индексы8Создание B-tree/GIN индексов
Неоптимальный план (CTE fence)6Переписали CTE как подзапросы
Implicit type cast5Явное приведение типов
Различия в оптимизаторе4Хинты через pg_hint_plan

К третьей неделе производительность стабилизировалась и начала расти за счёт «прогрева» кешей и тюнинга.

Результаты проекта

Миграция «СтрахГарант» заняла 4.5 месяца — на 2 недели меньше запланированных 5 месяцев. Финальное переключение прошло в выходные за 6 часов (из допустимых 8).

Динамика производительности после миграции:

ПериодTPSP99 латентностьПримечание
Последний день Oracle1 99845 мсBaseline
Неделя 1 (PostgreSQL)1 42078 мсNUMA + FK проблемы
Неделя 2 (PostgreSQL)2 62832 мсПосле оптимизации NUMA
Неделя 3 (PostgreSQL)3 76018 мсСтабильная работа

PostgreSQL показал рост TPS на 88% относительно Oracle после стабилизации. Основная причина — более агрессивное использование параллельного выполнения запросов и эффективная работа shared buffers.

Финансовый результат за первый год:

  • Экономия на лицензиях Oracle: 48 млн ₽
  • Стоимость лицензии Postgres Pro Enterprise: 4.2 млн ₽
  • Стоимость проекта миграции: 8.5 млн ₽
  • Чистая экономия в первый год: 35.3 млн ₽

Ключевые рекомендации из проекта:

  • Планируйте FDW isolation level с первого дня — read_committed вместо default serializable
  • Проводите максимально полное нагрузочное тестирование до миграции
  • Учитывайте NUMA-топологию серверов при выделении shared_buffers
  • Закладывайте 30% запас времени на «неявные проблемы» — они будут
  • Механизм дифференциальной синхронизации переиспользуем для аналогичных проектов

Часто задаваемые вопросы

Зависит от объёма и сложности. Для базы 40 ТБ с 1800+ таблицами и 620 хранимыми процедурами проект занял 4.5 месяца. Основное время уходит не на перенос данных (5 дней), а на конвертацию PL/SQL → PL/pgSQL (34% процедур требуют ручной доработки), настройку синхронизации изменений и тюнинг производительности.
Основной инструмент — ora2pg для конвертации схемы и переноса данных. Для синхронизации изменений во время миграции — oracle_fdw (Foreign Data Wrapper). Для мониторинга — pg_stat_statements. Для планирования задач — pgpro_scheduler или pg_cron. Для хинтов оптимизатору — pg_hint_plan.
Триггеры на Oracle-таблицах записывают изменения в Change Tracking Table. PostgreSQL через Foreign Data Wrapper читает эти изменения и применяет к локальным таблицам. Критично: установите isolation_level = read_committed для FDW-сервера, используйте параллельную обработку (до 64 потоков для горячих таблиц).
Три основные причины: NUMA-архитектура сервера (shared_buffers на одной ноде, процессы на другой), Foreign Keys блокирующие строки при проверке, различия в оптимизаторе запросов. Все решаемы: привязка к NUMA-нодам, удаление FK на горячих справочниках, pg_hint_plan для управления планами.
В нашем проекте экономия на лицензиях составила 48 млн ₽/год, стоимость Postgres Pro Enterprise — 4.2 млн ₽/год. С учётом стоимости миграции (8.5 млн ₽) проект окупился за 3 месяца. Дополнительная экономия — на специалистах: PostgreSQL DBA на рынке на 30-40% дешевле Oracle DBA.

Нужна помощь с проектом?

Специалисты АйТи Фреш помогут с архитектурой, DevOps, безопасностью и разработкой — 15+ лет опыта

📞 Связаться с нами
#миграция Oracle PostgreSQL#ora2pg#перенос базы данных#PostgreSQL оптимизация#Oracle миграция#Foreign Data Wrapper#синхронизация данных#NUMA PostgreSQL
Комментарии 0

Оставить комментарий

загрузка...