О клиенте и продукте
ВипСервис — крупнейший российский поставщик тревел-услуг для корпоративных клиентов. Холдинг входит в список 200 крупнейших частных компаний России по версии Forbes. Внутри компании целая экосистема цифровых продуктов, которые решают разные задачи от предиктивной аналитики до агрегации отелей. Work Solutions с 2020 года развивает систему бронирования тревел-услуг для корпоративного бизнеса Портбилет ТМС, благодаря которой каждый год путешествуют 13 миллионов пассажиров.
Краткие сведения
Количество часов в проекте: 665ч
Стек: PHP 8.0.28, Symfony 6.0, mysql 8.0.29
Формат работы: 1 senior-backend разработчик и 1 middle-backend разработчик
Ответственные со стороны заказчика: 1 руководитель проекта, 1 руководитель цифровых проектов
Задача
Перевести глобальных клиентов на платформу Портбилет TMC. До этого момента системой пользовалась только компания Вип Корпорейт Тревел (ВКТ), которая обслуживает локальных клиентов, а за международное направление отвечает Вип Бизнес Тревел (ВБТ). В структуре холдинга это два независимых подразделения. Добавление ВБТ означало, что внутри системы нужно вести отдельный учет по всем заказам, проводить интеграции с внешними API поставщиков и не только.
Проблема
Исторически Портбилет TMC рассчитан под обслуживание главной компании ВКТ. Если посмотреть сверху, система имеет древовидную структуру, где есть базовая компания и дочерние ответвления.
Работоспособность всего приложения зависит от основной компании. Например, у главной компании есть идентификатор «1». Этот идентификатор использовался повсеместно в коде проекта как константа, в том числе в клиентских сервисах. То есть, чтобы получить данные по компании, нужно в коде вызвать единицу.
Фактически, главная компания — это поставщик для всех дочерних компаний. И если у клиентов не было своих индивидуальных настроек, то они использовали глобальные настройки ВКТ.
Текущая архитектура не предусматривает добавление новой глобальной компании, поэтому ее появление само собой предполагает глобальные изменения в приложении.
Решение
Путей решения три: копия проекта, глобальный рефакторинг и мультитенантность.
Сравним их достоинства и недостатки.
Копия проекта. Первое, что приходит на ум. Такой подход требует меньше всего усилий на старте, но недостатки обнаружатся быстро.
Во-первых, одновременно сопровождать код в двух системах сложнее, чем в одной. Особенно, когда новую функциональность нужно добавить в оба сервиса, но при этом один из них сильно ушел вперед. Чтобы доработка функционировала в отстающей системе, ее придется подтягивать до состояния первой, в том числе и решать проблемы ну уровне различий в структуре базы данных, которые неизбежно возникнут.
Во-вторых, дубли систем не подразумевают сквозного использования, например, для роли администратора, чтобы он мог бесшовно переключаться между глобальными компаниями.
Больше восьми лет развивали сервис предоставления банковской гарантии для Госгарант. Однажды один из клиентов захотел приобрести систему как on-prem решение. Из-за ограничений по срокам пришлось сделать копию. Спустя год этих дублей стало пять. Их интегрировали в банки федерального уровня. Каждый требовал персональных доработок, из-за чего проблемы накапливались снежным комом. Разумеется, здесь мы решили не идти путем дублирования, так как сегодня нужна одна копия — а завтра десять.
Глобальный рефакторинг проекта. Решение из разряда «все переписать». Полностью перебрать кодовую базу проекта, чтобы избавиться от статических элементов в коде, а также добавить новый интерфейс с материнскими компаниями, которые работают автономно. Решение кажется верным, но слишком затратным.
Мультитенантность. Решение, которое предусматривает единый интерфейс и при этом позволяет не зависеть от статической «единички». Кажется, то, что нужно, чтобы доработать функциональность на возможность добавления ещё одной сервисной компании.
Что такое мультитенантность
Мультитенантность или мультиарендность — это особенность архитектуры приложения, которая позволяет управлять несколькими организациями (тенантами) одновременно. Для каждой организации выделяется собственная база данных, и система понимает, в какой момент с какой БД ей работать.
Основное правило мультитенантности — структура всех баз данных должна быть идентична. Все таблицы и поля должны быть одинаковыми. Тогда если мы делаем доработку только для одного тенанта, миграции всегда применяются на обе базы.
Как это контролируется? У каждого тенанта есть отдельная таблица, в которой перечислен список функций. У каждой записи внутри этой таблицы есть колонка с активностью. Если в колонке стоит true, то функционал используется на этом тенанте. Если false — не используется. Для разработчиков это сильно упрощает процесс работы: сразу понятно, какие функции глобальные, а какие применяются на конкретном проекте.
Препятствия
Несмотря на удобство функционала мультитенантности, не обошлось без проблем:
1. Распределение всех пользователей по БД. Теперь, когда в системе две базы данных, нужно понимать, какие пользователи к каким тенантам имеют доступ. Также нужно где-то хранить общие настройки и атрибуты. Для этого создали системную третью БД. Она содержит в себе таблицу с глобальными идентификаторами GUID и работает как кроссервис. В ней же обозначены разрешения, какой пользователь с каким тенантом может работать.
2. Разделение задач по тенантам. На проекте используем брокер Gearman, который еще шутливо называем «Дядя Гирман». Он умеет работать с фоновыми задачами и хранит внутри состояние о текущем подключении к БД. Дядя Гирман понимает, какие задачи брать, какие ставить помеченными и т.д. Нужно было придумать, как работать с двумя тенантами, чтобы задачи не пересекались. Для решения использовали консольные утилиты — когда доступ к сайту не через HTTP, а через консоль. У каждой команды появился новый аргумент с тенантом. Он сообщает о том, на каком тенанте должен выполняться код. Причем это касается и кода, и миграций.
3. Разделение доменов. Изначально приложение работало на одном домене, но так как каждый тенант — это отдельная компания со своими данными и юридическим лицом, необходимо подключить каждый тенант на отдельный домен.
Поменять запись в бд при переключении между тенантами в рамках одного домена несложно, но когда пользователь переключается на второй домен, у него свои cookies, настройки браузера и т.д. Тут нужен функционал автоматической авторизации пользователя на втором тенанте. Для этого сделали микросервис аутентификации, который также используем в качестве централизованной БД с данными о пользователях.
Запуск проекта
Для релиза подготовили чистую базу данных с одной компанией под новый проект. Написали скрипт, который делает копию БД, убирает ненужное и создает отдельную компанию. Команда запускается по кнопке на Jenkins и добавляет копию с текущей БД уже с нужными настройками. Поэтому запуск в продакшн прошел плавно без неожиданностей.
Что это дало заказчику
Заказчик получил возможность распределения данных по нескольким компаниям. Помимо этого, теперь в проект можно добавлять новые тенанты за довольно короткое время — разработчик со знанием системы сможет «поднять» новый тенант за неделю. Например, сейчас планируется подключение третьей компании в проект, и этот процесс уже не вызывает опасений.
На первый взгляд, добавление нового клиента не кажется чем-то столь сложным, но система создавалась другой командой и такая возможность в первоначальный замысел не входила. Команда Work Solutions повела себя профессионально — на своем опыте показала ограниченность решения с дублированием кодовой базы, при этом не стала настаивать на ее полном переписывании. Мультитенантность позволила запуститься за два месяца и получить нужный запас прочности.
Заключение
Мультитенантность позволила добавить в систему новые возможности без глобальной переработки. Если бы не это решение, приложение пришлось бы полностью рефакторить.
Создание копии приложения для нас стояло на последнем месте из-за проблем с дальнейшей поддержкой подобных сервисов. Но тут стоит учитывать ещё один момент — в мультитенантности весь код находится на одной серверной инфраструктуре. То есть один код обрабатывает одновременно несколько тенантов. И если бы нам нужно было полностью изолировать систему от остальных, то пришлось бы делать копию.