Один из наших длительных проектов - это крупное многопользовательское SaaS-решение (CRM-система) основанное на микросервисной архитектуре и развернутое в облаке Azure. Изначально это был MVP, где все части (сервисы, базы данных и т. д.) располагались на одной виртуальной машине. Со временем проект вырос в облачное распределенное решение с множеством веб- и мобильных клиентов.
В этой статье мы расскажем, как решили одну из проблем, с которой столкнулись в процессе разработки.
На тот момент проект включал около 12-14 сервисов, каждый из которых имел собственную базу данных в Azure SQL. Однако большинство сложных и критичных поисков осуществлялось по основной базе данных, в которой хранилось огромное количество информации о кандидатах (принятых, отклонённых, с которыми ведётся работа) по всем арендаторам.
CRM-решение было гибким и позволяло настраивать свойства кандидатов и множество связанных с ними данных, таких как история взаимодействия, профессиональные навыки и т. д. Поиск внутри приложения позволял задавать свои критерии фильтрации с большим количеством различных условий и переменных.
Хотя эта функция работала хорошо большую часть времени, всё же возникали проблемы из-за других факторов:
Всё это приводило к пересечению поисковых запросов с пакетным обновлением данных, высокой нагрузкой на базу данных, блокировками записей и перестройкой индексов, что ухудшало производительность поиска.
Решением стала разработка Search Engine - отдельного сервиса для эффективной реализации поиска.
Первым шагом было решено вынести функционал поиска в новый сервис, развернутый в собственном Azure App Service Plan. Это позволило выделить больше памяти и процессорных ресурсов и масштабировать их независимо. Важно, что App Service Plan также поддерживает масштабирование и переменное количество экземпляров сервиса, что положительно сказалось на надёжности и производительности критически важных функций.
Кроме того, мы внедрили API-шлюз, который скрыл за собой несколько сервисов и обеспечил возможность плавного переключения между основным сервисом рекрутинга и новым сервисом поисковой системы.
Другой сложной задачей было избежать снижения производительности поиска при массовых обновлениях в основной БД. Мы рассмотрели несколько подходов:
Elasticsearch - отличное решение для мощных поисковых функций, но в нашем случае оно не подошло из-за необходимости частого обновления данных и перестройки индексов.
Основная проблема заключалась в том, что наша база данных допускала и поддерживала единичные и массовые обновления данных и кастомизацию. Это означало, что для внесения изменений потребовалось бы переписывать большие объемы данных в Elasticsearch и часто перестраивать индексы.
Кроме того, это означало бы, что нам придется разрабатывать и поддерживать мост данных между двумя базами данных.
Идея синхронизации баз данных выглядела перспективно, но инструмент имеет ограничения и не поддерживает некоторые типы данных. Поэтому мы были вынуждены отказаться от него.
Рекомендуемый подход от Microsoft - использовать архитектуру High Availability с первичной БД для чтения и записи, и одной или несколькими вторичными репликами только для чтения.
Но эта функция поддерживается только на уровнях Premium и Business Critical в Azure SQL, что увеличило бы расходы. Поэтому для нашего клиента это решение не подошло.
У нас большой опыт разработки облачных решений на AWS и Azure.
Geo-replication - оказалось единственным подходящим для нас решением. Функция Geo-replication позволила реализовать подход read-scale out, зарезервировав дополнительный Azure SQL Server на котором была развернута Geo-реплика основной базы данных.
Еще одной задачей была необходимость поддержки кросс-запросов к базе данных. Поисковые запросы могли содержать условия, направленные к данным в основной БД, но также и дополнительные условия, которые должны были быть адресованы другой базе (в нашем случае БД с историей взаимодействия).
Первоначальная реализация включала в себя взаимодействие между двумя сервисами. Требовалось сформировать два подрезультата и собрать их в итоговый.
В качестве решения задачи мы использовали функции внешних источников данных и Cross-DB запросов. Но поскольку мы стали обращаться с поисковыми запросами к новому сервису, который работает с репликой, то было решено реализовать следующий подход:
Настройка нового Azure SQL Server с репликами обеих баз данных позволило адресовать все условия поиска в одну Базу Данных, которая использовала перекрестные запросы ко второй реплике, не привлекая к поиску ни одну из исходных баз данных.
Реализованный подход позволил:
API и реализация поиска при этом остались прозрачной для всей команды разработчиков, что также немаловажно для поддержания высокого уровня качества и скорости процесса разработки.
Этот опыт демонстрирует, как правильный выбор архитектурных решений может значительно улучшить производительность и стабильность систем, обеспечивая эффективное масштабирование и снижение затрат.