О клиенте и сервере в микросервисной архитектуре

от автора

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

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

В качестве прикладного дополнения к рассуждениям мы разработаем несколько Proof of Concept, сопроводим их разработку краткими пояснениями и приложим исходный код PoC.

Для нас «родным» стеком является Java 8 и Spring Boot 2, так что приложенные демки будут написаны на основе этих технологий с изрядными вкраплениями Spring Cloud. Мы также постараемся предложить несколько обособленных типовых решений для задач, которые, как нам показалось, могут в перспективе возникнуть перед разработчиками.

О нас

Мы — подразделение группы компаний «Миландр», занимающееся разработкой и поддержкой IoT-платформы «ИНФОСФЕРА». Этот продукт включает в себя комплекс решений для ЖКХ, умного дома, электросетевой энергетики и промышленных предприятий. Мы собираем данные с различных приборов (счетчиков, датчиков, камер, домофонов, умных устройств), позволяем клиентам оперировать этими данными (в том числе, с использованием алгоритмов ML), настраивать пользовательские сценарии автоматизации для обработки данных, а также осуществлять удаленное управление приборами.

Интерфейс "ИНФОСФЕРА Диспетчерская"
Интерфейс «ИНФОСФЕРА Диспетчерская»

Для кого эта статья

  1. Для новичков, желающих ознакомиться с основными компонентами Spring Cloud и принципами, лежащими в основе микросервисной архитектуры.

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

  3. Для разработчиков и архитекторов, желающих пополнить коллекцию типовых решений и почитать про грабли, на которые можно наступить при первом использовании Spring Cloud.

Про микросервисы

Микросервисам и Spring Cloud на Хабре уже посвящено множество статей, которыми, отчасти, мы вдохновлялись и которые хотели бы дополнить в тех моментах, где чувствовали недостаточное раскрытие темы.

Концепция микросервисной архитектуры достаточно молода, но мы уже успели за 5-6 лет посмотреть, как она прошла все этапы Gartner Hype Cycle.

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

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

Про Spring Cloud

Spring Cloud — набор инструментов, позволяющих организовать работу Spring-based приложений в соответствии с принципами микросервисной архитектуры.

Статьи, описывающие использование Spring Cloud

  • Отличная статья от @sqshq с уклоном в практические аспекты создания микросервисного приложения, с подробными описаниями действий и отличными примерами.

  • Простейшее демо, показывающее, как запустить Eureka Server и подключить к нему клиенты.

Небольшое уточнение для первой работы со Spring Cloud

Обращаем внимание на не совсем привычный механизм указания зависимостей от Spring Cloud. После создания проекта с зависимостями из Spring Cloud через Spring Initializr в глаза сразу бросается, что starters из Spring Cloud не наследуются из родительского pom-файла, как это происходит со стартерами Spring Boot, а приносятся через dependency management:

<properties>  	<spring-cloud.version>2020.0.3</spring-cloud.version>  </properties> ... <dependencyManagement>  	<dependencies> 		<dependency> 			<groupId>org.springframework.cloud</groupId>  			<artifactId>spring-cloud-dependencies</artifactId>  			<version>${spring-cloud.version}</version>  			<type>pom</type>  			<scope>import</scope>  		</dependency> 	</dependencies> </dependencyManagement>

1. Так что же за вопросы?

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

Вопрос

Варианты решения

Выбор принципа рендеринга веб-страниц

Client-side rendering, Server-side rendering, Mixed

Хранение и выдача интерфейса на клиент

Единая точка, микрофронтенды, несколько точек-страниц

Протоколы взаимодействия клиента и сервера, а также сервисов друг с другом

Брокер, HTTP, WS, смесь

Необходимость Service Discovery

Зависит от использования HTTP

Мы намеренно оставляем за пределами подробного рассмотрения (ограничившись упоминаниями, по крайней мере, в рамках данной статьи) следующие вопросы:

  • Использование Spring Security для обеспечения безопасности использования приложения,

  • Распределенные транзакции для обеспечения согласованности баз данных разных микросервисов,

  • Балансировка нагрузки для адаптируемости отдельных компонентов к возрастанию нагрузки,

  • Боевое развертывание, пока говорим преимущественно о процессе разработки. Некоторые недостатки предлагаемых подходов могут быть нивелированы за счет грамотного проектирования и администрирования.

1.1 Рендеринг страниц

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

Под этими словами подразумевается, где будет формироваться HTML-документ для дальнейшего его использования браузером. На текущем этапе развития технологий в основном предлагается использовать клиентский рендеринг (Client-side rendering, CSR) и серверный рендеринг (Server-side rendering, SSR).

Рассмотрим коротко суть каждого из подходов.

Серверный рендеринг

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

Преимущества

  • С точки зрения пользователя это позволяет быстро получить отрисованную страницу (и даже содержательную отрисовку, если сервер перед выдачей страницы сразу подставляет в нее данные).

