ERP на сервисной архитектуре

3262
#Разработка 18 ноября 2020

Мы в AGIMA занимаемся разработкой проектов самых разных типов как неповоротливых и медленных корпоративных систем, так и быстро развивающихся digital-продуктов. Бывают водопадные долгоидущие корпоративные порталы, бывают живые, быстро изменяющиеся проекты (например,Пятерочка и Перекресток). Работа по некоторым проектам тянется годами (АльфаСтрахование), бывают такие, которые завершаются за несколько месяцев. Иногда приходится делать маркетплейс автомобилей за 3 месяца (об этом расскажу как-нибудь в другой раз) или писать телеграмм бота за пару вечеров.

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

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

С самого начала стало понятно — будет сложно. По сути, перед нами стоял вызов — требовалось реализовать крупную ERP-систему с нуля. У клиента не было ни четких процессов (а значит, ERP нужно было сделать максимально гибкой к изменениям), ни финального спектра задач.

Большинство ERP, с которыми мы сталкивались, были на монолитной архитектуре. В этот раз мы решили предложить клиенту SOA (Service Oriented Architecture). И вот почему:

Почему именно сервисы?

ERP-система требует щепетильного подхода к процессам внутри компании, где она внедряется. Ее основная задача — управлять ресурсами и оптимизировать их использование.

Наш клиент занимается лизинговым обслуживанием, его компания является частью большой корпорации. Цели весьма глобальны — вплоть до экспансии на рынки Европы. Российский рынок еще не до конца освоен, но сроки уже обозначены, KPI выставлены — поэтому нужно было готовиться не просто к увеличению нагрузки (причем не только моральной), а к серьезным интеграциям и большим сложным расчетам на стороне системы.

И, конечно, вопрос о выборе архитектуры и технологий был критичным.

Первое, что хочется сказать:сервисная архитектура — не чудесная панацея и спасение от всех проблем. Перед тем, как выбрать ее, убедитесь, что вы не просто гонитесь за хайпом вокруг слова «микро». Если строить сервисы с таким подходом, вы скорее всего поймаете все проблемы монолитов, приправленные соусом из высокой связанности на сетевом уровне, да еще и с порогом входа высотой с Эльбрус (импортозамещение!).

Почему же мы выбрали именно SOA?

Неотлаженные процессы у клиента

На всякий случай уточню, что наш основной стек — Python. Практически весь бэкенд написан именно на нем. Но это не имеет особого значения.

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

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

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

Архитектура была выстроена на основе событийной модели, когда данные между различными компонентами системы передавались в определенном формате и вызывались из различных сервисов. Не было четкой градации того, какой сервис какие данные должен получить. При создании события в системе А, оно может попасть сразу в Б, В и Г.

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

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

Большое количество интеграций

Интеграция в монолитных системах — дело довольно тривиальное. Но и с ней бывают трудности.

Связаны они в основном с тем, что иногда нам приходится интегрироваться на разных версиях библиотек. К примеру, один из сервисов CRM общался на протоколе oData v4, другой из сервисов CRM — на oData v2. В случае с монолитом, возникли бы сложности. Пришлось бы поднимать 2 разных версии контейнеров с разными наборами пакетов. А что, если пакеты языка начнут конфликтовать еще и между собой? Усложняются отладка и разработка, увеличивается количество зависимостей в системе.

SOA решает вопрос проще — мы можем агрегировать всю логику сериализации данных и работы с конкретным протоколом внутри сервиса. Внешним потребителям будет доступна только нужная API.

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

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

Масштабируемость

Речь идет не только о техническом масштабировании ресурсов серверов — об этом уже рассказано в сотнях статей. Все прекрасно понимают, что в SOA гораздо легче манипулировать трафиком и поднимать больше контейнеров того сервиса, который испытывает деградацию. Но важен еще и момент масштабирования команды.

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

Для примера: гораздо проще понять, как устроен геосервис с небольшим набором API, с парой асинхронных методов и 5-10 моделями в БД, чем найти такой же модуль в монолите и понять, как и где он используется в остальных модулях.

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

Инкапсулирование

В своей практике мне случалось делать ужасные вещи — копировать целые методы, слегка меняя их названия и добавляя пару строк лишнего кода. Делалось это из-за того, что эти методы/классы использовались в большом количестве API и асинхронных задач. Любое вмешательство могло привести к непредвиденным результатам той или иной логической операции. Да, от этого есть спасение, хотя и не на 100% действенное — автотесты. Но все же знают, в каком мире мы живем — мало кто готов потратить 8 часов на написание тестов ради добавления одного логического условия в метод. Есть, конечно, и другие способы, но все они достаточно объемны в сроках реализации.

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

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

Какими преимуществами обладают сервисы в ERP?

Что нам дало внедрение архитектуры SOA в ERP? Наверно, больше всего было головной боли. Но, конечно, и профита получилось немало.

SOA выглядит очень красиво, но требует тщательного подхода к проработке деталей. Каждый из пунктов «почему именно SOA» имеет свой набор косяков и сложностей. Но все же положительный эффект их многократно перекрывает.

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

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

Система вышла достаточно объемной, но при этом лаконичной. Мы до сих пор развиваем и дорабатываем ее, но уже сейчас видно, что разобраться в конкретном сервисе гораздо проще, чем осознать, что происходит во всей ERP. На некоторые из этих сервисов мы даже вывели джунов, которые весьма успешно справляются с поставленными перед ними задачами.

Я бы не назвал следующий пункт объективным плюсом, но для меня он им является. Мы смогли испробовать несколько новых фреймворков, попробовали новые решения в конкретных небольших и изолированных сервисах. Это дало возможность испытать их в продакшене и понять, где есть подводные камни, где еще не протоптана дорожка на stackoverflow, а где, вообще, еще не исправлены баги.

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

С какими проблемами мы столкнулись

Архитектура

Самый важный и самый проблемный компонент. Вам не удастся выжить, если в команде нет сильного архитектора. Архитектура решает все. Любая мелочь на этапе архитектуры может привести к килодням переработок и исправлений не одного, а целой плеяды сервисов.

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

Стоимость ошибки на разных этапах разработки:

tabl.png

Документация

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

Разработчикам тоже не всегда удается быстро вспомнить, почему этот класс или метод реализованы именно так, а не иначе. Поскольку система напрямую реализует бизнес-процессы, избежать специфичных выкрутасов в коде не получится. Почему, например, в этом методе есть пограничное значение, после которого логика кардинально меняется? Вопрос... И такие вопросы — на каждом шагу. Многие из них решил процесс документирования. Мы описываем базовые схемы сервисов, рисуем диаграммы потоков, описываем статусную и объектную модель, делаем краткую аннотацию к каждому сервису. В коде подробно прописываем, что тестирует каждый тест, даем комментарии к сомнительным моментам в коде. Конечно, в каждом сервисе есть автогенератор swagger, и каждый сервис предоставляет его по своему URL, закрытому авторизацией и привилегиями доступа.

tabl.png
tabl.png

Скриншоты нарочно заблюрены, NDA. Но суть должна быть понятна.

Команда проекта достаточно быстро расширяется, неизбежна и текучка и сменяемость. Если на старте я тратил примерно полдня на рассказ новому разработчику, зачем нужна эта система, что делает каждый из сервисов, и как они общаются между собой, то сейчас, мне достаточно объяснить только бизнес-составляющую и отправить разработчика читать документацию и изучать код. Конечно, документация не даст 100% понимания проекта, вопросы, естественно, останутся, но разработчик поймет 80% проекта.

Инфраструктура

С инфраструктурой тоже все не так просто, как кажется. Не нужно забывать, что любой некорректно настроенный API может привести к проблемам.

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

Это требует специальных манипуляций с API, плюс требуется тщательнее подходить к этапу сериализации данных.

То же касается синхронизации данных между сервисами. Если где-то в системе изменились данные, скорее всего, это потребует изменения данных в другом сервисе, который их использовал. Мы для этого используем брокер сообщений RabbitMQ. В дальнейшем, если система разрастется, планируем интегрировать Kafka.

