Как мы реализовали Low-code на микросервисах
Привет Хабр!
Меня зовут Алексей Пушкарёв, я — архитектор продуктовых решений компании ELMA. Наша команда разрабатывает одноименную Low-code платформу. В этой статье я расскажу, почему мы выбрали микросервисную архитектуру для Low-code системы вместо классической монолитной, которой до этого занимались много лет. Поясню, почему использовали именно такие технологии и решения, с какими недостатками сами столкнулись. Поговорим, как такая архитектура сказалась на Low-code разработчиках.
Для кого эта статья? Для архитекторов, аналитиков, внедренцев и тимлидов и всех тех, кому интересна тема Low-code. Мне кажется, что в публичном пространстве мало информации об архитектуре таких решений и применяемых технологиях. Для многих они до сих пор остаются черным ящиком, что нередко приводит к обманутым ожиданиям, и в целом недоверию к Low-code как к технологии. Хочу показать, что находится под капотом у таких систем на примере платформы, которую сами разрабатываем.
Пару слов о Low‑code и No‑code
Итак, наша команда разрабатывает Low-code платформу ELMA365. Давайте разберемся, что это значит. Термин Low-code разные вендоры трактуют достаточно по-разному, поэтому напишу, что подразумеваем под этим мы. Low-code — это набор инструментов для разработки приложений и среда для исполнения этих приложений. Исходя из названия понятно, что цель таких систем — снизить объем кода, который потребуется для разработки.
Но в отличии от No-code решений, в Low-code программный код остается полноценным инструментом разработки. Нет цели полностью избавиться от кода. Low-code платформу можно воспринимать как фреймворк, где часть разработки построена на различных визуальных конструкторах. А в тех случаях, где кодом решить задачу получится быстрее или требуется расширить возможности системы используется программирование.
Что представляют собой инструменты Low-code разработки?
Инструменты разработки в Low-code — это конструкторы, чаще всего различные редакторы схем, WYSIWYG-редакторы, или просто наборы таблиц для настройки бизнес-логики работы приложения. Примеры — редактор схемы бизнес-процесса и WYSIWYG-редактор для создания пользовательских форм. Для того чтобы заработали схемы, настройки и другие артефакты, которые мы создали в визуальных инструментах платформы, используется движок. С помощью него осуществляется интерпретация этих моделей. Если быть точнее, есть несколько движков для разных моделей. Движки также включены в платформу и составляют среду исполнения для разработанных приложений.
Структура компонентов, из которых состоит решение на Low-code платформе ELMA365
Какие приложения можно разрабатывать с помощью Low-code платформ? Практически любые. Но, как правило, у каждой платформы есть своя ниша. Например, наша платформа, специализируется на разработке корпоративных приложений, нацеленных на совместную работу пользователей и автоматизацию бизнес-процессов компании. Эта сфера разработки, как правило, связана с постоянно меняющимися требованиями и необходимостью постоянной доработки приложений.
Почему мы решили делать Low-code на микросервисной архитектуре?
Мы начинали разработку системы как облачное SaaS-решение для малого и среднего бизнеса в 2018 году. Своих распределенных дата-центров у нас нет, поэтому мы арендуем вычислительные мощности. Исходя из этого мы очень внимательно относимся к использованию вычислительных ресурсов. Наши потребности:
гибкая система управления нагрузкой — управление лимитами нагрузки для компаний,
гибкая автоматическая масштабируемость системы под нагрузкой, создаваемой клиентами.
К тому времени у нас уже был успешный опыт разработки монолитной Low-code системы (наш прошлый продукт) и первая мысль была — адаптировать под работу в SaaS. Однако выстроить эффективное управление нагрузкой на разные экземпляры системы для разных компаний в нашей архитектуре у нас не получилось. Все сводилось к развертыванию отдельных виртуальных машин под каждого клиента, что приводило к нескольким проблемам:
высокая себестоимость решения,
сложность автоматизации управления нагрузкой.
Поэтому мы приняли решение — строить систему с нуля, используя микросервисную архитектуру с контейнеризацией и автоматическим масштабированием приложения. Этот вариант давал нам существенное преимущество — гибкость. Ну и чего скрывать, в то время тема микросервисов была на пике популярности.
Промышленным стандартом для такой задачи можно назвать Kubernetes. Этот сервис позволяет гибко настраивать масштабирование серверных ресурсов и предоставляется облачными провайдерами. На нем мы свой выбор и остановили.
Вторым следствием SaaS-решения явилась мультитенантная архитектура и разделяемая инфраструктура. Мультитенантная архитектура очень распространена среди таких решений (SaaS), можно сказать Best practice. Ее суть сводится к тому, что в одном экземпляре приложения работает одновременно несколько компаний (клиентов) и используются общие вычислительные ресурсы. Изоляция компаний друг от друга осуществляется внутри самого приложения и за счет разделения баз данных. А исполнение скриптов, разработанных пользователями, происходит внутри песочницы, без прямого доступа к ресурсам сервера, только через API самой системы. Это позволяет максимально эффективно использовать облачные ресурсы и не допускать несанкционированного доступа к чужим данным.
Кроме того, разделение на сервисы позволило нам организовать работу нескольких команд и разделение на несколько репозиториев. Каждая команда работает в своем изолированном пространстве, соблюдая контракт для работы с другими сервисами системы. Также существует набор внутренних стандартов, библиотек и принципов для всего процесса разработки. Замечу, что разделение на сервисы и соответствующая инфраструктура позволяет легче встраивать в систему свои High-code сервисы в тех случаях, когда Low-code инструментов становится недостаточно.
Мультитенантная и микросервисная структура ELMA365
Спустя пару лет у нас появилась также поставка On-Premises для развертывания внутри компаний заказчиков, т.к. использовать SaaS-решение оказались готовы далеко не все. Принципиальных изменений в архитектуру для On-Premises решений мы не вносили. Допускаю, что для части клиентов такая архитектура избыточно сложна, особенно когда речь не идет о высоких нагрузках. Однако реализовать поддержку двух видов архитектур в одном приложении практически невозможно — получились бы разные приложения. Решение, которое мы нашли — вариант поставки On-Premises с упрощенным вариантом администрирования и упаковкой кластера в один контейнер.
Архитектура системы глазами Low-code разработчика
В большинстве известных мне Low-code платформ используется монолитная архитектура, как с точки зрения разработки платформы, так и с точки зрения разработки решений на платформе. Существует подход, когда используются отдельные инструменты Low-code, например, процессный движок, а все остальное разрабатывается кодом — там может применяться микросервисная архитектура. Но это все же классическая разработка с отдельно взятым движком, а не разработка на Low-code платформе.
В ELMA365 мы скомбинировали микросервисную архитектуру платформы и привычную для Low-code разработчиков единую среду разработки и исполнения решений.
Если смотреть на систему глазами Low-code разработчика, то процесс разработки будет выглядеть как построение монолитного решения. При этом внутреннее разделение на сервисы и необходимость связывать их между собой будет от него скрыто. Почему так?
Во-первых, специфика задач, которая решается Low-code инструментами — это достаточно высокоуровневая разработка бизнес-логики, затрагивающая разные аспекты работы компании. Часто требуется работа с данными из разных областей в одном месте.
Во-вторых, сами разработчики фокусируются на логике бизнеса, а значит мыслят сквозными процессами, затрагивающими разных пользователей. Поэтому для них логика разбивки решений автоматизации может не вписываться в концепцию сервисов.
Однако мы заложили концепцию разделения функциональности на несколько решений. Принцип разделения идет по бизнес-задачам и различным сферам деятельности компании, а не по группировке функциональности. Такой подход нам кажется более простым и понятным как для аналитиков, так и для Low-code разработчиков. А в случаях, когда необходимо расширение самой платформы, например, при поддержке специфического протокола обмена данными, применяется классическая разработка микросервисов и интеграция с ними.
Технологический стек
Мы построили нашу платформу на микросервисной архитектуре, и это дает нам некоторую гибкость в разработке сервисов для системы и подборе оптимального инструмента разработки. Мы можем использовать разные языки программирования, разные системы хранения данных, разные фреймворки и библиотеки.
Однако мы не стали ударяться «в полную самостоятельность» и писать каждый сервис «кто во что горазд». Для компании содержать множество центров компетенций по каждому языку программирования и фреймворку очень накладно. Это доступно только компаниям-гигантам с тысячами разработчиков. Поэтому мы взяли за внутренний стандарт разработку на языке Go.
Этот язык достаточно простой, производительный и очень хорошо подходит для микросервисной разработки. Не буду подробно останавливаться на Go, я думаю многие с ним знакомы. Также есть несколько сервисов, разработанных на C#, например, сервис для автоматической генерации документов и работы с ними в формате Microsoft office. C# был выбран, потому что, во-первых, у нас уже есть такие компетенции (предыдущие наши продукты ELMA ¾ были разработаны как раз на нем), а во-вторых, нам подходит набор библиотек, который есть под .Net для работы с документами.
При разделении системы на отдельные микросервисы мы стремимся придерживаться принципа изоляции вычислений при выполнении бизнес-транзакций. Поэтому в одном сервисе может быть сосредоточено достаточно много логики.
На текущий момент в системе несколько десятков сервисов. Если требуется выделить новый — внимательно анализируем требования. Без серьезного обоснования необходимости мы новые микросервисы не выделяем. Каждый сервис работает со своим набором данных. Доступ к данным другого сервиса осуществляется через сам сервис, а не прямым вызовом в базу данных.
Совсем упрощать сервисы мы не стали — в какой-то момент, на наш взгляд, сложность интеграции сервисов между собой уже начинает превышать сложность самой логики его работы. Конечно, простой сервис можно относительно безболезненно написать заново, вместо того, чтобы в нем глубоко разбираться и дорабатывать. Но просто так заменить один сервис на другой не получится: важно сохранить совместимость и реализовать миграцию. Иначе могут пострадать Low-code решения, созданные на платформе. Поэтому мы выбрали нечто среднее между мелкими простыми сервисами и монолитным решением, а к переписыванию сервисов целиком прибегаем достаточно редко.
Общение между микросервисами мы построили на gRPC вызовах для синхронного взаимодействия. Для асинхронных вызовов и событийного взаимодействия используем шину RabbitMQ. Для работы с фронтовой частью и внешнего API — HTTP-интерфейс. Также он используется для исполнения пользовательских скриптов, создаваемых в рамках Low-code решений. Передача данных между сервисами осуществляется, как правило, в параметрах в самом сообщении. Для передачи значительных объемов данных может применяться и внешний кэш, для него мы используем Redis.
Ниже на схеме отражены главные сервисы используемые в системе. Оговорюсь, что некоторые нюансы опущены, чтобы сохранить читаемость.
Схема взаимодействия микросервисов
Фронтовая часть — web, десктоп и мобилка
Кратко расскажу про фронтовую часть нашей системы. Мы предоставляем 3 интерфейса для пользователей: web, десктопное и мобильное приложения. При этом все они построены на web-технологиях, поэтому мобильное и десктопное приложение — это по сути тонкие обертки. А в основе — web-приложение. В десктопе мы используем Electron, а в мобильном — Cordova.
Разрабатывать нативное мобильное приложение мы не стали, т.к. в этом случае для Low-code платформы придется обеспечить работу интерфейсов, созданных Low-code разработчиками, на всех платформах. А реализовать отдельный интерпретатор для мобильного интерфейса — задача крайне сложная и вряд ли получится лучше, чем существующие html-интерпретаторы.
Само web-приложение построено на Angular и представляет собой single page application c динамической подгрузкой данных с сервера. Это позволяет нам строить достаточно отзывчивый интерфейс, несмотря на некоторую избыточность и неоптимальность, порождаемую самой спецификой Low-code подхода. В создаваемых Low-code решениях интерфейс разрабатывается с помощью визуального конструктора, а разработчики в большинстве задач не готовы тратить время на тонкую оптимизацию.
Сама фронтовая часть разработана монолитно, в отличие от бэкэнда. Разбивку на микрофронтенды на данный момент не применяем. Так сложилось исторически, допускаю, что придем к этому в дальнейшем.
Где хранятся данные?
Основным хранилищем пользовательских данных выступает база данных PostgreSQL. В ней находятся структурированные данные, которые использует сама платформа. В ней же находятся данные объектов, которые разрабатывает Low-code разработчик в своих приложениях. Замечу, что Low-code разработчик не работает напрямую с хранилищем, а использует либо интерфейс системы (где настраивает структуру данных, возможности фильтрации, формы приложений), либо внутренний SDK в сценариях. Работу со структурой данных, индексами и построение запросов платформа берет на себя.
В PostgreSQL большинство данных компании хранятся в единой схеме данных. То есть мы не стали выделять отдельные базы под каждый микросервис. При этом мы разделяем схемы между разными компаниями — одна схема на каждый тенант. Такой подход нам позволяет с одной стороны упростить администрирование БД и упростить создание бэкапов, с другой изолировать данные клиентов друг от друга и повысить безопасность. Также это обеспечивает целостность и связность на уровне базы данных. Хотя мы придерживаемся подхода, что каждый сервис работает со своим набором таблиц, это обеспечивается на уровне самой разработки, а не раздельных схем данных.
Структура таблиц в базе у нас тоже не совсем классическая: данные пользовательских объектов хранятся в JSONB-полях в виде документов, рядом также в JSONB-поле лежат настройки доступа к элементу. Связи между объектами хранятся в отдельной таблице связей, реализуя, таким образом, связь «многие ко многим». Такой подход позволяет нам с одной стороны реализовывать Schemaless-хранение структурированных данных, проще реализовывать операции изменения структуры данных через Low-code инструменты и хранить вложенные структуры. С другой стороны, мы можем использовать реляционные механизмы соединения таблиц и построение выборок данных, например, для отчетности. Для ускорения выборок мы используем индексацию полей документов хранящихся в JSONB-полях. В системных таблицах, используемых самой платформой, мы уже применяем и классическую реляционную структуру полей. Хотя где-то используем и JSONB-поля с вложенными структурами. Такой подход к хранению данных можно назвать гибридным. Замечу, что в работе с данными мы используем принцип мягкого удаления: из интерфейса системы или Low-code инструментов жестко ничего удалить нельзя. Такой подход для бизнес-пользователей более предпочтителен, поскольку снижает риск потери данных в случае ошибок пользователей или Low-code разработчиков и позволяет сохранять целостность данных.
Пример структуры хранения пользовательских данных
При этом одним хранилищем данных мы не ограничиваемся, и часть данных вынесли в MongoDB. В частности там лежит часть системных данных, например, настройки компании и авторизационные данные пользователей. Кроме того, мы вынесли в MongoDB хранение сообщений и чатов. Для этих задач MongoDB показала лучшую производительность и удобство хранение в сравнении с хранением данных в PostgreSQL.
Для хранения файлов используем S3 совместимое хранилище, которое может считаться современным стандартом, в нашем случае это система MinIO.
Плюсы и минусы микросервисной архитектуры
Главные плюсы в реализации микросервисной (сервисной) архитектуры:
Удобно вести параллельную разработку платформы несколькими командами и поддерживать достаточно высокий темп.
Гибкое и автоматическое масштабирование системы, эффективно используются ресурсы кластера в SaaS-решении.
Возможно построить отказоустойчивое решение как в условиях размещения в облаке, так и в частном кластере.
Сохраняется достаточная простота и понятность разработки решений для Low-code разработчиков.
К минусам я бы отнес:
В случае использования поставки On-Premises к администратору на месте будут предъявляться существенные требования по настройке и администрированию инфраструктуры, то есть определенный уровень знаний настроек Kubernetes. В помощь ему мы постарались все подробно описать в справке. Также отдельного внимания и существенных серверных ресурсов будут требовать системы логирования и мониторинга.
Более сложная отладка и поиск ошибок в сравнении с монолитной архитектурой при разработке платформы. При эксплуатации системы, также могут потребоваться дополнительные инструменты мониторинга и обработки логов.
Избыточное потребление ресурсов на накладные расходы в случае поставки On-Premises, когда не требуется гибкое масштабирование инфраструктуры.
Спасибо, что прочитали этот лонгрид до конца! Статья получилась достаточно объемной. Надеюсь, что вы узнали для себя что-то новое о Low-code системах и наш опыт окажется для вас полезным.
В заключении хочу сказать, что наша команда разработки не стремится создавать решение строго «по учебнику». Все практики знают, что непрерывная промышленная разработка требует гибкости, интеграции различных концепций и бесконечной череды компромиссов между различными вариантами. Мы стремимся находить не идеальные, но «достаточно хорошие» решения, которые мы можем воплощать и поддерживать своими силами в обозримые сроки и с приемлемым качеством.
Как вы считаете, выигрывают ли Low-code системы, используя микросервисную архитектуру вместо классического монолита?
И что в целом думаете про идею использования Low-code платформ как фреймворка для разработки?