Недостатки

  • После отрисовки страница может еще некоторое время находиться в неоперабельном состоянии, до подготовки JS-кода к исполнению;

  • Без ухищрений в клиентском коде может потребоваться полное обновление страницы для обновления данных.

Технологии:

  • JSP;

  • Thymeleaf;

  • Mustache;

  • + ваши любимые JS-библиотеки;

Клиентский рендеринг

Данный подход, напротив, предполагает формирование страницы на клиенте посредством исполнения JS-кода. Например, так поступает React при работе через JSX, формируя содержимое страницы из кусков разметки, возвращаемой из компонентов.

Преимущества

  • Почти одновременно происходит отрисовка страницы и наступает готовность к взаимодействию.

Недостатки

  • В скорости отрисовки проигрывает серверному рендерингу, поскольку JS необходимо загрузить, а затем исполнить на клиенте. Страница будет отрисована и готова к работе только по окончании работы всего необходимого для отрисовки JS-кода.

Ссылки

Хорошая обзорная статья (картинки взяты из нее же)

1.2 Протоколы взаимодействия клиента и сервера, а также сервисов друг с другом

Для микросервисной архитектуры в качестве механизма обмена данными между сервисами зачастую рекомендуют использовать событийно-ориентированную архитектуру

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

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

Здесь мы вплотную подошли к вопросу: как следует организовать взаимодействие сервисов друг с другом? Как определить, следует ли в конкретной ситуации использовать событийный подход или следует использовать типичные для HTTP запрос-ответ? Порассуждаем.

Событийный подход хорошо показывает себя, когда:

  • стороне, публикующей событие, не требуется ответ (реакция на это событие);

  • есть несколько сервисов, заинтересованных в получении события;

  • существует потребность в сохранении события в топике (для случая использования Kafka или другого персистентного брокера сообщений) для возможности потом его прочитать из топика.

С другой стороны, HTTP лучше подходит в ситуациях:

  • когда требуется получить ответ на запрос;

  • когда требуется выполнить синхронную операцию;

  • когда не нужно привносить дополнительную сложность в систему, ограничившись синхронным обращением одного сервиса к другому.

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

Случай “внешнего” клиента

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

  1. Хардкод адресов в клиентском коде (негибко и очень сложно поддерживать в боевом окружении, отметаем);

  2. Параметризация клиентского кода (негибко, отметаем);

  3. Использование паттерна Service Discovery для клиента (подробнее о Service Discovery можно прочесть ниже) и отправка HTTP запросов напрямую к сервисам;

  4. Использование паттерна API gateway для проксирования всех запросов к backend-сервисам через единую точку входа. Подробнее об API gateway можно прочесть ниже;

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

Из пяти вариантов для рассмотрения остаются два:

  1. Service Discovery;

  2. API Gateway.

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

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

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

Другая сильная сторона API Gateway заключается в возможности выступать шлюзом для внешних запросов даже в том случае, когда внутри сервисной сети используется обмен данными только через брокер. Нам не удалось найти готовых реализаций инструментов, которые могли бы производить «перекладывание» запроса к backend в брокер, но подобная логика может быть без труда реализована и посредством обычных Web-контроллеров.

Существует еще один вариант архитектуры — введение дополнительного слоя сервисов 1-го уровня, способных принимать запросы от API Gateway по HTTP и перекладывать их в брокер. Такой подход позволяет и не вводить HTTP API для всех сервисов и сохранить “чистоту” API Gateway, не привнося туда логику работы с брокером.

Еще одно преимущество API Gateway над подходом Service Discovery может быть получено при использовании WebSocket для полнодуплексного обмена данными между клиентом и сервером. Например, может возникнуть задача уведомлять клиентское приложение о происходящих в системе событиях. Источниками таких событий могут выступать несколько backend-сервисов. Вместо поддержания нескольких WS-соединений с такими сервисами клиент может поддерживать одно соединение с одной точкой, предоставляющей возможность пересылки таких уведомлений через WS. API Gateway — неплохой кандидат для решения этой задачи.

Исходя из приведенных рассуждений, мы сформулировали для себя следующие выводы:

  1. Смешение событий и HTTP-запросов — допустимая практика;

  2. Запрос от клиента к API Gateway следует направлять по HTTP;

  3. Во внутренней сети для поиска адресов сервисов для последующего выполнения HTTP-запросов следует использовать Service Discovery. Он же может служить заделом для масштабирования;

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

Spring Cloud Gateway

Ссылки

  1. Концепт API Gateway

  2. Официальный гайд по Spring Cloud Gateway

  3. Гайд от Baeldung по Spring Cloud Gateway

Документация по java-конфигурации Spring Cloud Gateway:

  1. PredicateSpec

  2. UriSpec

Spring Cloud Gateway принимает входящие внешние запросы, проверяет свои правила маршрутизации и, в случае нахождения подходящего для пришедшего запроса правила, перенаправляет запрос к одному из сервисов.

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

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

