SaaS и мультитенантная архитектура в Symfony приложении. Часть 2

Ilya Lavrentev
6 min readDec 27, 2017

--

В прошлой статье мы рассмотрели историю развития модели SaaS и на примере интернет-магазина разобрали преимущества и недостатки данной модели. Но что, если мы ведь хотим разрабатывать SaaS сервисы, а не использовать их? Настало время заглянуть «под капот» и посмотреть как это работает.

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

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

Общая база данных, общая схема

Данные всех организаций хранятся в одной базе данных, в общих таблицах. Для разделения данных используется дополнительный столбец, например tenant_id.

При таком подходе следует внимательно отнестись к системе разделения данных разных организаций. Ответственность за это лежит на разработчиках, и, если вдруг они что-то упустили, — данные организации могут стать доступными для всех пользователей системы.

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

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

Разные базы данных, одинаковые схемы

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

Данные клиентов разделены на уровне базы данных, поэтому беспокоиться об изолированности и придумывать логику разделения данных нам не требуется.

У нас появляются новые возможности к масштабированию. Мы можем распределять базы данных клиентов на разные сервера, тем самым распределяя нагрузку. И если появится клиент, чья религия не позволяет хранить данные у третьих лиц — вы можете предложить разместить базу данных на его собственном сервере.

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

Проблемы с восстановлением данных клиента тоже не будет — мы просто восстанавливаем базу данных из дампа и радуемся жизни.

Однако, если мы планируем использовать облачный сервис для хранения данных клиентов, то создание базы данных для каждого клиента может обойтись дороже, так как цена может зависеть от количества используемых баз данных.

Одна база данных, разные схемы

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

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

Есть мнение, что с восстановлением данных при использовании такого подхода могут быть проблемы, т.к. при восстановлении из дампа будет восстановлена вся база целиком, а значит и данные для всех клиентов. Однако при использовании PostgreSQL и команды pg_restore вы можете указать схему с помощью опции schema, и данные будут восстановлены только для указанной схемы.

Такой подход позволяет экономить денежные средства, так как все данные будут находиться в одной базе данных.

Гибридный подход

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

Реализация на Symfony

Архитектуру мы рассмотрели. Осталось разобраться, как это можно реализовать с помощью Symfony.

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

Давайте разберём это на конкретном примере. Сделаем SaaS-сервис для регистрации достижений супергероев. Каждый супергерой будет отдельным клиентом, у которого будут свои достижения. В общей базе данных мы будем хранить список зарегистрированных героев и настройки подключения к их базам данных.

Структура проекта будет выглядеть следующим образом:

Всю логику по переключению источника данных мы поместим в SaasBundle, и в AppBundle будем строить логику таким образом, будто бы мы делаем сервис для одного клиента и ничего не знаем о мультитенантности. В качестве базы данных будем использовать PostgreSQL.

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

К сожалению, стандартный драйвер Doctrine для PostgreSQL не даёт возможности указать схему, поэтому первым делом мы должны будем расширить его и дополнить метод connect.

Для того, чтобы PostgreSQL начал работать с определённой схемой, нужно выполнить команду SET SCHEMA ‘значение’. По умолчанию будет использоваться public схема.

Чтобы иметь возможность переключать источник данных «на лету», нам нужно расширить класс Connection из Doctrine и определить в нём метод switchConnection, который и будет заниматься переключением.

Теперь нам нужно определить настройки для наших подключений, для этого внесём некоторые изменения в файл app/config.yml

Мы определяем два соединения:

  • shared соединение отвечает за подключение к общей базе данных
  • tenant используется для соединения с источником данных клиента

Для tenant соединения мы указали класс драйвера, который мы переопределили, а также wrapper_class для соединения.

Для каждого определённого нами соединения будут созданы сервисы, которые имеют следующую структуру имени:

doctrine.dbal.{название_соединения}_connection

Нам также нужно описать два менеджера сущностей.

Сделано это для того, чтобы иметь разные маппинги для описания таблиц.

Определять клиента мы будем по субдомену. Когда клиент переходит по адресу spiderman.hero.local, мы должны отображать данные клиента spiderman.

Чтобы взять идентификатор клиента из адреса, мы создаем обработчик события kernel.request.

Мы берём идентификатор клиента из httpHost, получаем модель клиента и, если клиент найден, переключаем источник данных. В данном примере клиент содержит информацию об адресе сервера, имени базы данных и схеме. Но мы так же можем хранить у него такую информацию как имя пользователя и пароль для подключения к базе данных.

И это ещё не всё. В 4xxi, в проектах на Symfony мы часто используем консольные команды, и на данный момент наши команды ничего не знают о наших клиентах. К счастью, в Symfony есть событие console.command, которое позволяет выполнить некоторую логику до выполнения команды, а также добавить её дополнительные параметры.
Создадим обработчик для данного события, в который будем добавлять дополнительный параметр tenant для указания идентификатора клиента. Если такой параметр будет указан — в качестве менеджера сущностей будет использован tenant, иначе shared.

Вот теперь всё. Создан SaaS-сервис с мультитенантной архитектурой, которая использует гибридный подход для хранения данных клиентов. Мы можем хранить данные клиентов либо в отдельных базах, либо использовать несколько схем одной базы данных.

Полный код проекта вы можете посмотреть в репозитории.

Всем спасибо за внимание и удачной разработки!

--

--