All Articles ↓
1 неделю назад

Мультитенантность в CUBA с помощью Citus

Вступление

Мультитенантность - это режим работы программного обеспечения, при котором несколько независимых экземпляров одного или нескольких приложений работают в совместно используемой среде. Экземпляры (тенанты) изолированы логически, но физически - интегрированы.

В интернете можно найти вот такое понятное объяснение, в котором мультитенантность с арендой в офисном здании:

Многоквартирный дом - прекрасный образец мультитенантной архитектуры. В нем централизована система безопасности (на въезде), электричество, водоснабжение и другие удобства. Всем этим владеет арендодатель многоквартирного дома и предоставляет квартиросъемщикам (в случае приложения - тенантам).

В этой статье мы поговорим о мультитенантной архитектуре и рассмотрим одну из реализаций этого подхода в CUBA Platform.

Краткая история мультитенантности

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

Но со временем пропускная способность интернета возросла, и теперь мы можем говорить о мегабитных и гигабитных скоростях в каналах передачи данных. Поэтому намного проще стало поставлять ПО через интернет - загрузка сотни мегабайтов занимает меньше времени, чем поход в магазин за DVD или Blu-ray-диском с ПО.

В то же время, развитие каналов связи и компьютерного оборудования вывело на рынок виртуализацию, а позже и IaaS (инфраструктура как услуга). Стало проще настроить сервер с уже установленным приложением и предоставить пользователям доступ к нему. В результате производители стали работать по модели SaaS - “ПО как услуга”. Клиенту достаточно просто приобрести доступ к уже развернутому приложению. Отсутствие необходимости поддерживать локальные сервера снижало затраты.

Установка приложения для каждого клиента все еще была непростой, поэтому разработчики начали использовать мультитенантную архитектуру - чтобы упростить развертывание и поддержку. Вы все еще можете встретить в интернете приложения, развернутые по модели SaaS. Например, Salesforce уже давно начали использовать мультитенантный подход и SaaS. Но реализация мультитенантных приложений - непростая задача. Нужно позаботиться о разделении пользовательских данных, совместном использовании ресурсов, совместном использовании данных и других вещах. Новейшее решение всех этих задач выразилось в модели PaaS (платформа как сервис).

Развитие контейнеризации и процессов DevOps - главные угрозы для мультитенантных приложений. Сейчас очень легко запустить новый сервер или целую инфраструктуру, включая базы данных, службы приложений, сервера кэширования и обратный прокси-сервер благодаря таким продуктам как Docker и Kubernetes. Для разработчика намного проще реализовать сингл-тенант приложение и развертывать его экземпляры для каждого нового клиента буквально за секунды, и зачастую полностью автоматически. Иногда это может привести к феномену под названием “cube sprawl” - “рой кубернетесов”, но “волшебной палочки” в мире пока не существует.

Тем не менее, мультитенантные приложения не теряют своей актуальности (взять, например, Salesforce, Work Day, Sumo Logic, и т.д.), и, опять же, на разработку таких продуктов есть спрос. Рассмотрим некоторые подходы к реализации мультитенантности и разберемся, что для этого предлагает CUBA Platform.

Реализации мультитенантности

Есть три основных модели, применяющихся в построении мультитенантного хранилища данных:


1. Единая база данных. Данные тенантов хранятся в одной схеме и в одних и тех же таблицах. Чтобы разделить данные разных групп пользователей (тенантов), разработчик вводит колонку дискриминатора, которая хранит идентификатор тенанта. Это означает, что мы изолируем отделенные данные на уровне запроса. Приложение неявно добавляет условие фильтрации для каждого запроса, следовательно, невозможно выбрать данные другой группы.

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



2. Второй подход - использовать разные схемы баз данных для разных групп пользователей, экземпляр приложения при этом один на всех. Эта архитектура эффективна, если данные разных тенантов должны обрабатываться по-разному - например, если эти данные должны соответствовать законам разных стран. В то же время у вас остается возможность предоставить доступ к общим данным, чтобы не было повторов. Для реализации этой архитектуры нужно использовать такой же механизм перенаправления запросов в разные схемы. Например, в Spring Framework для этого есть класс RoutingDataSource.

