Здравствуйте, хабровчане! Сегодня предлагаем вам очередной интересный пост на неисчерпаемую тему микросервисов, на этот раз — для корифеев и неофитов языка Java. Читаем и голосуем!

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

ВВЕДЕНИЕ

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

  • Улучшается масштабирование – различные части приложения масштабируются независимо друг от друга
  • Эффективное устранение сильной связанности между различными частями системы – это всегда желательно, но лучше всего достигается именно при помощи микросервисов
  • Повышается надежность системы – при отказе одного сервиса остальные сохраняют работоспособность.
  • Свобода при выборе технологий – каждый сервис можно реализовывать при помощи той технологии, которая лучше всего подходит для данного случая.
  • Улучшенная многоразовость компонентов – сервисы (даже те, что уже развернуты) можно совместно использовать в разных проектах
  • Существует и множество других достоинств, зависящих от конкретной архитектуры или решаемой проблемы

Естественно, многие такие преимущества позволяют не только выстроить более качественную систему, но и облегчить жизнь разработчика, сделать его труд более благодарным. Разумеется, можно сколько угодно спорить о них, поэтому давайте просто сойдемся на том, что микросервисы полезны (что подтверждается на опыте таких крупных компаний, как Netflix и Nginx). Как и для любой другой архитектуры, для микросервисов характерны свои недостатки и сложности, которые требуется преодолевать. Наиболее важные таковы:

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

ПРОБЛЕМА

Итак, вот мы и подошли к вопросу, с которым сталкиваются большинство команд, приступающих к работе с микросервисами. Учитывая, какова цель работы с микросервисами и рекомендумые приемы их реализации, сталкиваемся с проблемой: «Нам нужны слабо связанные сервисы, между которыми почти не будет общего кода и зависимостей. Таким образом, всякий раз, когда мы потребляем некоторый сервис, нужно писать классы, которые будут обрабатывать отклик. А как же принцип «DRY» (Не повторяться)? Что делать?». В таком случае легко удариться в два антипаттерна:

  • Давайте сделаем так, чтобы сервисы зависели друг от друга! Что ж, это означает, что о слабом связывании можно забыть (здесь нам его точно не добиться), и что свобода в выборе технологии также будет утрачена: логика будет рассыпана по всему коду, и предметная область чрезмерно усложнится.
  • Давайте просто копипастить код! Это не так плохо, поскольку, как минимум, позволяет сохранить слабое связывание и не допускает перенасыщения домена логикой. Клиент не может зависеть от кода сервиса. Однако, будем честны; никто не хочет повсюду копипастить одни и те же классы и писать массу трафаретного кода всякий раз, когда планируется потреблять этот гнусный пользовательский сервис. Принцип «Суши код» превратился в мантру не просто так!

РЕШЕНИЕ

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

Этот подход обладает следующими достоинствами:

  • Сервис полностью отделяется от клиента, а конкретные сервисы не зависят друг от друга – библиотека автономна и клиенто-специфична. Она может быть даже заточена под конкретную технологию, если мы работаем сразу с несколькими технологиями.
  • Релиз новой версии клиента никак не зависит от клиента; при наличии обратной совместимости клиенты могут даже «не заметить» релиза, поскольку именно клиент обеспечивает поддержку библиотеки
  • Теперь клиенты СУХИЕ – никакой избыточный код не копипастится
  • Интеграция с сервисом ускоряется, но при этом мы не теряем никаких преимуществ микросервисной архитектуры.

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

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

ПРИМЕР СО SPRING BOOT



Итак, я объяснил решение, а теперь продемонстрирую его в коде. Кстати, вот и возможность лишний раз пропиарить мою любимую микросервисную библиотеку — Spring Boot. Весь пример можно скачать из репозитория на Github, созданного специально для этой статьи.

Spring Boot позволяет разрабатывать микросервисы с места в карьер – да, я не преувеличиваю. Если Dropwizard показался вам быстрым, то вы весьма удивитесь, насколько удобнее работать со Spring Boot. В этом примере мы разрабатываем очень простой сервис User, который будет возвращать смоделированный объект User JSON. В дальнейшем этот сервис будет использоваться службой уведомления и табличной службой, фактически, выстраивая различные представления данных; однако, в обоих случаях сервису требуется понимать объект User.

СЕРВИС USER

В UserServiceApplication будет находиться основной метод. Поскольку это Spring Boot, при запуске он также включает встроенный сервер Tomcat:

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class UserServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(UserServiceApplication.class, args);
	}
}

В самом деле, проще и быть не может! Spring Boot – очень категоричный фреймворк, поэтому, если умолчания нас устраивают, то набирать вручную почти ничего не приходится. Однако, одну штуку поменять все-таки придется: речь о заданном по умолчанию номере порта. Посмотрим, как это делается в файле application.properties:

server.port = 9001

Просто и красиво. Если вам доводилось писать REST-сервис на Java, то вы, вероятно, знаете, что для этого нужен Controller. Если делаете это впервые – не волнуйтесь, писать контроллеры в Spring Boot совсем просто:

package com.example;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @RequestMapping("/user")
    public User getUser(@RequestParam(value="id", defaultValue="1") int id) {
        return new User(id);
    }

}

Так мы просто позволим пользователю выполнять запросы к конечной точке /user?id=, где id может соответствовать любому пользователю, который нас интересует. Учитывая, насколько просты эти классы – в самом деле, вся логика должна лежать в конкретном классе User. Этот класс сгенерирует заготовочные данные и будет сериализован при помощи Jackson (библиотека JSON для Java):

package com.example;

import java.util.ArrayList;
import java.util.List;

public class User {

    private final long id;
    private final String forename;
    private final String surname;
    private final String organisation;
    private final List<String> notifications;
    private final long points;
    // Друзья признаны нежелательными и использоваться не будут
    private final List<String> friends;

    public User(int id) {
        String[] forenames = {"Alice", "Manjula", "Bartosz", "Mack"};
        String[] surnames = {"Smith", "Salvatore", "Jedrzejewski", "Scott"};
        String[] organisations = {"ScottLogic", "UNICEF"};

        forename = forenames[id%3];
        surname = surnames[id%4];
        organisation = organisations[id%2];
        notifications= new ArrayList<>();
        notifications.add("You have been promoted!");
        notifications.add("Sorry, disregard the previous notifaction- wrong user");
        points = id * 31 % 1000;

        // У вас нет друзей
        friends = new ArrayList<>();

        this.id = id;
    }

    // Геттеры и сеттеры на все случаи…
}

Вот и весь сервис, необходимый для создания User JSON. Поскольку это первый рассматриваемый нами сервис Spring Boot, не помешает заглянуть и в файл .pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.example</groupId>
	<artifactId>user-service</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>user-service</name>
	<description>Demo user-service with Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.3.5.RELEASE</version>
		<relativePath/> <!-- ищем родительский узел в репозитории -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
			<version>2.5.0</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

При вызове сервиса, id которого равен 10, видим такой вывод JSON:



КЛИЕНТСКАЯ БИБЛИОТЕКА

Допустим, у нас есть два сервиса, использующих этот API – сервис уведомления и личный кабинет. В реалистичном примере объект User мог бы оказаться гораздо сложнее, и клиентов у нас могло быть не два, а больше. Клиентская библиотека – простой проект под названием user-client-libs, состоит из единственного класса:

@JsonIgnoreProperties(ignoreUnknown = true)
public class UserView {

    private long id;
    private String forename;
    private String surname;
    private String organisation;
    private List<String> notifications;
    private long points;

    public UserView(){

    }

    public long getId() {
        return id;
    }

    public String getForename() {
        return forename;
    }

    public String getSurname() {
        return surname;
    }

    public String getOrganisation() {
        return organisation;
    }

    public List<String> getNotifications() {
        return notifications;
    }

    public long getPoints() {
        return points;
    }
}

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

КЛИЕНТЫ

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

Вот контроллер сервиса личного кабинета:

import com.example.user.dto.UserView;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class UserDashboardController {

    @RequestMapping("/dashboard")
    public String getUser(@RequestParam(value="id", defaultValue="1") int id) {
        RestTemplate restTemplate = new RestTemplate();
        UserView user = restTemplate.getForObject("http://localhost:9001/user?id="+id, UserView.class);
        return "USER DASHBOARD <br>" +
                "Welcome " + user.getForename() +" "+user.getSurname()+"<br>"+
                "You have " +user.getPoints() + " points! Good job!<br>"+
                "<br>"+
                "<br>"+user.getOrganisation();
    }

}

А это контроллер сервиса личных уведомлений:

import com.example.user.dto.UserView;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class UserNotificationController {

    @RequestMapping("/notification")
    public String getUser(@RequestParam(value="id", defaultValue="1") int id) {
        RestTemplate restTemplate = new RestTemplate();
        UserView user = restTemplate.getForObject("http://localhost:9001/user?id="+id, UserView.class);
        String response = "NOTIFICATIONS";
        int number = 1;
        for(String notification : user.getNotifications()){
            response += "
 Notification number "+(number++)+": "+notification;
        }
        return response;
    }

}

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

<dependency>
	<groupId>com.example</groupId>
    <artifactId>user-client-libs</artifactId>
	<version>0.0.1-SNAPSHOT</version>
</dependency>

Все, что осталось сделать в этом примере – запустить все три сервиса на портах 9001, 9002 и 9003 посмотреть вывод:

Личный кабинет:



Уведомления:



ЗАКЛЮЧЕНИЕ

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

В примере со Spring Boot со всей очевидностью продемонстрировано, насколько удобен такой подход; кроме того, оказывается, что микросервисы гораздо проще, чем могут показаться. Если хотите подробнее изучить этот проект – смотрите у меня на Github и попробуйте его развить.

Удачи с разработкой микросервисов!

P.S. — от авторов перевода:

> Вот книга о Spring Boot.
> Вот книга о микросервисах в Spring

Хотите какую-нибудь?
Spring и микросервисы

Проголосовало 29 человек. Воздержалось 17 человек.

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

Поделиться с друзьями
-->

Комментарии (6)


  1. gkislin
    17.02.2017 19:55
    +1

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


    • Логирование: slf4j-api<-logback-classic
    • Валидация: javax.el-api<-org.glassfish.web.javax.el
    • JPA: hibernate-jpa-2.1-api<-hibernate-core


    1. sshikov
      18.02.2017 12:13

      Почему же непонятно где? Обманули почти везде. Вот смотрите:

      * Улучшается масштабирование – различные части приложения масштабируются независимо друг от друга

      Тема не раскрыта. Попробуйте масштабировать такой сервис в рамках предложенной архитектуры и расскажите, что получилось. Скорее всего — не получится.

      * Эффективное устранение сильной связанности между различными частями системы – это всегда желательно, но лучше всего достигается именно при помощи микросервисов

      Ага. Ценой сильного снижения производительности. JSON же везде и REST. Вообще утверждение про «лучше всего» ни на чем не основано, и скорее всего — неправда. И попросту недоказано.

      * Повышается надежность системы – при отказе одного сервиса остальные сохраняют работоспособность.

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

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

      Как правило это нафиг никому не нужная свобода.

      * Улучшенная многоразовость компонентов – сервисы (даже те, что уже развернуты) можно совместно использовать в разных проектах

      Это не было продемонстрировано.

      Ну и в целом — берем любую книгу о том, как писать EJB, примерно 5-10 летней давности (желательно чтобы это были EJB 3, и там уже было JAX RS). И имеем примерно такой же по объему код, ровно с теми же свойствами. Разворачиваем внутри tomcat, и имеем «множество других достоинств», которые тут не созволили продемонстрировать и даже назвать. Например, централизованный мониторинг.

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


      1. Throwable
        18.02.2017 15:17
        +2

        У меня сложилось такое впечатление, что сейчас под трендом "микросервисы" пытаются втюхать Spring Boot, Rest и иногда каким-то образом припаять Docker. И если посмотреть в Google Trends, то собственно так оно и есть. То есть чтобы в мозгу разработчика четко отложилось: java-приложения, оказывается, можно разрабатывать без контейнера, но для этого нужен Spring Boot, который нам даст Rest. И это теперь называется микросервисами.


        Тем не менее, 10 лет назад в эпоху засилия SOA и JavaEE, когдя я писал что-то вроде:


        @WebService
        public class AddService {
            @WebMethod
            public int add(int a, int b) {
                return a+b;
            }
            public static void main(String[] args ){
                Endpoint.publish("http://0.0.0.0:1234/AddService", new AddService());
            }
        }

        не было подходящего названия для этого. Люди недоумевали: если это java-приложение, то где интерфейс с кнопочками? Если это веб-приложение, то где сервер приложений, куда оно должно деплоиться? Здесь даже не было более-менее знакомого слова "Spring". И вплоть до того, что клиенты отказывались принимать работу, поскольку не знали каким словом это назвать (мой клиент все-таки придумал название — "тамагочи"))).


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


        1. sshikov
          18.02.2017 15:26
          +1

          Ну я об этом и толкую. Это все можно уже лет 10 как в таком же ровно виде делать.

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


  1. Twindo
    17.02.2017 22:06

    Может быть я не совсем по теме, но все же: почему бы вам не сделать, что-то типа на подобии краудфандинговой площадки в области перевода книг?


  1. ZoRDoK
    20.02.2017 09:21
    +1

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

    Мне кажется, кроме перечисленных решений (копипаста и чуть большая связанность), есть ещё два: выделить библиотеку в отдельный микросервис со своим API, и, ИМХО, лучший вариант, отказаться от разделения тех микросервисов, которые делят общий код — скорее всего их разделение искусственно.