По сути, для начала работы со Spring Cloud Gateway нужно сделать лишь две вещи:

  1. Добавить зависимость в ClassPath;

  2. А Определить бин типа org.springframework.cloud.gateway.route.RouteLocator и добавить его конфигурацию;
    ИЛИ
    Б Прописать конфигурацию в файле application.properties/yaml
    Пример

На этапе разработки мы решили остановиться на java-конфигурации. Это позволило опробовать более тонкие варианты конфигурации API-gateway. Однако, вместе с тем, в промышленной эксплуатации этот вариант привносит и ограничения, не позволяя изменять конфигурацию «на лету» (например, с использованием Spring Cloud Config), вынуждая менять код приложения, исполняемого в боевом окружении, и, как следствие, требуя пересборки приложения при необходимости внести изменения в конфигурацию шлюза.

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

Пример 1: Меняем request path при помощи регулярного выражения. Запрос /google/hello приведет к обращению по адресу с параметром http://google.com/search?q=hello.

@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {   return builder   	.routes()  		.route(r -> r  			.path("/google/**")  			.filters(gatewayFilterSpec -> gatewayFilterSpec.rewritePath("/google/(?<appendix>.*)", "/search?q=${appendix}"))  			.uri("http://google.com")) 	.build(); }

Пример 2: Отрезаем от path 1 блок. Запрос /yandex/hello/123 будет перенаправлен на http://yandex.ru/hello/123

@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {   return builder   	.routes() 		.route(r -> r       .path("/yandex/**")       .filters(gatewayFilterSpec -> gatewayFilterSpec.stripPrefix(1))       .uri("[http://yandex.ru](http://yandex.ru)")) 	.build(); }

Пример 3: Формируем URI для перенаправления, вычленяя адрес сервиса из path запроса.

@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {   return builder   	.routes() 	  .route(r -> r       .path("/service/**")       .filters(gatewayFilterSpec -> gatewayFilterSpec.changeRequestUri(serverWebExchange -> {           ServerHttpRequest originalRequest = serverWebExchange.getRequest();           URI oldUri = serverWebExchange.getRequest().getURI();           UriComponentsBuilder newUri = UriComponentsBuilder.fromUri(oldUri)           .host(originalRequest.getPath().subPath(3, 4).toString() + ".com") // 0,1,2,3 - /service/<serviceName>,            .port(null)           .replacePath(originalRequest.getPath().subPath(4).toString());           return Optional.of(newUri.build().toUri());         }))       .uri("http://ignored-URI")) // Этот URI игнорируется 	.build(); }

Пример 4: Прописываем route с минимальным приоритетом. Запросы по адресам, не предусмотренным в других route, будут пересылаться по адресу localhost:8090.

@Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {   return builder   	.routes() 	  .route(r -> r 	  .order(Integer.MAX_VALUE) 	  .path("/**") 	  .uri("[http://localhost:8090](http://localhost:8090)")) 	.build(); }

1.3 Хранение интерфейса и его выдача на клиент

В рамках решения задачи передачи интерфейса для исполнения на клиент также существует несколько вариантов действия.

Микрофронтенды

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

Оригинальное изображение - https://techrocks.ru/2018/09/19/prepare-your-skill-set-for-web-developer-interviews-2/microfrontends/
Оригинальное изображение — https://techrocks.ru/2018/09/19/prepare-your-skill-set-for-web-developer-interviews-2/microfrontends/

Объединение компонентов может осуществляться как на клиенте, так и на сервере.

Преимущества

  • Гибко — позволяет развивать UI разных компонентов независимо друг от друга.

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

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

Недостатки

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

  • Накладные расходы на формирование интерфейса из кусочков. Как трудовые, так и вычислительные.

  • Подход молодой, и его нужно глубоко и аккуратно изучать.

Технологии

Единая точка хранения интерфейса

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

Вариант 1 — Выделенный UI-сервис

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

Преимущества

  • Логическая обособленность интерфейсного кода и страниц от остального кода приложения.

Недостатки

  • Необходимость создания и администрирования отдельного сервиса.

Технологии

  • Spring Web + JSP/Thymeleaf как простейший пример для минимальной реализации.

Вариант 2 — UI Gateway

Выше мы уже упоминали паттерн API Gateway. Существует практика расширения этого шлюза до UI Gateway — шлюза с функционалом хранения и раздачи страниц и клиентского кода.

Преимущества

  • Меньшее количество сервисов, чуть большая простота администрирования, не нужно прописывать дополнительные роутинги на UI.

Недостатки

  • Как мы помним, API Gateway — единая точка отказа системы. Даже при наличии его реплицированных экземпляров в эксплуатации обновление такого шлюза для обновления UI выглядит достаточно рискованной операцией.

Технологии

Spring Web + JSP/Thymeleaf как простейший пример для минимальной реализации.

Вариант 3 — Page UI-сервис

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

Преимущества

ссылка на оригинал статьи https://habr.com/ru/company/milandr/blog/563954/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *