Summary: Сотрудники Лиги Цифровой Экономики Юлия Крашеница, DBA PostgreSQL, Андрей Ходус, ведущий эксперт, и Елена Жикривецкая, старший эксперт PostgreSQL, рассказали об особенностях миграции с системы управления базами данных Oracle на PostgreSQL и возникших сложностях, а еще поделились своим опытом их решения.

Миграция различных схем — как это было

Так как в миграции участвовало множество людей (команда ДБА, аналитики, разработчики, тестировщики, команда нагрузки) — не можем отвечать за всех. Расскажем, как процесс проходил со стороны ДБА, а также раскроем интересные задачи, с которыми столкнулись другие команды.

При выборе инструментов миграции мы быстро остановились на уже описанной на Хабре утилите ora2pg, поскольку к ней составлена подробная документация, в нее интегрировано гигантское количество параметров и «ручек», которые можно крутить. 

Установку и настройку подробно описывать не будем — это уже сделано в других статьях. Однако у нас накопился большой опыт миграции высоконагруженных (и не очень), тяжелых и копеечных схем.  Хотим поделиться нюансами этих процессов.

Для переезда мы выбрали следующее архитектурное решение — известную связку PostgreSQL + ETCD + Patroni + HAProxy + Keepalived. Такой кластер мы уже эксплуатировали. Он чувствителен к дисковым массивам и сетевым задержкам, поэтому могут возникать небольшие проблемы с переездами мастера. Тем не менее, эта архитектура много раз помогала нам в сложных ситуациях с масштабированием — вертикальным и горизонтальным, — балансировкой нагрузки и обслуживанием баз. На обычных репликах делать такое было бы сложнее.

Чтобы при миграции максимально защитить ПРОД-данные и не создавать дополнительной нагрузки, миграцию большинства сервисов мы проводили через «промежуточный Oracle»: поднимали отдельный Instance Oracle, в который грузили дамп с прода, и мигрировали уже с него.

Когда мы определились с технологиями, решили начать миграцию с одного из самых сложных и тяжелых сервисов, размер БД которого превышал 500 Гб. Технически мигрировать через ora2pg оказалось не сложно. Мы столкнулись только с несколькими типичными проблемами (о них расскажем чуть ниже). Сложнее было построить процесс, чтобы перевести пользователей на новую СУБД с минимальным прерыванием, учитывая, что процесс миграции такого объема явно длился более 10 часов.

Мы договорились использовать слегка нестандартный подход и сделать на системе-источнике два канала: под Oracle и под PostgreSQL. В результате система могла присылать новые данные в обе СУБД одновременно. За время миграции система отдавала данные в Oracle и в то же время копила транзакции на системе-источнике для PostgreSQL, которую мы обрабатывали сразу после основного этапа миграции. 

В какой-то момент мы получили две синхронные СУБД, и в течение пары недель смотрели на расхождения. Так разработка могла отловить баги или некорректную миграцию данных. Когда мы убедились, что базы для приложения работают одинаково, переключили пользователей. Здесь все прошло нормально. 

Вторым этапом переключали выгрузки, и когда мы включили их на PostgreSQL, то поняли, что онлайн-запросы стали тормозить. Тут-то нам и пригодился HAProxy с его возможностью балансировки. Выгрузки в основном читают данные, поэтому мы просто направили приложения на отдельный порт HAProxy, который ссылался на реплики. Теперь выгрузки могли спокойно лопатить огромный объем данных, а онлайн-пользователи жили на отдельной ноде, которая отдавала данные преимущественно из ОЗУ — быстро.

Подход, описанный выше, не универсален. Например, в одном из сервисов у нас не было технической возможности пустить систему-источник сразу в два канала (на Oracle и PostgreSQL одновременно). А сервис был крайне критичен, чтобы мигрировать его с прерыванием. В этом случае мы использовали «подход с домиграцией»: сначала мигрировали константные данные (ту их часть, которая со стопроцентной вероятностью не будет меняться за период миграции; в нашем случае — архивные данные, которые предписано хранить не менее полугода привет, 152 ФЗ) и в конце домигрировали оставшуюся часть.

Теперь перейдем к более технической стороне.

Проблемы, с которыми мы сталкивались при работе с ora2pg

Итак, первым делом надо определить порядок миграции. В конечном итоге выбор подхода у нас сводился к следующей матрице:

 

