Вчимося розгортати мікросервіси. Частина 1. Spring Boot та Docker

Вчимося розгортати мікросервіси. Частина 1. Spring Boot та Docker

Привіт Хабр.

У цій статті я хочу розповісти про свій досвід створення навчального середовища для експериментів із мікросервісами. При вивченні кожного нового інструменту мені завжди хотілося спробувати його не тільки на локальній машині, але і в більш реалістичних умовах. Тому я вирішив створити спрощену мікросервісну програму, яку згодом можна буде "обвішувати" всякими цікавими технологіями. Основна вимога до проекту – його максимальна функціональна наближеність до реальної системи.

Спочатку я розбив створення проекту на кілька кроків:

  1. Створити два сервіси — 'бекенд' (backend) та 'шлюз' (gateway), запакувати їх у docker-образи та налаштувати їхню спільну роботу

    Ключові слова: Java 11, Spring Boot, Docker, image optimization

  2. Розробка Kubernetes конфігурації та деплой системи в Google Kubernetes Engine

    Ключові слова: Kubernetes, GKE, resource management, autoscaling, secrets

  3. Створення чарту за допомогою Helm 3 для більш ефективного керування кластером

    Ключові слова: Helm 3, chart deployment

  4. Налаштування Jenkins та пайплайну для автоматичної доставки коду в кластер

    Ключові слова: Jenkins configuration, plugins, separate configs repository

Кожному кроці я планую присвятити окрему статтю.

Спрямованість цього циклу статей у тому, як написати мікросервіси, бо як змусити їх працювати у єдиній системі. Хоча всі ці речі зазвичай лежать за межами відповідальності розробника, думаю, що однаково корисно бути знайомим із ними хоча б на 20% (які, як відомо, дають 80% результату). Деякі безумовно важливі теми, такі як забезпечення безпеки, будуть залишені за дужками цього проекту, оскільки автор мало розуміє система створюється виключно для особистого користування. Я буду радий будь-яким думкам та конструктивній критиці.

Створення мікросервісів

Сервіси були написані Java 11 з використанням Spring Boot. Міжсервісна взаємодія організована з використанням REST. Проект буде включати мінімальну кількість тестів (щоб потім було, що тестувати в Jenkins). Вихідний код сервісів доступний на GitHub: бекенд и шлюз.

Щоб мати можливість перевірити стан кожного з сервісів, в їх залежності був доданий Spring Actuator. Він створить ендпойнт /actuator/health і повертатиме 200 статус, якщо сервіс готовий приймати трафік, або 504 у разі проблем. В даному випадку це досить фіктивна перевірка, тому що сервіси дуже прості, і при якомусь форсмажорі вони, швидше, стануть повністю недоступні, ніж збережуть часткову працездатність. Але в реальних системах Actuator може допомогти діагностувати проблему до того, як її почнуть битися користувачі. Наприклад, у разі виникнення проблем із доступом до БД, ми зможемо автоматично на це зреагувати, припинивши обробляти запити зламаним екземпляром сервісу.

Сервіс Backend

Сервіс бекенду буде просто рахувати та віддавати кількість прийнятих запитів.

Код контролера:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

    @GetMapping("/requests")
    public Long getRequestsCount() {
        return counter.incrementAndGet();
    }
}

Тест на контролер:

@WebMvcTest(RequestsCounterController.class)
public class RequestsCounterControllerTests {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void firstRequest_one() throws Exception {
        mockMvc.perform(get("/requests"))
            .andExpect(status().isOk())
            .andExpect(MockMvcResultMatchers.content().string("1"));
    }
}

сервіс Gateway

Шлюз переадресовуватиме запит сервісу бекенду, доповнюючи його наступною інформацією:

  • ID шлюзу. Він потрібний, щоб можна було за відповіддю сервера відрізнити один екземпляр шлюзу від іншого
  • Якийсь "секрет", який відіграватиме роль дуже важливого пароля (№ ключа шифрування важливої ​​куки)

Конфігурація в application.properties:

backend.url=http://localhost:8081
instance.id=${random.int}
secret="default-secret"

Адаптер для зв'язку з бекендом:

@Service
public class BackendAdapter {

    private static final String REQUESTS_ENDPOINT = "/requests";

    private final RestTemplate restTemplate;

    @Value("${backend.url}")
    private String backendUrl;

    public BackendAdapter(RestTemplateBuilder builder) {
        restTemplate = builder.build();
    }

    public String getRequests() {
        ResponseEntity<String> response = restTemplate.getForEntity(
backendUrl + REQUESTS_ENDPOINT, String.class);
        return response.getBody();
    }
}

Контролер:

@RestController
@RequiredArgsConstructor
public class EndpointController {

    private final BackendAdapter backendAdapter;

    @Value("${instance.id}")
    private int instanceId;

    @Value("${secret}")
    private String secret;

    @GetMapping("/")
    public String getRequestsCount() {
        return String.format("Number of requests %s (gateway %d, secret %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

запуск:

Запускаємо бекенд:

./mvnw package -DskipTests
java -Dserver.port=8081 -jar target/microservices-backend-1.0.0.jar

Запускаємо шлюз:

./mvnw package -DskipTests
java -jar target/microservices-gateway-1.0.0.jar

перевіряємо:

$ curl http://localhost:8080/
Number of requests 1 (gateway 38560358, secret "default-secret")

Все працює. Уважний читач зауважить, що нам нічого не заважає звернутися до бекенду безпосередньо в обхід шлюзу (http://localhost:8081/requests). Щоб це виправити, послуги повинні бути об'єднані в одну мережу, а назовні "стояти" повинен тільки шлюз.
Також обидва сервіси ділять одну файлову систему, плодять потоки і одночасно можуть почати заважати один одному. Було б непогано ізолювати наші мікросервіси. Цього можна досягти за допомогою рознесення додатків по різних машинах (багато грошей, складно), використання віртуальних машин (ресурсоємно, довгий запуск) або за допомогою контейнеризації. Очікувано вибираємо третій варіант і Docker як інструмент для контейнеризації

Docker

Якщо коротко, то докер створює ізольовані контейнери, по одному на програму. Щоб використовувати докер, потрібно написати Dockerfile - інструкцію зі збирання та запуску програми. Далі можна буде зібрати образ, завантажити його до реєстру образів (№ Dockerhub) і в одну команду розгорнути свій мікросервіс у будь-якому докеризованому середовищі.

Докер-файл

Одна з найважливіших характеристик образу - це його розмір. Компактний образ швидше завантажиться з віддаленого репозиторію, займе менше місця, і ваш сервіс швидше стартує. Будь-який образ будується виходячи з базового образу, і рекомендується вибирати найбільш мінімалістичний варіант. Хорошим варіантом є Alpine – повноцінний дистрибутив Linux з мінімумом пакетів.

Для початку спробуємо написати Dockerfile "в лоб" (відразу скажу, що це поганий спосіб, не робіть так):

FROM adoptopenjdk/openjdk11:jdk-11.0.5_10-alpine
ADD . /src
WORKDIR /src
RUN ./mvnw package -DskipTests
EXPOSE 8080
ENTRYPOINT ["java","-jar","target/microservices-gateway-1.0.0.jar"]

Тут ми використовуємо базовий образ на основі Alpine із вже встановленим JDK для складання нашого проекту. Командою ADD ми додаємо в образ поточну директорію src, відзначаємо її робочою (WORKDIR) та запускаємо складання. Команда EXPOSE 8080 сигналізує докеру, що додаток у контейнері використовуватиме його порт 8080 (це не зробить додаток доступним ззовні, але дозволить звернутися до додатка, наприклад, з іншого контейнера в тій же мережі докера).

Щоб упакувати сервіси в образи, потрібно виконати команди з кореня кожного проекту:

docker image build . -t msvc-backend:1.0.0

В результаті отримуємо образ розміром 456 Мбайт (з них базовий образ JDK 340 зайняв Мбайт). І все при тому, що класів у нашому проекті на пальці перерахувати. Щоб зменшити розмір нашого образу:

  • Використовуємо багатокрокове складання. На першому кроці зберемо проект, на другому встановимо JRE, а третім кроком скопіюємо все це новий чистий Alpine образ. У фінальному образі виявляться лише необхідні компоненти.
  • Скористаємося модуляризацією java. Починаючи з Java 9, можна за допомогою інструменту jlink створити JRE лише з потрібних модулів

Для допитливих, ось хороша стаття про підходи зменшення розмірів образу https://habr.com/ru/company/ruvds/blog/485650/.

Підсумковий Dockerfile:

FROM adoptopenjdk/openjdk11:jdk-11.0.5_10-alpine as builder
ADD . /src
WORKDIR /src
RUN ./mvnw package -DskipTests

FROM alpine:3.10.3 as packager
RUN apk --no-cache add openjdk11-jdk openjdk11-jmods
ENV JAVA_MINIMAL="/opt/java-minimal"
RUN /usr/lib/jvm/java-11-openjdk/bin/jlink 
    --verbose 
    --add-modules 
        java.base,java.sql,java.naming,java.desktop,java.management,java.security.jgss,java.instrument 
    --compress 2 --strip-debug --no-header-files --no-man-pages 
    --release-info="add:IMPLEMENTOR=radistao:IMPLEMENTOR_VERSION=radistao_JRE" 
    --output "$JAVA_MINIMAL"

FROM alpine:3.10.3
LABEL maintainer="Anton Shelenkov [email protected]"
ENV JAVA_HOME=/opt/java-minimal
ENV PATH="$PATH:$JAVA_HOME/bin"
COPY --from=packager "$JAVA_HOME" "$JAVA_HOME"
COPY --from=builder /src/target/microservices-backend-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]

Перестворюємо образ, і він у результаті схуд у 6 разів, склавши 77 Мбайт. Не погано. Після цього готові образи можна завантажити в реєстр образів, щоб ваші образи були доступні для скачування з інтернету.

Спільний запуск сервісів у Docker

Для початку наші сервіси мають бути в одній мережі. У докері існує кілька типів мереж, і ми використовуємо найпримітивніший з них - bridge, що дозволяє об'єднувати в мережу контейнери, запущені на одному хості. Створимо мережу наступною командою:

docker network create msvc-network

Далі запустимо контейнер бекенда з ім'ям 'backend' із образом microservices-backend:1.0.0:

docker run -dit --name backend --network msvc-net microservices-backend:1.0.0

Bridge-мережа надає з коробки service discovery для контейнерів за їх іменами. Тобто сервіс бекенда буде доступний у мережі докеру за адресою http://backend:8080.

Запускаємо шлюз:

docker run -dit -p 80:8080 --env secret=my-real-secret --env BACKEND_URL=http://backend:8080/ --name gateway --network msvc-net microservices-gateway:1.0.0

У цій команді ми вказуємо, що ми прокидаємо 80 порт нашого хоста на 8080 порт контейнера. Опції env ми використовуємо для встановлення змінних середовища, які автоматично будуть відніматися спрингом та перевизначать властивості з application.properties.

Після запуску викликаємо http://localhost/ і переконуємось, що все працює, як і в минулому випадку.

Висновок

У результаті ми створили два простенькі мікросервіси, запакували їх у докер-контейнери і спільно запустили на одній машині. У отриманої системи, однак, є низка недоліків:

  • Погана стійкість до відмов - у нас все працює на одному сервері
  • Погана масштабованість - при збільшенні навантаження було б непогано автоматично розгортати додаткові екземпляри сервісів та балансувати навантаження між ними
  • Складність запуску - нам знадобилося ввести як мінімум 3 команди, причому з певними параметрами (це тільки для 2 сервісів)

Для усунення перелічених вище проблем існує ряд рішень, таких як Docker Swarm, Nomad, Kubernetes або OpenShift. Якщо вся система буде написана на Java можна подивитися у бік Spring Cloud (гарна стаття).

В наступної частини я розповім про те, як я налаштовував Kubernetes та деплоїв проект у Google Kubernetes Engine.

Джерело: habr.com

Додати коментар або відгук