Kafka — более мощный брокер, поддерживающий колоссальное количество транзакций. Но его использование требует более строгой поддержки, ведь его настройка и внедрение гораздо сложнее, чем у того же RabbitMQ. В нашем случае количество транзакций и интеграций пока не достигло такой величины, чтобы вкручивать его в инфраструктуру. Мы решили удешевить разработку, поскольку на 100% уверены, что кластер RabbitMQ справится с предстоящими нагрузками. Я бы не рекомендовал сразу подключать Kafka, если нет каких-то железных аргументов в его пользу.

Помимо прочего, для сервисной архитектуры не подойдут упрощенная инфраструктура на docker или docker-compose. Вы не сможете организовать достаточную гибкость и универсальность решения. Необходимо настроить оркестратор (kubernetes или его аналог) и держать специальную команду DevOps-поддержки, поскольку разработчики backend/frontend не имеют нужных специализаций в DevOps и не смогут обеспечить надлежащий уровень поддержки.

Подробнее об этом можно почитать в статье моего коллеги.

Мониторинги

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

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

Каждый сервис имеет soft-delete механизм, гарантирующий, что ни один объект в сервисах не будет физически удален из БД.

Также мы мониторим объем трафика в сети, чтобы предсказать ее деградацию. Мониторятся все запросы в системе, проверяются изменения на SSO, логируются все изменения файлов, изменение объектов на каждом сервисе.

И конечно же, стандартные параметры, вроде чтения/записи дисков, нагруженности баз данных, CPU и т.д.

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

tabl.png
tabl.png

Авторизация ресурсов

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

Для оптимизации этого момента мы реализовали SSO-сервис на основе JWT-токенов. Принцип их работы описан на Хабре не раз, я остановлюсь лишь вкратце. В JWT есть три основных блока: header, payload, signature. В header описано, как должен вычислять JWT-токен, в payload — полезные данные, signature шифрует данные и подписывает их секретным ключом. Благодаря криптографической подписи, JWT-токен валиден в течение всего срока его жизни и позволяет быть уверенным, что данные в payload не скомпрометированы.

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

tabl.png

Это позволяет нам экономить ресурсы — убрать полезную информацию внутрь payload. В этом случае нам не придется постоянно запрашивать привилегии пользователей в SSO-сервисе, а кроме того, мы можем хранить метаданные пользователя (например, ФИО) прямо в токене.

Сетевые издержки

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

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

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

CAP-теорема

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

Я не буду уделять ей много внимания. Чего-то интересного в решении этой теоремы мы на проекте еще не достигли. Думаю позже мы выпустим отдельную статью по ее решению.

Безопасность

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

Вышеперечисленные требования касаются как SSO, API, интерфейсов frontend, инфраструктуры, так и работы с хранилищем файлов. Не нужно забывать, что любой файл, хранимый в системе, должен быть обязательно проверен антивирусом и лежать в изолированном хранилище, к которому нельзя подключиться через какой-либо сервис.

Основные требования, которые используются не повсеместно — сканирование статических анализаторов кода, проверка на большое количество классических уязвимостей, регламент предоставления доступов к БД, отсутствие рутовых пользователей на серверах. Этот список достаточно объемный.

Итог

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

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

В данный момент у нас реализовано более 15 сервисов, пять библиотек sdk, три каркаса для фреймворков с общими модулями (Django, Aiohttp, Fastapi).

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

Мы использовали Django на первых этапах, т.к. нам была нужна быстрая административная панель для управления объектами. В ближайшее время начнутся работы по реализации кастомной админки, общей для всей ERP, поэтому дни нынешней сочтены. Django — прекрасный фреймворк, но он гораздо медленнее, чем асинхронные Fastapi.

Все новые сервисы реализуются уже на Fastapi, т.к. он понравился нам больше чем Aiohttp.

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

Вот такая сложная, долгая, многоступенчатая, полностью состоящая из нюансов работа.




Контент-хаб

0 / 0
+7 495 981-01-85 + Стать клиентом
Услуги Кейсы Контент-хаб