3. Третий тип мультитенантной архитектуры хранит данные в разных БД. Он также задействует механизм маршрутизации, но перенаправляются запросы между БД, а не схемами. Эта модель относительно сложная в плане администрирования и поддержки. При обновлении приложения нужно обновлять все БД. Обновление версии базы данных тоже не самая тривиальная задача, особенно если у вас много тенантов. С другой стороны, тенанты разделены по определенному признаку, так что риск получить доступ к чужим данным и задействовать другие ресурсы приложения - минимален. Также такой подход обеспечивает хорошую масштабируемость, хранилище данных можно масштабировать индивидуально в зависимости от требований группы..

Плюсы и минусы мультитенантности

Мультитенантная архитектура обладает определенными плюсами и минусами:

Плюсы:

  1. Себестоимость. По сравнению с другими решениями затраты на нее меньше.
  2. Упрощенный режим поддержки - администраторам нужно следить, патчить, обновлять и настраивать только одну систему.
  3. Масштабируемость - добавление нового тенанта является стандартным, быстрым и простым процессом.

Минусы:

  1. Создать мультитенантное приложение сложнее, чем обычное.
  2. Еще один проблемный момент - адаптивность, особенно для приложений, пользователи которых находятся в разных странах. Могут быть разные требования к защите персональных данных, расчетам бизнес-логики и т.д..
  3. Проблема “любопытного соседа” - другая группа пользователей может получить доступ к вашим данным или использовать ваши ресурсы.
  4. Проблема с единой точкой отказа. Если есть ресурс, которым пользуются все клиенты (например, база данных), недоступность этого ресурса отразится на всех.

Мультитенантность в CUBA

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

Если вы создаете мультитенантное приложение с помощью CUBA, нужно решить, какие сущности будут хранить данные тенантов. Возьмем, например, демо-приложение “PetClinic”. Модель данных приложения представлена на диаграмме ниже.

text

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

Если вам нужно, чтобы сущность поддерживала мультитенантность в CUBA, необходимо реализовать специальный интерфейс - com.haulmont.cuba.core.entity.TenantEntity. После этого аддон начнет учитывать специальную колонку для разделения групп пользователей. Например, вот фрагмент кода сущности Pet:

public class Pet extends NamedEntity implements TenantEntity {
   
   @TenantId
   @Column(name = "TENANT_ID")
   protected String tenantId;

   @NotNull
   @Column(name = "IDENTIFICATION_NUMBER", nullable = false)
   protected String identificationNumber;

   @OneToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "TYPE_ID")
   protected PetType petType;

Обратите внимание, что все “системные” сущности в CUBA по умолчанию поддерживают мультитенантность. Так что все пользователи, роли, сохраненные фильтры, роли безопасности и т.д. у вас будут отдельными для каждого клиента.

После создания сущностей CUBA добавит дополнительное условие “where tenant_id =” для всех JPQL-запросов (включая соединения), генерируемых для мультитенантных сущностей. Для разработчиков такой подход достаточно понятен, если они не планируют использовать нативный SQL. В таком случае необходимо эксплицитно добавить дополнительное условие в запросы для предотвращения утечки данных.

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

SELECT *
FROM PETCLINIC_PET t1
        LEFT OUTER JOIN PETCLINIC_OWNER t0
                        ON ((t0.ID = t1.OWNER_ID)
                       AND (t0.TENANT_ID = t1.TENANT_ID))
        LEFT OUTER JOIN PETCLINIC_PET_TYPE t2
                        ON (t2.ID = t1.TYPE_ID)
WHERE ((t1.TENANT_ID = 'clinic_one') AND (t1.DELETE_TS IS NULL))

Обратите внимание на условия запроса: AND (t0.TENANT_ID = t1.TENANT_ID)) и (t1.TENANT_ID = 'clinic_one'). Эти условия неявно добавляются фреймворком CUBA.

Но есть ли способ реализовать мультитенантность на уровне не только приложения, но и БД, без изменения программного кода? Одним из решений является Citus.

Citus: мультитенантность для PostgreSQL

Как сказано в описании: “Citus - это беспроблемный Postgres, созданный для масштабирования. Это расширение для Postgres, которое поставляет данные и запросы в кластер устройств. Являясь расширением (а не форком), Citus поддерживает новые версии PostgreSQL, позволяя пользователям использовать новые возможности, и при этом совместим с существующими инструментами PostgreSQL.

Citus горизонтально масштабирует PostgreSQL на несколько серверов путем сегментирования и тиражирования. Его движок исполнения запросов распараллеливает входящие SQL-запросы по серверам, обеспечивая ответы больших массивов данных в режиме реального времени (менее секунды).”

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

Это значит, что вы можете использовать модель мультитенантности CUBA и получите физически разделенные базы данных без переписывания запросов. Рассмотрим подробнее процесс создания базы данных на Citus.

Прежде всего нужно создать кластер базы данных и добавить к нему ноды. Кластер состоит из ноды-координатора и рабочих нод. каждая рабочая надо хранит данные, специфичные для групп пользователей, а координатор перенаправляет запросы согласно колонке “tenant_id”. Приложение посылает запросы только координатору. Этот процесс представлен на картинке ниже (изображение взято из документации Citus DB):

text

Также для работы с Citus нужно определить типы таблиц во время создание БД. Есть три типа:

  1. Распределенный. Это таблицы, разделенные между рабочими узлами. Они содержат данные, специфичные для тенантов.
  2. Ссылочные таблицы - хранят информацию, которую могут использовать все тенанты. В случае PetClinic это будут сущности “Specialty” и “Pet Type”.
  3. Локальные таблицы. Сервисные таблицы, которые не задействованы в запросах на соединение с распределенными или ссылочными таблицами. Например, таблицы метаданных Citus, хранящиеся в узле-координаторе.

Корпоративная версия Citus позволяет разделять данные групп пользователей явно, назначая оператор:

SELECT isolate_tenant_to_new_shard('table_name', 'tenant_id');

Это заменит разделение на основе хэша, которое используется по умолчанию.

Использование Citus в CUBA

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

Процесс создания БД изменен. Нужно создать расширение для Citus, добавить ноды и репликацию:

CREATE EXTENSION citus;

SELECT * from master_add_node('localhost', 9701);
SELECT * from master_add_node('localhost', 9702);

SET citus.replication_model = 'streaming';

Одно из удобств состоит в том, что большую часть времени можно разрабатывать приложение на локальной “обычной” PostgreSQL, а скрипты создания таблиц Citus добавить уже непосредственно перед пробным развертыванием или продакшеном (если у вас хватит смелости пропустить пробный этап). Единственное, что изменится, это адрес базы данных - вместо локальной дев-БД нужно задать адрес БД кластера координатора Citus.

Что касается создания и обновления БД, необходимо определить распределяемые таблицы эксплицитно (имя таблицы и колонку разграничения):

SELECT create_distributed_table('petclinic_vet', 'tenant_id');
SELECT create_distributed_table('petclinic_owner', 'tenant_id');
SELECT create_distributed_table('petclinic_pet', 'tenant_id');
SELECT create_distributed_table('petclinic_visit', 'tenant_id');

И подобным же образом сделать это со ссылочными таблицами:

SELECT create_reference_table('petclinic_specialty');
SELECT create_reference_table('petclinic_vet_specialty_link');
SELECT create_reference_table('petclinic_pet_type');

Чтобы улучшить производительность, помимо разграничения можно добавить слияние таблиц. Но со стороны CUBA не нужно ничего менять, мультитенантность - более чем прозрачный процесс.

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

Заключение

Создание мультитенантных приложений может оказаться весьма непростой работой. Нужно учесть все возможности, прежде чем начать реализовывать этот подход. Но технологии - только часть сложностей такой задачи: нужно также учитывать стоимость лицензий, тарифы облачных сервисов, удобство сопровождения, масштабируемость и т.д. Сегодня модель PaaS наравне с контейнеризацией и налаженными DevOps-процессами выглядит более привлекательно, чем “традиционная” мультитенантная архитектура, однако спрос на такие приложения все еще достаточно высок.

Если вы планируете в дальнейшим работать с мультитенантностью, убедитесь, что ваше приложение соответствует всем требованиям безопасности, таким как GDPR, HIPAA и т.д. Некоторые из них могу открыто запрещать хранить данные в одной и той же базе, так что вам нужно будет задействовать подход с отдельными базами данных для соответствия таким требованиям.

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

Андрей Беляев