Сервис низкой критичности

Сервис высокой критичности

Легковесная БД

Одномоментная миграция напрямую

Одномоментная миграция через промежуточный инстанс

Тяжелая БД

Миграция с последующей обработкой накопившихся на системе-источнике транзакций

Двухуровневая миграция через промежуточный инстанс

Объясним.

Одномоментная миграция напрямую

Суть в том, чтобы мигрировать напрямую из БД Oracle, останавливая приложение на время миграции. Это самый простой случай, порядок будет следующим:

  • остановить приложение,

  • провести миграцию данных через ora2pg заранее подготовленными скриптами,

  • сравнить две БД,

  • переключить пользователей на PostgreSQL,

  • провести обслуживание.

Одномоментная миграция через промежуточный инстанс

Мигрировать через тот самый «промежуточный Oracle». Здесь уже чуть запарнее:

  • остановить приложение, снять дамп с Oracle,

  • раскатать дамп на промежуточном Oracle,

  • поднять приложение для записи в два потока (как писали выше),

  • провести миграцию данных с промежуточного инстанса в PostgreSQL,

  • сравнить PostgreSQL с промежуточным инстансом,

  • обработать транзакции, накопленные на системе-источнике,

  • сравнить две БД,

  • переключить пользователей,

  • провести обслуживание.

Миграция с последующей обработкой накопившихся в системе-источнике транзакций

Делать все так же, как при одномоментной миграции напрямую, а потом обрабатывать на стороне приложения накопившиеся транзакции (это уже не задача DBA).

Двухуровневая миграция через промежуточный инстанс

Мигрировать в два этапа:

  • Вычленить константную часть данных, провести ее миграцию через промежуточный инстанс. Чтобы разделить части данных между собой, нужно заглянуть «под капот» и найти в логике БД, каким образом (например, по датам) можно это организовать. Соответствующие селекты вносятся в поле REPLACE_QUERY конфига ora2pg.

  • Остановить приложение.

  • Домигрировать оставшуюся часть.

  • Сравнить БД.

  • Переключить пользователей.

Выбор варианта миграции: что еще на него влияет?

Есть еще несколько факторов, которые следует учитывать в том числе при выборе варианта миграции:

  • Типы данных в БД. Ни для кого не секрет, что типы данных в Oracle и PostgreSQL слегка отличаются. Некоторые мапятся тривиально — добавлением незамысловатой строчки в конфиг ora2pg:

    DATA_TYPE CLOB:text, BLOB:bytea, RAW:uuid, NUMBER:bigint

Или когда нужно переписать из char или number в boolean:

REPLACE_AS_BOOLEAN PROANALYTICS_REGION_MAPPING:FILTER_ENABLED COMPETITOR_URL:FILTER_ENABLED COMPETITOR_FILTER:ENABLED

Но есть и те, которые требуют чуть более скрупулезного подхода. Пришлось повозиться с BLOB, миграцию которого планировали в oid. Последний в PostgreSQL хранится в системной таблице, так что просто переложить его напрямую не получится. План миграции расширился на следующие несколько пунктов:

  • Переименовать исходный столбец типа oid в PostgreSQL как <name>_temp.

  • Создать такой же как исходный столбец, но с типом данных bytea.

  • Провести миграцию данных без дополнительных манипуляций в скриптах.

  • Перелить данные из дополнительного столбца с помощью функции lo_from_bytea(0, "column_name").

  • Удалить дополнительный столбец.

  • Переименовать исходный.

  • Связи между таблицами. Если в схеме имеются связи по ключу, есть два пути:

  1. Простой — отключить триггеры для каждой таблицы перед миграцией:

Alter table schema_name.table_name disable trigger all;

И включить после миграции:

Alter table schema_name.table_name enable trigger all;

  1. Сложный — мигрировать ступенчато, разбив все таблицы на пачки и проводя перенос сначала родительских, а потом зависимых от них дочерних таблиц. В каждый конфиг прописывать по одной пачке, запускать по очереди: каждый следующий — не раньше, чем закончится предыдущий.

  • Последовательности. При создании структуры БД сразу создаются сиквенсы. Соответственно, у них есть настройки по умолчанию вроде START 1. О заливке данных сиквенсам не известно, его текущее значение не изменяется. Поэтому после загрузки надо подвинуть сиквенс на максимальное значение соответствующего столбца. Следующим запросом для БД Oracle можно сформировать соответствующие запросы в PostgreSQL, чтобы поправить все последовательности в БД:

SELECT 'SELECT setval(''' || lower(sequence_owner || '.' || sequence_name) || ''', ' || last_number || '+' || increment_by || ');' AS SQL

FROM all_sequences s

WHERE lower(SEQUENCE_OWNER) = lower('schema_name’);

  • Скорость. Чтобы максимально эффективно использовать ресурсы сервера, с которого проводятся миграции, придется подбирать параметры, а конкретно:

TYPE COPY — по умолчанию был задан INSERT, что, как показала практика, сильно медленнее, даже с разбивкой на потоки по уникальному ключу.

LONGREADLEN — необходимо выставить равным предполагаемому размеру самого тяжелого BLOB в Oracle.

DATA_LIMIT / BLOB_LIMIT — количество пачек данных (блобов), подгружаемых в память. Чем больше, тем быстрее, но в сочетании с параметром выше не должен перекрывать имеющиеся ресурсы.

JOBS — количество параллельных процессов для отправки данных в Oracle.

ORACLE_COPIES — эта директива конфигурации добавляет поддержку нескольких процессов для извлечения данных из Oracle. Имеет смысл только при включенном DEFINED_PK.

DEFINED_PK — нужно указывать первичный ключ, по которому можно разбить данные для распараллеливания потоков в пункте выше.

PARALLEL_TABLES — количество таблиц, миграция которых проводится параллельно. Имеет смысл, если их больше одной.

Приведем пример, иллюстрирующий важность грамотно подобранных параметров конфигурации.

Скорость миграции самой крупной БД со стандартными параметрами была 24 часа, за счет подбора параметров (TYPE, DATA_LIMIT) удалось снизить время до 10 часов, чего нам было достаточно с точки зрения бизнеса. 

Еще одна крупная БД содержала в себе две таблицы с 256 млн и 35 млн строк типа CLOB, со стандартным конфигом скорость превышала сутки. Однако с помощью корректировки параметров удалось сократить скорость до двух часов. Это зависит от БД источника, БД приемника и сервера, с которого запускается ora2pg. Их нужно выставлять индивидуально для каждой БД по результатам тестирований.

Вопросы оптимизации PostgreSQL

Помимо сложностей с миграцией, были и проблемы с оптимизацией PostgreSQL.

Для оценки состояния БД мы используем данные мониторинга. Особое внимание обращаем на пики по загрузке CPU, что может говорить о том, что есть неоптимальные запросы. Смотрим также планы «тяжелых» и долгих запросов, изучаем блокировки.

Работа с Oracle немного «разбаловала» разработчиков: даже не совсем оптимальные запросы выполнялись довольно быстро, плюс Oracle традиционно запускают на очень хороших и дорогих железных серверах. Там, где данные были не в оперативке, быстро читались с диска. 

Postgres, как и положено в современном мире, размещали уже на виртуальных машинах и managed service, что, конечно, не дает такого запаса по железу. Поэтому нам пришлось долго и кропотливо работать над оптимизацией запросов.Один из сервисов давался особенно сложно: там были простые селекты из таблицы на 30 млн строк, однако таких запросов выполнялось около 6к tps, и требования к их скорости выполнения были «чтобы не хуже, чем на Oracle», а это 60 мс на 99-й перцентиль.

Но не меньшее внимание уделили эффекту «раздувания» таблиц (table bloating) — чем больше в них «пустого» пространства, тем больше нагрузка на дисковую подсистему и CPU. 

Крайне важно сразу после миграции провести обслуживание БД и определиться, какими инструментами и с какой периодичностью проводить его в дальнейшем. Прерывания в работе БД были недопустимы, поэтому мы использовали инструменты для неблокирующего обслуживания.

Для больших таблиц и баз прибегали к pgcompacttable: он не сильно нагружает сервер и почти никак не влияет на производительность СУБД. Для небольших таблиц использовали pg_repack, который значительно нагружает сервер в момент обслуживания, но проводит его быстро и выполняет кластеризацию (с упорядочиванием по индексу) — это значительно улучшает производительность выполнения запросов.

Для БД на PostgreSQL мы позаботились об очистке старых данных и заранее подготовили хранимые plpgsql-процедуры, которые удаляют (DELETE) накапливающиеся со временем неактуальные строки. Процедуры запускаются по расписанию в pg_cron и выполняют DELETE пачками, чтобы избежать длительных транзакций.

PostgreSQL по дефолту настроен на работу со слабым железом, поэтому мы устанавливали параметры, которые после корректировали по результатам нагрузочного тестирования. В качестве примера приведем настройки СУБД PostgreSQL для одного из сервисов.

  • Потребление ресурсов:

shared_buffers: 2GB

work_mem: 16MB

maintenance_work_mem: 2024MB[5] 

  • Планирование запросов:

effective_cache_size: 64GB

random_page_cost: 0.1

seq_page_cost: 0.1

  • Журнал предзаписи:

wal_level: hot_standby

wal_log_hints: ‘on’

checkpoint_completion_target: 0.8

 checkpoint_timeout: 30min

max_wal_size: 2GB

  • Архивация:

archive_mode: ‘on’

archive_timeout: 1800s

  • Репликация:

max_wal_senders: 30

wal_keep_segments: 4

vacuum_defer_cleanup_age: 700000

  • Ведомые серверы:

hot_standby: ‘on’

max_standby_archive_delay: 1800s

max_standby_streaming_delay: 1800s

  • Автоматическая очистка:

autovacuum_max_workers: 8

autovacuum_vacuum_scale_factor: 0.01

autovacuum_analyze_scale_factor: 0.005

Балансировка

Как уже писали выше, в архитектуру решения сразу была заложена возможность балансировки, это был один из козырей в рукаве. Воспользоваться им, однако, пришлось буквально сразу после миграции первого тяжелого сервиса: PostgreSQL просто не держал все разнообразие запросов, свалившееся на одну ноду, да еще в условиях, когда все данные близко не помещались в ОЗУ. 

Благо настроить балансировку было легко: достаточно добавить новую точку в конфигурационный файл HAProxy и сделать reload сервиса. Балансировка работает примерно так: у Patroni есть REST интерфейс (стандартно на порту 7000), по нему можно получить много информации (например, localhost:7000/master выдает 200, если хост — мастер, и код ошибки, если таковым не является). То же самое для точек /replica. Для сервисов, высоконагруженных на чтение данных, очень полезной оказалась точка /health, которая балансирует запросы на все живые ноды сразу.

Соответственно, на каждом новом кластере PostgreSQL + patroni мы устанавливали сразу три точки. Мастер использовали по умолчанию, а точку с репликой можно было отдать аналитикам для мониторинга или отправить туда выгрузки.

Ошибка canceling statement due to conflict with recovery

С другой проблемой столкнулись, когда «пустили» выгрузки балансироваться на реплики. При выполнении долгой выгрузки на мастере стро́ки успевали меняться, из-за чего PostgreSQL выдавал ERROR: canceling statement due to conflict with recovery. 

Решить эту проблему можно, исправив конфигурацию кластера patroni через patronictl edit-config:

max_standby_archive_delay: 1800s (как долго должен ждать ведомый сервер, прежде чем отменять запросы, конфликтующие с очередными изменениями в WAL).

max_standby_streaming_delay: 1800s (то же самое, только не для архивных файлов, а для streaming).

Однако максимальную защиту от такой ошибки дал следующий параметр:

vacuum_defer_cleanup_age: 50000 (задает число транзакций, на которое будет отложена очистка старых версий строк при VACUUM и изменениях HOT).

***

Мы рассказали о проблемах миграции разных схем (в том числе — высоконагруженных) и возможными решениями возникших трудностей. Надеемся, этот опыт оказался для вас полезным. Сталкивались ли вы с подобными сложностями, как их решали? Если ваша экспертиза чем-то отличается — пишите в комментариях.

Комментарии (2)


  1. nmorozova
    27.01.2023 12:22

    А перенос процедур? Или логика не на стороне pl/sql, а в БД только хранение? Или это за рамками статьи?)


    1. Digital_League Автор
      27.01.2023 13:34

      У нас была только одна процедура очистки архивных данных, которую мы легко переписали. Других процедур в БД не было. На других проектах тоже занимались переносом процедур, может быть, опишем в следующих статьях:)