Введение

Greengage Database — наш форк Greenplum Database. Основная идея — оставить исходный код открытым и продолжить разработку и совершенствование базы данных. Мы собираемся перенести Greengage Database на более новую версию Postgres, предоставив более богатый набор функций всем пользователям сообщества и нашим клиентам.

Но эта задача сложнее, чем может показаться. Ранее для реализации функций массивно-параллельных вычислений Greenplum Database основная функциональность Postgres была существенно переработана. Таким образом, обновление версии Postgres создавало огромные сложности. Например, переход с Postgres 9 на Postgres 12 потребовал огромных усилий — между мажорными релизами было почти 5 лет разработки.

Чтобы сделать процесс перехода на новую версию Postgres более плавным и безболезненным, мы взяли на себя обязательство уменьшить внутреннюю связанность ядра Postgres и функций MPP в Greengage Database.

Сам Postgres позволяет расширять свою функциональность за счёт мощного и продуманного механизма расширений, которые способны изменять стандартное поведение Postgres с помощью набора хуков. Таким образом, если бы большинство функций MPP можно было организовать как внешние расширения для чистого ядра Postgres, это значительно упростило бы обновление ядра.

Имея это в виду, мы начали масштабный рефакторинг ядра Greengage Database. Мы собираемся разделить ядро ​​Postgres и специфичные функции Greengage Database, используя стандартные существующие средства Postgres для расширений.

Перенос функций MPP в расширения
Перенос функций MPP в расширения

Цель Arenadata — отделить ядро Greengage от PostgreSQL, что позволит более плавно обновлять версии PostgreSQL. Greengage Database сможет использовать самые новые функции и улучшения безопасности PostgreSQL, сохраняя при этом возможности MPP.

Перенос Orca

В качестве первого шага к разделению функциональности мы начали с Orca.

Orca — мощный планировщик запросов в Greengage Database, разработанный для значительного повышения производительности и эффективности запросов. Он достигает этого за счёт подхода оптимизации сверху вниз, оптимизации на основе стоимости операций в запросе, усовершенствованных методов оптимизации соединений (join) и расширяемости.

Используя Orca, Greengage Database обеспечивает исключительную производительность и масштабируемость для сложных аналитических задач. Orca — это ключевой компонент, лежащий в основе производительности Greengage, ответственный за оптимизацию планов выполнения запросов.

До рефакторинга Orca был глубоко встроен в код ядра Greengage Database.

Первоначально Orca разрабатывался как автономный модуль, который подключался к Greenplum Database. Но в какой-то момент он был встроен в ядро Greenplum Database. Исходный код Orca находился в дереве исходного кода базы данных Greenplum Database как часть её бэкенда. Со временем связь между Orca и остальной частью кода Greenplum Database становилась всё теснее. Прежде чем мы смогли переместить Orca в расширение, нам пришлось реорганизовать все места, где ядро ​​напрямую взаимодействовало с Orca.

После тщательного анализа мы разработали следующий план действий:

  1. Создать отдельное расширение для Orca, которое будет построено в виде разделяемой библиотеки (shared library), и подключить Orca через хук планировщика, реализованный в разделяемой библиотеке. На этом этапе код Orca всё ещё собирается и связывается с основным исполняемым файлом Postgres, но планирование выполняется через хук.

  2. Выполнить рефакторинг кода Greengage, чтобы удалить другие прямые связи с Orca:

    • компонент memory protection;

    • компонент explain;

    • функции gp_optimizer;

    • GUC optimizer;

    • расширение pg_hint_plan.

  3. Переместить все исходные файлы Orca в расширение.

  4. Переместить GUC, связанные с Orca, в расширение.

  5. Устранить все оставшиеся проблемы.

  6. Профит!

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

Шаг 1. Создание нового расширения, подключение Orca через хук планировщика и переработка процедуры инициализации

Все расширения, специфичные для Greengage, находятся в папке gpcontrib в корне проекта. Мы добавили туда новое расширение orca. Расширение собирается только в том случае, если сборка настроена без ключа --disable-orca. Расширение создаёт разделяемую библиотеку. При установке расширения изменяется файл postgresql.conf.sample, в который добавляется настройка:

shared_preload_libraries = 'orca'

На этом этапе расширение содержит функции инициализации и деинициализации разделяемой библиотеки:

/* Hooks for plugins to get control in planner() */
planner_hook_type planner_hook = NULL;
void
_PG_init(void)
{
    if (!process_shared_preload_libraries_in_progress)
        ereport(ERROR,
                (errcode(ERRCODE_INTERNAL_ERROR),
                    errmsg("This module can only be loaded via shared_preload_libraries")));

    if (!(IS_QUERY_DISPATCHER() && (GP_ROLE_DISPATCH == Gp_role)))
        return;
    prev_planner = planner_hook;
    planner_hook = orca_planner;
}

void
_PG_fini(void)
{
    planner_hook = prev_planner;
}

Функция инициализации регистрирует функцию PlannedStmt * orca_planner(Query *parse, int cursorOptions, ParamListInfo boundParams) в planner_hook, которая вызывается из функции Postgres planner(). orca_planner() — точка вызова планировщика Orca. В случае если Orca может создать план, он используется для выполнения запроса. В случае если Orca не удалось создать корректный план, orca_planner() вызывает standard_planner(). Это делается только для экземпляра координатора, поскольку этап планирования выполняется на координаторе. (Примечание: строго говоря, сегменты также могут выполнять планирование локальных запросов, отправляемых координатором. Но Orca никогда не использовался для планирования таких вспомогательных запросов, для этого применялся только планировщик Postgres. Так что с точки зрения сегмента ничего не изменилось.)

Здесь возникла первая проблема. Чтобы работать с Orca, нам нужно сначала сделать некоторую инициализацию, вызвав функцию InitGPOPT(). Раньше это делалось в функции InitPostgres(). Эта функция вызывается для каждого бэкенд-процесса при его запуске. Но теперь мы можем полагаться только на функцию _PG_init() внутри расширения, которая вызывается во время инициализации Postmaster. InitGPOPT() выделяет память для внутренних структур, а защита памяти (memory protection) включается только в InitPostgres(). Таким образом, мы не могли вызвать InitGPOPT() до включения защиты памяти и не могли поместить её в _PG_init(), что можно было бы считать наиболее очевидным подходом.

Ещё одна интересная вещь связана с деинициализацией. Функция деинициализации TerminateGPOPT() в Orca вызывалась из ShutdownPostgres(), которая, в свою очередь, зарегистрирована как функция обратного вызова для завершения процесса. Мы могли бы создать собственную функцию обратного вызова и зарегистрировать её с помощью before_shmem_exit() в _PG_init()…​ Но, как уже упоминалось, _PG_init() вызывает Postmaster. И когда Postmaster создаёт новый экземпляр бэкенда, все его функции обратного вызова удаляются. Таким образом, никакие функции обратного вызова, зарегистрированные в _PG_init(), не вызываются при выходе из бэкенд-процессов.

Чтобы обойти эти ограничения, мы перешли к отложенной инициализации Orca из хука планировщика. Это допустимое место, поскольку планировщик может быть вызван только уже инициализированным бэкенд-процессом координатора, когда защита памяти корректно настроена и включена. И после завершения инициализации так же регистрируется функция обратного вызова для деинициализации.

Итак, общая логика orca_planner() в упрощённом виде описана на схеме ниже.

Логика orca_planner
Логика orca_planner

После выполнения вышеописанных шагов мы получили основу, которая позволила нам избавиться от других связей между кодом ядра Orca и Greengage Database.

Шаг 2. Рефакторинг кода

Шаг 2.1. Перенос Orca из компонента memory protection

Использование Orca подразумевает увеличенное использование памяти бэкенд-процессом Greengage. Функция GPMemoryProtect_TrackStartupMemory() содержала зависимость времени компиляции от Orca, которая была необходима для вычисления объёма выделенной памяти для каждого процесса. Поскольку это не позволяло переместить весь код Orca в разделяемую библиотеку и сделать его работу прозрачной для ядра GPDB, нам пришлось переработать эту часть. Кроме того, старый подход не учитывал другие возможные расширения, которые могли влиять на использование памяти.

Чтобы преодолеть указанные выше проблемы, мы ввели новую функцию GPMemoryProtect_RequestAddinStartupMemory(Size size). Теперь расширение должно вызывать GPMemoryProtect_RequestAddinStartupMemory() в своей функции _PG_init(), если расширение влияет на память, выделенную для запуска каждого процесса. Такой вызов был добавлен в функцию Orca _PG_init(). А код, связанный с Orca, удалён из GPMemoryProtect_TrackStartupMemory(). Результат, накопленный всеми вызовами GPMemoryProtect_RequestAddinStartupMemory(), теперь учитывается в GPMemoryProtect_TrackStartupMemory().

Шаг 2.2. Перенос Orca из компонента explain

Компонент explain имел две зависимости времени компиляции от Orca:

DXL — это язык на основе XML, который используется для кодирования всей необходимой информации, передаваемой в Orca или получаемой из него (например, запрос на план, готовые планы или метаданные, запрашиваемые Orca). Команда Greengage Database explain может выводить план выполнения запроса в формате DXL. Но эта функциональность специфична только для Orca, поэтому расширение Orca — подходящее место для её инкапсуляции.

Postgres предоставляет хук ExplainOneQuery_hook. Мы использовали этот хук для подключения к компоненту explain и весь специфичный для Orca код, связанный с explain в формате DXL, переместили в расширение. Если требуется DXL-вывод, функция orca_explain() вызывает Orca для создания плана и выводит план в формате DXL. Если Orca не удалось создать план для запроса, отображается соответствующее уведомление. Если DXL-вывод не требуется, мы просто вызываем ранее зарегистрированный ExplainOneQuery_hook или, в случае его отсутствия, стандартную процедуру explain.

Вывод плана запроса в формате DXL
Вывод плана запроса в формате DXL

Что касается имени планировщика, то в ExplainPrintPlan() были жёстко закодированы только два варианта: Postgres-based planner и GPORCA, что совершенно не поддаётся расширению. Решение о том, какое имя выводить, принималось на основе поля planGen структуры PlannedStmt. Тип поля был enum:

typedef enum PlanGenerator
{
    PLANGEN_PLANNER,		/* plan produced by the planner*/
    PLANGEN_OPTIMIZER,		/* plan produced by the optimizer*/
} PlanGenerator;

Значение PLANGEN_PLANNER должно было соответствовать стандартному планировщику Postgres, а значение PLANGEN_OPTIMIZER — Orca. Это не оставляло места для подключения какого-либо другого планировщика. Таким образом, это поле было удалено, и было добавлено новое — plannerName с типом const char *. Теперь любой внешний планировщик может указать своё имя во время планирования, и оно будет правильно выведено командой explain.

Шаг 2.3. Перенос Orca из функций gp_optimizer

Есть несколько специфичных для Orca функций, которые можно вызывать из кода SQL:

Функции стали частью системного каталога и используются в инструментах инфраструктуры Greengage Database, поэтому их нельзя легко вынести из ядра. В случае если Greengage Database собирается без Orca, эти функции всё равно должны существовать, но при вызове показывать соответствующее сообщение.

Для решения этой проблемы реализация базовых функций DisableXform(), EnableXform(), LibraryVersion() была перемещена в разделяемую библиотеку Orca. Реализация функций-обёрток enable_xform(), disable_xform(), gp_opt_version() была обновлена: теперь они пытаются загрузить базовые функции из разделяемой библиотеки. Попытка загрузить символ из разделяемой библиотеки обёрнута в блок PG_TRY & PG_CATCH, поскольку библиотека может отсутствовать. Если библиотека отсутствует, мы перехватываем ошибку и показываем соответствующее сообщение.

Шаг 2.4. Перенос Orca из параметра GUC optimizer

У базы данных Greengage есть много конфигурационных параметров (Global User Configuration, GUC), связанных с Orca. Но особое внимание следует уделить параметру optimizer. Его семантика была «включить или отключить Orca». Кроме того, мы не могли оставить его нетронутым, поскольку он также имел зависимость времени компиляции от Orca.

Поскольку мы переносили Orca из ядра, логичной мыслью было бы вынести этот параметр GUC наружу вместе с Orca. Но исторически слишком многое зависит от этого параметра GUC. Перенос его наружу мог бы привести к увеличению дельты и вызвать новые проблемы. Поэтому на текущем этапе мы оставили его в ядре, но с обновлённой семантикой: теперь он определяет, включён общий внешний планировщик или нет. Теперь внешний планировщик отвечает за проверку значения этого параметра GUC и, если он отключён, просто передаёт управление стандартному планировщику.

Также значение по умолчанию для этого параметра GUC было установлено в off. Теперь внешний планировщик должен установить его в on в своей функции _PG_init() следующим образом:

SetConfigOption("optimizer", "on", PGC_POSTMASTER, PGC_S_DYNAMIC_DEFAULT);

Шаг 2.5. Перенос Orca из расширения pg_hint_plan

Расширение pg_hint_plan также содержало зависимость времени компиляции от Orca, так как хук plan_hint_hook был определён внутри Orca. Оставлять одно расширение зависимым от другого нехорошо, поэтому нам пришлось переработать и его.

Мы переместили определение plan_hint_hook из ORCA в src/backend/optimizer/plan/planner.c. Хотя этот хук в настоящее время используется только в расширении Orca, он позволяет использовать его с любым планировщиком, которому необходимо получить список указаний из расширения (например, pg_hint_plan).

Кроме того, мы сделали этот хук типизированным и возвращающим указатель на HintState, как он и делает на самом деле. Нет причин оставлять его с типом generic и возвращающим void *.

Шаг 3. Перемещение исходных файлов

После того как все вышеперечисленные шаги были выполнены, мы смогли переместить большую часть исходных файлов Orca в расширение gpcontrib/orca, включая:

  • src/backend/gporca/* (здесь располагалось ядро ​​Orca);

  • src/backend/gpopt/* (здесь располагался связующий код для соединения Orca с Greengage);

  • src/backend/optimizer/plan/orca.c;

  • src/include/optimizer/orca.h;

  • src/include/gpopt/*.

Помимо перемещения исходных файлов, мы обновили инструменты линтера и форматирования кода (src/tools/fmt и src/tools/tidy) и переместили их в gpcontrib/orca/tools, поскольку они используются специально для Orca.

Шаг 4. Перемещение оставшихся GUC, связанных с Orca

Помимо параметра GUC optimizer, уже упомянутого выше, есть длинный список GUC, используемых внутри Orca. Поэтому, как только все исходники Orca были перемещены в расширение, эти GUC также были перемещены туда.

Известные ограничения

Как видно из разделов выше, хук планировщика, определённый в расширении Orca, передаёт управление ранее зарегистрированному хуку только в том случае, если Orca не удалось создать план. Это происходит потому, что другой хук может вызвать стандартный планировщик, чего мы не хотим. Если Orca создал план, это наша конечная остановка, нам не нужно больше планировать.

Но другие расширения могут захотеть зарегистрировать хук планировщика не с целью создания нового плана, а, например, для мониторинга планирования или для помощи планировщику. Один из таких примеров — расширение pg_hint_plan.

То же относится и к хуку explain. Если запрошен формат вывода DXL, все ранее зарегистрированные хуки не получат управления.

Единственное жизнеспособное решение на данный момент — сделать так, чтобы все такие хуки вызывались до вызова Orca. В результате Orca (или любой другой подобный внешний планировщик) должен быть первой загружаемой разделяемой библиотекой. Поэтому он должен быть в shared_preload_libraries на первом месте.

Выводы

После выполнения всех описанных выше шагов мы создали версию Greengage, в которой Orca может быть динамически подключён и отключён путём добавления либо удаления строки orca в GUC shared_preload_libraries и перезагрузки кластера. Это изменение значительно уменьшило связанность кода и увеличило связность кода в продукте, что всегда хорошо с точки зрения качества программного обеспечения.

Основные преимущества переноса Orca:

  • более простое обновление PostgreSQL;

  • улучшенная модульность и удобство обслуживания;

  • потенциал для более гибкой и расширяемой платформы Greengage Database.

После рефакторинга можно легко добавить любой другой внешний планировщик, расширяющий стандартную функциональность Greengage Database. Таким образом, это может придать мощный импульс работе всего сообщества Greengage.

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