Полезная статья для девопсов и сисадминов
Как быстро зафейлить новый проект Java? Просто взять и применить все, что ты услышал на последней Java конференции;) Как быстро сделать энтерпрайзный проект минимальной командой в короткие сроки? Верно — подобрать оптимальную архитектуру и правильные инструменты. Senior Developer из команды Jmix Дмитрий Черкасов рассказывает о компромиссном варианте между хайповыми (все еще) микросервисами и монолитами, который называется Self-Contained Systems. Кажется, он выпьет меньше крови и сохранит ваши нервы. Дальше — рассказ от первого лица.
Отмечу, что мы будем называть Self-Contained Systems «автономные системы» или кратко — SCS.
Как следует из названия, принцип подхода прост — разбиваем систему по доменам. В каждом домене находится независимый монолит (система). Стараемся максимально не допускать связность между подсистемами. Напоминает микросервисы? Но не делайте поспешных выводов!
Пройдя «Вьетнам» на одном энтерпрайз проекте с SCS-подобной архитектурой, я захотел сделать R&D по автономным системам. Хотелось бы поделиться опытом исследований и рассказать как готовить SCS. Какой спектр задач может покрыть новоявленная концепция и для каких проектов SCS станет идеальным решением?
Как я узнал о SCSКак-то на одном из дейликов или планерок мы с коллегами подняли срач дискутировали на тему таргет аудитории Jmix. Можно ли написать огромный маркетплейс без колоссальных усилий на платформе? А так как я уже знал ответ на этот вопрос, пройдя медные трубы в кровавом энтерпрайзе, у меня возникла мысль написать проект, демонстрирующий, что можно сделать, используя монолиты в 24 году (куда большего, чем вы могли бы представить).
Очевидно, в бигтехе — Сбере, Озоне, ВТБ и других явова (см. ЯВОВА/ВОТВАСЯ) все архитекторы будут воротить нос, как только услышат слово «монолит». В общем, у кого-то это щелкает, а у кого-то это не щелкает.
В первую очередь, SCS, как и сама статья, будут интересны людям, занимающимся E-commerce приложениями. Насмотревшись на кровавый энтерпрайз, могу сказать, если у вас скудный ресурс, а система большая, то SCS может стать целебной таблеткой, решающей многие проблемы среднестатистического проекта.
Вторая часть аудитории — проекты, использующие современные фуллстак-фреймворки. Я, как разработчик фреймворка Jmix, а также любитель Ruby on Rails, могу заявить, что написание SCS будет для вас занимательным опытом. Ну или хотя бы прочтение данной статьи позабавит вас, как и мои цитаты Тинькова.
Когда не стоит смотреть в сторону SCS?Первое, если у вас большая нагрузка (в основном это касается UI), сложный и нестандартный UI, который проще написать на современных фронтенд фреймворках с использованием SOA/микросервисов. В общем, не тривиальный UI — не для нас. Второе, если в проекте какие-то вычисления или риал-тайм работа с данными, проще будет сразу писать микросервисы.
Стоит отметить, что все перечисленные выше проблемы обычно не касаются энтерпрайза.
Детальное погружениеИндикатор того, что ваш проект будет хорошо ложиться на SCS — наличие большого количества доменов и бизнес-логики, связанной с заполнением всяких форм. Узнали? Это же наш (не)любимый энтерпрайз. Главное преимущество, которое получает проект при разделении на автономные системы — это «микросервисные монолиты». В таком подходе мы имеем повышенную устойчивость, деплой и другие плюшки, о которых я расскажу далее.
Если говорить душно более научно, то:
Self Contained Systems — это независимые, самодостаточные и слабосвязанные системы (подсистемы), каждый из которых представляет собой отдельную часть функциональности в рамках большого программного продукта.
Перейдем на простонародный язык. Автономные системы — это «сообщающиеся монолиты» с некоторым количеством «но», заимствованных из микросервисов. Главные концепции — минимальная связанность между системами и отказоустойчивые коммуникации.
Disclaimer. У самурая нет плана архитектуры — есть только путь задачи!В правильных руках и монолит может стать крайне эффективной архитектурой (см. GitHub, GitLab, Shopify). Не относитесь к статье как к постулатам! Зачастую, выбор архитектуры может быть делом вкуса и скиллов команды.
Хотелось бы вкратце рассказать о главной акуле SCS — Innoq. С ресурсов и сайта Innoq взято большинство контента, представленного в статье. Если перейти по ссылке, можно почитать историю об успешном успехе компании, построившие свою E-Commerce систему на базе Self-Contained Systems. Скриншот ниже, взятый с сайта Innoq, примерно знакомит нас с архитектурой SCS.
Как мы можем наблюдать, каждая из подсистем Innoq — это набор монолитов, разбитых на домены. Подмечу, есть общая система, которая собирает все разрозненные элементы UI вместе. Сейчас опустим этот момент, но чуть позже я объясню, зачем нужен этот элемент системы.
Чтобы лучше прочувствовать разницу между SOA и микросервисами, я добавил картинку ниже, демонстрирующую разницу в этих подходах.
Философия Self-Contained Systems!Сообщество энтузиастов сформулировали некоторое количество принципов, следуя которым можно написать хорошую систему, построенную на SCS. На сайте SCS можно детально почитать про данные принципы.
Основные принципы:Ниже находится пересказ всех поинтов, описанных комьюнити и Innoq.
От себя добавил некоторые недосказанные поинты, а где-то добавил комментарии. Здесь не будет никаких искажений оригинала, лишь объяснения. Мое мнение о работе SCS в реальном мире я выскажу чуть позже.
Все эти принципы по отдельности носят лишь рекомендательный характер, не стоит слишком сильно подгонять свою систему под все вышесказанные поинты. Но чем меньше вы отступаете от постулатов, тем меньше проблем вы получите в перспективе.
Ниже комментарии от меня, которые поясняют некоторую недосказанность философии Self-Contained Systems.
Теперь обсудим что мы получаем, выбрав SCS. На какие грабли можно наступить в таком подходе?
Однако все эти недостатки могут быть менее релевантными для B2B и E-Commerce систем, где количество пользователей вторично по сравнению с множеством бизнес-процессов и объемом бизнес-логики. Круто? ДА это же круто.
Той еще проблемой для меня стал выбор темы демо. CRM? Пэт-клиника? Онлайн магазин? Public Cloud? Ох, вы бы знали сколько я думал над тем, что же выбрать... В итоге, взвесив все плюсы и минусы, я решил, что мой выбор — доставка еды. Эта тема не только предоставляет широкий спектр бизнес-процессов, но и близка к повседневной реальности аудитории. Ну кто хоть раз не пользовался доставкой еды? И понять легко и бизнес-процесс прозрачный и большой;)
Раз с темой разобрались, пора переходить к планированию. В выбранной области стоит выделить:
Далее опустим некоторое количество правил, которые нам будут сильно усложнять работу. Обычно такие правила являются жизненно важными для бизнеса, но с хай-левел точки зрения являются мелочами. Например, мы не будем обрабатывать случай, когда курьер поскользнулся на льду и расшибся:)
Пожалуй, иконка проекта выбрана. А может мне стоило попросить DALL-e сгенерировать ее вместо меня... Кстати, на иконке спойлеры =)
Пора выбрать основные домены внутри доставки еды, без которых понятие доставки не может работать.
Ресторан (Restaurant System):В этом домене фокусируется вся информация о ресторанах. Здесь осуществляется регистрация и создание новых ресторанов. Администраторы ресторанов имеют возможность настраивать меню, добавлять и редактировать блюда, а также принимать заказы на готовку еды. Этот домен является ключевым для организации бизнес-процесса внутри ресторанов.
Домен доставки фокусируется на управлении курьерами. Здесь происходит регистрация курьеров, назначение им роли доставщика, а также менеджмент заказов, связанных с доставкой. В этом домене осуществляется эффективное управление логистикой и выполнением заказов.
Центральным доменом в системе доставки еды является система заказов. Здесь пользователи начинают бизнес-процесс, создавая заказы. Интерфейс для создания заказов, наполнение корзины и все запросы, связанные с бизнес-процессом доставки еды, исходят именно из этого домена. Здесь происходит взаимодействие с другими подсистемами и интеграции для эффективного выполнения заказов.
В реальной системе должны существовать дополнительные домены, без которых доставка не имеет смысла. Эти домены включают систему платежей, уведомлений, построения маршрутов доставки и триггеров геолокации. Они представляют собой важные аспекты реальной системы, но в рамках демонстрации SCS мы упростим структуру и сфокусируемся на интеграциях между основными подсистемами.
Перед проведением демо Self-Contained Systems я решил упростить некоторые аспекты бизнес-процесса доставки еды. Мы не будем рассматривать сложные сценарии, такие как оплата и отмена заказов, чтобы минимизировать сложность написания кода и ускорить прототипирование проекта. Роли будут минимальными для удобства работы, и мы учтем другие аспекты, которые упростят и ускорят процесс написания доставки еды. Наша основная цель — продемонстрировать эффективные интеграции между различными подсистемами.
Основные задачи для демоУ нас есть три ключевые подсистемы и три типа пользователей.
Подсистемы:
Ниже представлена Flow-диаграмма того, что будет происходить в системе. Да. Она немножечко страшная;) А вы видели простые Flow-диаграммы?
Стоит сделать отступление, что будет еще один шаг, который мы опустим. Это шаг самой физической доставки курьером от точки А до точки Б. Вместо этого мы просто подождем 10 секунд и сразу поменяем статус заказа на «доставлен».
Для более наглядного представления мы решили вместо Flow-диаграммы использовать более высокоуровневую BPMN-диаграмму.
Это более интуитивное представление процесса. В общих чертах, мы создали простой сценарий:
Учитывая полную автономию и независимость наших трех систем, мы избегаем использования фреймов или ссылок, так как нет необходимости в установлении жестких связей между ними. Каждая из трех систем эффективно функционирует независимо от других.
Из-за высокой степени автономии между системами нам не требуется создавать отдельную систему, которая бы служила корневой для всех подсистем и предоставляла бы контент в виде iframe-ов для каждой из них. Вместо этого мы предпочитаем поддерживать независимость каждой системы, не нарушая ее автономии.
Такой подход дает нам гибкость и свободу в разработке каждой подсистемы, а также упрощает управление и поддержку проекта в целом, поскольку каждая система может эффективно разрабатываться и внедряться независимо от остальных.
Для самых любознательных я приведу краткий пример системы с общим layout-ом в конце статьи.
Выбор технологийМысли сбежавшего из дурки о выборе технологий
При выборе технологий для реализации архитектурного паттерна Self-Contained Systems в энтерпрайз-проекте моем демо учтем не только функциональные требования, но и особенности объектной области. А у нас есть некоторый функционал, которые можно покрыть реализациями из коробки.
В данном случае, я остановился на языке программирования Java со Spring Boot-ом. Но вы думаете что я буду использовать просто Бут, будучи разработчиком другого фреймворка?
Конечно же воспользуемся Jmix! С его помощью мы получаем не только современный UI, но и встроенный движок Bpmn, что важно для обеспечения потребностей бизнес-процессов.
Дополнительно, Flowable используется в качестве Bpmn 2.0 движка, предоставляя необходимую асинхронность и отказоустойчивость, которые являются критическими компонентами для успешной работы в корпоративной среде. В контексте управления пользователями между системами было принято решение использовать Keycloak. Этот выбор обусловлен не только простотой интеграции, но и созданием единой точки управления пользователями, что обеспечивает удобство и единообразие.
Итак, технологический стек нашего проекта включает в себя:Настоятельно рекомендую тщательно ознакомится с тезисами, изложенными ниже, тк они фактически решают какие границы ответственности мы используем.
В данном примере мы сознательно упрощаем сценарий, демонстрируя, что для построения устойчивой системы не всегда необходимо использовать асинхронные и/или реактивные методы коммуникации.
При получении ответа от другой системы мы возобновляем пользовательскую задачу, и процесс продолжает свой ход. Этот подход решает множество проблем, связанных как с устойчивостью, так и с минимизацией межсистемных коммуникаций.
Изначально статья должна была быть детальной, но если затянуть с объяснением всего, то кажется ее бы можно было изложить только в 12 часовой видос на youtube или небольшую книгу. Потому буду давать фрагменты кода и комментарии. Начнем сначала.
Нужен CRUD для ресторанов, еды и меню внутри ресторана. Так как мы пишем на Jmix — у нас есть возможность написать полноценный сайт, а не использовать Rest API и какой-нибудь Postman.
Сущности первой необходимости:
Пропущу код, связанный с созданием UI, лишь скажу, что мы создали:
Прикрутим Rest для того, чтобы можно было из системы Заказов доставать список ресторанов и их содержимого:
RestaurantController
@Secured(FullAccessRole.CODE) @RestController @RequestMapping(value = "api/v1") public class RestaurantController { private final RestaurantRepository restaurantRepository; private final DataManager dataManager; private final AttachmentService attachmentService; private final FetchPlans fetchPlans; private final RestaurantMenuRepository restaurantMenuRepository; @GetMapping("/restaurants") public List<RestaurantDTO> listRestaurants() { return StreamSupport.stream(restaurantRepository.findAll().spliterator(), false) .map(restaurant -> { var dto = new RestaurantDTO(); dto.setId(restaurant.getId()); dto.setName(restaurant.getName()); dto.setDescription(restaurant.getDescription()); dto.setIcon(attachmentService.getAttachmentAsByteArray(restaurant)); return dto; }) .toList(); } @GetMapping("/restaurants/{id}") public RestaurantDTO getRestaurant(@PathVariable Long id) { Restaurant restaurant = restaurantRepository.getById(id); var dto = new RestaurantDTO(); dto.setId(restaurant.getId()); dto.setName(restaurant.getName()); dto.setDescription(restaurant.getDescription()); dto.setIcon(attachmentService.getAttachmentAsByteArray(restaurant)); return dto; } @GetMapping("/restaurants/{restaurantId}/menus") public List<RestaurantMenuDTO> listRestaurants(@PathVariable Long restaurantId) { return restaurantMenuRepository.findRestaurantMenuByRestaurantId(restaurantId) .map(menu -> { var menuDTO = dataManager.create(RestaurantMenuDTO.class); menuDTO.setId(menu.getId()); menuDTO.setName(menu.getName()); menuDTO.setItems(convertItemsToDTO(menu.getItems())); return menuDTO; }) .toList(); } }
Итак, как только мы написали все необходимые эндпоинты, можем перейти к настройке ресторанов. Создадим пару ресторанов и еду в них, чтобы было с чем работать в системе заказов.
Предлагаю создать StarBucks, а то в последнее время его не хватает. Хотя нет, чего действительно не хватает — это Сбер Пиццы. Реализуем же фантазии, кто знает, возможно это станет правдой...
Заполним рестораны. Остановимся только на сберпицце)
Думаю со cбера хватит. Переходим к OrderService.
На этом этапе нам необходим лишь функционал создания корзины, все остальное будет реализовано в следующих шагах с бизнес-процессом.
RestaurantClient
@Component public class RestaurantClient extends AbstractClient { public List<RestaurantDTO> listRestaurants(String subjectToken) { String url = MessageFormat.format("{0}/api/v1/restaurants", caclRestaurantUrl()); return getApi(url, HttpMethod.GET, new ParameterizedTypeReference<List<RestaurantDTO>>() {}, null, subjectToken); } public RestaurantDTO getRestaurantById(Long restaurantId, String subjectToken) { String url = MessageFormat.format("{0}/api/v1/restaurants/{1,number,#}", caclRestaurantUrl(), restaurantId); return getApi(url, HttpMethod.GET, new ParameterizedTypeReference<RestaurantDTO>() {}, null, subjectToken); } public List<RestaurantMenuDTO> listRestaurantMenus(Long restaurantId, String subjectToken) { String url = MessageFormat.format("{0}/api/v1/restaurants/{1,number,#}/menus", caclRestaurantUrl(), restaurantId); return getApi(url, HttpMethod.GET, new ParameterizedTypeReference<List<RestaurantMenuDTO>>() {}, null, subjectToken); } }
Опустим лишние детали, нас тут интересует только то, что все «стартовые» запросы процесса по доставке еды могут быть синхронные, т.к. используются только на стороне юзера для UI.
Перейдем к проверке работы корзины, сделаем заказУдостоверимся, что все меню и еда пришла верная
Соберем инвестиционный портфель. Ой, то есть соберем корзину еды, конечно же.
Отлично, корзина готова и можно приступать к самому интересному — бизнес процессу, который мы обозначили пару топиков назад.
Так как у нас есть Flowable и есть реальный бизнес процесс, который можно описать, то построим всю доставку еды на BPMN:
Все те же шаги, что и в том красивом бизнес процессе. Осталось дело за малым - привязать все ServiceTask (те что с колесиком) к Java коду.
Больше деталей: как UserTask решит все мои проблемыМногие задумались, а как я буду решать проблему, когда возникает некоторое ожидание, когда мы ждем готовку еды или когда ищем курьера?
И тут самая главная идея BPMN нам помогает!
UserTask — это объект нотации bpmn, когда бизнес процесс перестает исполняться и встревает на UserTask в ожидании, когда таску продолжат. То есть, мы пойдем исполнять по очереди java код для каждой сервис таски, пока не наткнемся на первую UserTask-у. Чтобы «разморозить» исполнение бизнес-процесса, нам понадобиться специально найти эту таски и сказать «а-ну продолжайся!».
Короткий экскурс в bpmn окончен. Погнали к делу:
Как только наш заказчик подтвердил свою корзину, мы начнем бизнес-процесс и присвоим ему статус НАШЕЙ КОРЗИНЫ (заказа). Получается, зная, в каком мы инстансе бизнес-процесса, мы знаем и номер заказа.
Остается только запустить процесс из экрана, когда заказчик нажимает кнопку подтверждения (я тут нарочно избегаю слово клиент во избежание путаницы со словом SomeHttpClient).
public ProcessInstance startOrderProcess(String orderId) { AppUser appUser = (AppUser) currentAuthentication.getUser(); Map<String, Object> processVariables = Map.of( PROCESS_USER_KEY, appUser.getUsername() ); return runtimeService.startProcessInstanceByKey(ORDER_PROCESS_SCHEMA_ID, orderId, processVariables); }
И сразу пройдем к первому шагу. Присваиваем ему статус нового и сохраняем в транзакционный контекст.
@Override protected void doTransactionalStep(DelegateExecution execution, OrderEntity order, SaveContext saveContext) { order.setStatus(DraftOrderStatus.NEW_ORDER); saveContext.saving(order); doSomeWork(); }Асинхронные ServiceTask и bpmn паттерн Request-Wait-ResponseAfter
Далее все наши ServiceTask будут исполняться в отдельных потоках, будут помечены асинхронным флагом и принадлежать к разным транзакциям, тем самым мы даем гарантии, что наш код будет всегда максимально изолирован и устойчив к ошибкам. Да, да, бизнес процесс можно еще и откатывать)
Не буду освещать все шаги, так как они однотипны, лишь рассмотрим один общий паттерн, по которому проходит весь бизнес процесс:
RequestRestaurantCookStep
@Service public class RequestRestaurantCookStep extends AbstractTransactionalStep { private final Logger log = LoggerFactory.getLogger(RequestRestaurantCookStep.class); private final RestaurantClient restaurantClient; private final OrderService orderService; @Override protected void doTransactionalStep(DelegateExecution execution, OrderEntity order, SaveContext saveContext) { String username = getVariable(execution, PROCESS_USER_KEY); String subjectToken = exchangeOidcTokenForUser(username); String result = systemAuthenticator.withUser(username, () -> restaurantClient.publishRestaurantCookRequest(order.getRestaurantId(), orderService.convert(order), subjectToken)); log.info("Result from restaurant system for cook request: " + result); order.setStatus(DraftOrderStatus.WAIT_FOR_RESTAURANT); saveContext.saving(order); doSomeWork(); } }
RestaurantClient#publishRestaurantCookRequest
public String publishRestaurantCookRequest(Long restaurantId, OrderDTO orderDTO, String subjectToken) { String url = MessageFormat.format("{0}/api/v1/restaurants/{1,number,#}/cook", restaurantUrl, restaurantId); return getApi(url, HttpMethod.POST, new ParameterizedTypeReference<String>() {}, orderDTO, subjectToken); }
RestaurantController#getRestaurantCookRequest
@PostMapping("/restaurants/{restaurantId}/cook") public String getRestaurantCookRequest(@PathVariable Long restaurantId, @RequestBody OrderDTO orderDTO) { cookOrderService.submitNewCookOrderFromDTO(orderDTO); // we will not bring case that restaurant will not cook, placeholder response return "Accepted"; }
CookOrderService#submitNewCookOrderFromDTO
public void submitNewCookOrderFromDTO(OrderDTO orderDTO) { var cookOrderRequest = create(CookOrderRequest.class); cookOrderRequest.setOrderId(orderDTO.getOriginOrderId()); cookOrderRequest.setIsDone(false); cookOrderRequest.setRestaurant(restaurantRepository.getById(orderDTO.getRestaurantId())); cookOrderRequest.setCookingItems(createCookingListFromDTO(cookOrderRequest, orderDTO)); save(cookOrderRequest); }
OrderProcessController#continueOrderRestaurantStep
@PostMapping("/orders/{orderId}/restaurantstep/{restaurantId}") public void continueOrderRestaurantStep(@PathVariable String orderId, @PathVariable String restaurantId) { orderProcessManager.continueProcessByRestaurantStep(orderId, restaurantId); }
OrderProcessManager#continueProcessByRestaurantStep
@PostMapping("/orders/{orderId}/restaurantstep/{restaurantId}") public void continueOrderRestaurantStep(@PathVariable String orderId, @PathVariable String restaurantId) { orderProcessManager.continueProcessByRestaurantStep(orderId, restaurantId); }
Усложнять статью не буду, потому опущу код, связанный с точно такими же шагами с курьером и сервис-тасками, которые просто меняют статус заказа.
Подведем промежуточные итоги для текущего демо проекта:
Но все же! Мы не использовали iframe и проект получился не очень связным в UI и все связи находятся только на уровне коммуникаций. Предлагаю вкратце рассмотреть пример, как можно написать SCS при помощи iframe.
Данный вариант построения самодостаточных систем является более каноничным, т.к. на сайте в первых поинтах сразу упоминают iframe. Хоть я и не считаю, что такой подход имеет большой потенциал (достаточно трудно найти подходящий бизнес-сценарий), мы обязаны его рассмотреть.
Перейдем на Amazon.com, возьмем как пример веб-магазин, у которого существует множество поддоменов(подсистем):
Что происходит когда пользователь заходит на такой сайт?
Пример взят из видео-ряда англоязычного контент-мейкера с ютуба.
Надеюсь, я развлек вас сколько-то интересным контентом. А в ваших руках появился еще один инструмент решений проблем в проектировании ПО.
Робота з доставкою (КЦ та Дарккітчен) Рекомендації по вивантаженню номенклатури для зовні...