Прощай, Маша, не поминай лихом! Как мы переходили с MariaDB на PostgreSQL
Привет, Хабр! Меня зовут Игорь, и я один из разработчиков НОТА ЮНИОН. При подборе сотрудников (рекрутменте) есть много рутинных задач, отнимающих немало времени. Чтобы рекрутеры могли больше времени уделять, скажем так, творческой части своей работы, есть решение «Нота Юнион». Это набор инструментов для автоматизации подбора сотрудников. И в этом году мы перевели его базу данных с MariaDB на PostgreSQL. Задача оказалась масштабной, пришлось изрядно потрудиться. Хочу рассказать о том, почему мы решили поменять базу и как это реализовали. Возможно, вам это поможет сразу выбрать более подходящий под ваш продукт вариант.
Почему выбрали MariaDB
В 2019 году нам понадобилось быстро разработать HR CRM‑систему, которая покрывала бы основные требования рекрутёров нашей группы компаний. Так появился НОТА ЮНИОН (ex. Talentforce). Сердцем проекта служил малоизвестный в СНГ CRM‑движок, крутился он на банальном LAMP (Linux, Apache, MySQL, PHP). Несмотря на такой стек, у движка были очень мощные инструменты, от добавления новых полей до изменения внешнего вида форм, настраиваемых «процессов» (так называется сущность, в которой можно настроить условия выполнения определённых действий — триггеры) и формирования всевозможных отчётов. Причём всё это обилие работало без вмешательства команды разработки, что называется, прямо из коробки.
Однако требования бизнеса не всегда можно удовлетворить простым «лоу‑кодом». Стек проекта обладал абсолютно всеми инструментами для построения «динамической системы», которая была бы максимально изменяемой под хотелки каждого заказчика. Но набор поддерживаемых БД был скромен: либо MySQL (MariaDB), либо MSSQL. Сначала этого хватало. Внутрикорпоративное использование не предполагало хранения терабайтов данных, высокой частоты обращений к БД и формирования огромных отчётов, которые загружались бы больше тридцати секунд. К тому же, раз оно уже поддерживается, фактически, нативно, то и «поднять» систему доработками бизнеса стало намного проще.
Предпосылки перехода на новую БД PostgreSQL
Однажды «маленькая» система исключительно для внутреннего использования решила попытать своё счастье на рынке. За время развития проект внешне и внутренне претерпел колоссальные изменения: стек увеличивался, большой монолит стали разбирать на микросервисы. С продуктом росла и команда.
С появлением заказчиков увеличивалось количество данных, причём самого разного формата. Как и везде, рекрутёрам приходится регулярно выгружать отчёты об эффективности своей работы, поэтому запросы к базе становились всё «тяжелее», а их количество росло.
В конце концов данных стало так много, что MariaDB перестала справляться даже с формированием индексов, часто падала при операциях вставки (INSERT, UPDATE) и банальном поиске по первичным ключам. Выгрузка отчётов длилась десятки секунд, и, поначалу, нам приходилось делать механизмы, которые регулярно собирали данные в фоновом режиме, чтобы людям не приходилось подолгу наблюдать крутящийся индикатор. Никто не любит ждать, и порой одна лишняя секунда может спровоцировать человека составить гневное письмо команде техподдержки. Поэтому решили переводить проект на другую СУБД, которая удовлетворяла бы таким критериям:
Работа с повышенной нагрузкой. Многочисленные пользователи создают большое количество запросов. Хоть мы и кешировали на чтение всё, что могли, но были и такие процессы, которым требовались «свежие» данные. А ведь информацию нужно было ещё и записывать, и обновлять: перевод кандидата по этапам воронки подбора запускал последовательность самых разных действий — отправку писем, смену статусов, пересчёт прогрессов, поиск конфликтов между вакансиями и так далее. А поскольку НОТА ЮНИОН позволяла вручную сформировать любые конвейеры, то и действий можно было придумать бесконечное количество.
Масштабирование. По мере работы база кандидатов пополняется всё новыми и новыми записями, некоторые люди откликаются по несколько раз, имеют множество резюме и контактных данных, встречается и дублирование информации. Поэтому со временем неизбежно возникает потребность в масштабировании. И нужно было сделать так, чтобы оно было как можно более безболезненным и быстрым.
Отказоустойчивость. Постепенно существующая база разрослась так, что стала неспособна формировать индексы и решала прилечь ненадолго отдохнуть.
Широкий выбор типов хранимых данных. Так как проект интегрирован со множеством внешних систем — телефонией, карьерными сайтами, системами управления предприятиями, SAP, 1С, сервисами служб безопасности, — то возникает вопрос хранения данных и их поиска на основании структур, предоставляемых внешними системами.
Open‑source. Важно даже не то, что, чем больше людей посмотрело в код, то тем больше потенциальных багов будет исправлено, а то, что ПО бесплатное. Сегодня есть трудности с оплатой большинства проприетарного ПО. А в случае с open‑source заказчики оберегают себя от получения очередной статьи расходов.
Проанализировав все эти требования, мы выбрали очевидную замену для базы данных — PostgreSQL.
План перехода
Как сказал Мольтке‑старший, «Ни один военный план не выдерживает первого же столкновения с противником». Но это не означает, что планировать не нужно. Мы составили для себя такой порядок действий:
Поиск инструмента для автоматической миграции схемы и данных из одной СУБД в другую.
Определение специфичных для MySQL возможностей, которых нет в целевой СУБД.
Написание драйвера и подключение его к ORM.
Доработка кодовой базы под специфику целевой СУБД.
Автотестирование, модульное тестирование, ручное smoke‑тестирование сценариев, которые не получается покрыть тестами (ввиду изменчивости итогового поведения различных сценариев в зависимости от настройки приложения).
Исправление миграций.
Исправление дефектов, возникших при миграции из одной БД в другую.
Регрессионное тестирование.
Обновление эксплуатационных сред:
создание резервных копий MariaDB;
запуск экземпляра новой PostgreSQL;
остановка основного сервиса;
миграция данных в автоматическом режиме;
обновление и запуск основного сервиса;
запуск миграций;
оперативный smoke-тест.
У нас не высоконагруженная система, в которой требования к uptime 99,999%, поэтому по договорённости с заказчиками мы выделяли себе немного времени для вдумчивой миграции и тестирования перед передачей приложения обратно пользователям в промышленную эксплуатацию. Развёрнутые у заказчиков системы обновляли поэтапно, предварительно делая снимки (снапшоты) систем на случай, если что‑то пойдёт не так.
Проблемы, с которыми мы столкнулись при переходе
Драйвер
Начали, пожалуй, с очевидного: где связующее звено бизнес‑логики с СУБД? В большинстве веб‑фреймворков есть встроенная ORM‑библиотека, которая позволяет разработчикам не углубляться в тонкости построения запросов и не искать, где же в этот раз ты забыл поставить запятую и почему этого столбца не существует. Однако, если бы всё было так просто в крупных проектах… В нашем случае логика работает с «драйвером» — неким классом, который преобразует команды от ORM‑модели к запросу. На момент миграции официального драйвера попросту не существовало. Поэтому пришлось писать его самостоятельно, с оглядкой на драйвер MySQL/MariaDB.
Схема взаимодействия класса драйвера с моделями
ORM и SQL
Наша система хоть и имела ORM‑модель, но её возможности были ограничены: ORM умела только в CRUD‑операции, строить «плоские» запросы получения связанных данных по внешним ключам, делать простую сортировку и фильтрацию. Если дело доходило до сложных аналитических запросов к базе, то их приходилось писать вручную разработчикам вместе с аналитиками. С одной стороны, такой подход не слишком гибкий, потому что разработчик должен обладать хорошими знаниями по составлению SQL‑запросов и эффективности их выполнения. А с другой стороны, требования у всех разные, и даже самая мощная ORM со временем превратилась бы в обузу с присущими проблемами, вроде проблемы N+1 и непонимания, что же она там за запрос сформировала под капотом и как нам сделать его эффективнее.
Изначально система была разделена на модули (сервисы) с индивидуальными зонами ответственности. Например, модуль «Вакансии» отвечает за список вакансий, их единичное представление и т. д. На каждый модуль выделили по QA‑инженеру, который перебирал бы тестовые сценарии, читал журналы ошибок и ставил разработчику задачу с набором проблемных запросов, которые или не обрабатывали совсем, или обрабатывали, но не так, как нужно.
Функции и хранимые процедуры
Да, мы их используем. Но не массово, иначе понадобилось бы переписывать огромную кучу старого SQL‑кода. Наши функции использовались для простых операций типа «Посчитай количество рабочих дней с учётом исключений, выходных и праздников, хранящиеся в такой‑то таблице». Это лучше с точки зрения производительности, чем делать такие вычисления на стороне кода.
С хранимыми процедурами всё понятно: увидел, переписал, проверил, занёс в миграцию.
Обычные SQL‑запросы также изобиловали различными встроенными функциями, тут тоже ничего сложного: ищи аналог, заменяй, проверяй. Если не получалось найти достойный аналог, то приходилось дописывать бизнес‑логику под новые условия.
Отсутствие достойного аналога
Отчёты
В НОТА ЮНИОН есть конструктор, позволяющий строить обычные табличные отчёты с не очень мудрёными графиками. Большинству заказчиков такой подход удобен. Они могут сформировать отчёт либо самостоятельно, либо с помощью наших консультантов, в любом случае это не занимает много времени, так как тому, кто собирает отчёт, не приходится вникать в особенности построения SQL‑запросов. SQL‑запросы для таких отчётов собираются полностью автоматически и выполняются в СУБД. Конструктор использовал множество агрегатных функций и особенностей MySQL, поэтому пришлось и его дорабатывать для совместимости с PostgreSQL.
Есть ещё и расширенные отчёты. SQL‑запросы для них собирали аналитики, и затем разработчики «реализовывали» эти запросы, раскидывали информацию нужным чартам, настраивали фильтрацию по данным. Такие отчёты требуются крупным заказчикам, для которых недостаточно конструктора. В рамках миграции на PostgreSQL наши действия были просты: вижу запрос — переписываю, чтобы был совместим, а далее тестировщики проверяют на соответствие требованиям отчётов.
Однако такой подход в последнее время кажется нам устаревшим и трудозатратным. Поэтому сейчас мы проверяем возможность перенести отчёты в BI‑сервис, который устраивал бы нас достаточно и покрывал бы желания бизнеса. Но это уже совсем другая история и тянет на отдельную статью…
Плагины
Система позволяет добавлять функциональность с помощью плагинов. Например, не всем заказчикам нужна интеграция с порталами обучения, поэтому не имеет смысла интегрировать в основной сервис лишний код, который никогда не будет использоваться и потащит за собой библиотеки. А если настанет момент, когда заказчику понадобится такая функциональность, можно будет подключить этот плагин, прописать пару конфигураций, и готово. Однако неудобство в том, что плагины хранятся в обособленных репозиториях, каждый из них необходимо было подключить, протестировать и, при необходимости, доработать под целевую СУБД.
Автоматическое восстановление
Вопрос в зал. Как добавить в реляционную СУБД базу новый столбец? Большинство ответило бы так:
Сначала добавим атрибут в ORM‑модель.
Создадим (силами ORM) или вручную напишем миграцию.
Запустим миграцию.
Это классический путь, а у самураев он особенный. Наш основной сервис имеет такой механизм как «самовосстановление», этакий Kubernetes для базы данных, который периодически проверяет схему БД на соответствие внутренней ORM‑модели: просматривает типы столбцов, значения по умолчанию и другие свойства (например, может ли значение быть NULL). При обнаружении отступлений от ORM генерируется «исправляющий» запрос к БД, который приведёт схему в соответствие с моделью. Вот тут‑то и вылезли проблемы: после тестовых запусков сервис считал, что вообще всё не так, как должно быть. И действительно, типы данных в разных СУБД различались. Пришлось дорабатывать механизм с учётом типов данных, которые ранее не существовали, а также исправлять логику формирования таблиц, индексов и столбцов.
Блок‑схема восстановления модели, хранящей в себе объект вакансии
Несоответствие типов
Абсолютно нормальная ситуация, когда в разных СУБД есть одинаковые и разные типы данных. Проблему решали так же, как и с функциями: искали аналогичные типы и заменяли, если требовалось — модифицировали запросы в бизнес‑логике, дорабатывали механизм восстановления.
Схожие типы данных в разных СУБД
Инструменты миграции
Для процесса миграции выбрали инструмент pgloader. Нас привлекла его гибкость в приведении одних типов к другим. Например, по умолчанию pgloader воспринимал tinyint(1)
как bool. Но нам необходимо было оставить целочисленное значение, поэтому написали конфигурацию ко всем типам, которые нас не устраивали. Например:
Пример конфигурационного файла pgloader`а
Выводы и результаты
После смены СУБД мы сразу же увидели результат на графиках мониторинга. Сервис базы стал потреблять почти на 30% меньше процессорного времени и на 20% меньше оперативной памяти. Увеличилась скорость выполнения ресурсоёмких запросов, да и в целом система стала более отзывчивой.
Конечно же, стоит сказать и о том, что смена СУБД положительно повлияла на привлекательность стека продукта среди заказчиков, так как промышленные системы редко используют MySQL (MariaDB). Ну и, конечно же, репутация у PostgreSQL намного лучше.
Однако работа на этом не завершается. Мы продолжаем оптимизировать существующие запросы и аналитику, улучшать нашу ORM‑модель, чтобы меньше зависеть от написания запросов вручную.
На момент написания статьи на PostgreSQL мигрировали 80% наших заказчиков. Статистика показала нормальный уровень обращений в техподдержку. Количество багов не выросло, уменьшились жалобы на производительность системы. Можно сказать, пользователи ничего не заметили. Также мы уже начали фактическую миграцию некоторых заказчиков на S3, чтобы ещё сильнее увеличить производительность и отказоустойчивость системы.