Учим се да разгръщаме микросервизи. Част 1. Spring Boot и Docker

Учим се да разгръщаме микросервизи. Част 1. Spring Boot и Docker

Здравей, Хабр.

В тази статия искам да споделя опита си в създаването на учебна среда за експериментиране с микросервизи. При изучаването на всеки нов инструмент винаги желаех да го пробвам не само на локалната машина, но и в по-реалистични условия. Затова реших да създам опростено микросервисно приложение, което впоследствие ще може да ‘обвешвам’ с различни интересни технологии. Основното изискване към проекта е да бъде максимално функционално близко до реалната система.

Първоначално разделих създаването на проекта на няколко стъпки:

  1. Създаване на два сървиса — ‘бекенд’ (backend) и ‘шлюз’ (gateway), опаковане в docker образи и настройване на съвместната им работа.

    Ключови думи: Java 11, Spring Boot, Docker, оптимизация на изображения.

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

    Ключови думи: Kubernetes, GKE, управление на ресурси, автоматично мащабиране, секрети.

  3. Създаване на чарт с помощта на Helm 3 за по-ефективно управление на кластера.

    Ключови думи: Helm 3, разполагане на чарт.

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

    Ключови думи: конфигурация на Jenkins, плъгини, отделно хранилище за конфигурации.

На всяка стъпка планирам да посветя отделна статия.

Фокусът на този цикъл статии не е върху това как да пишем микросервизи, а как да ги накараме да работят в единна система. Въпреки че всички тези неща обикновено са извън отговорността на разработчика, смятам, че е полезно да сме запознати с тях поне на 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 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("Брой на заявките %s (шлюз %d, секрет %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/
Брой заявки 1 (шлюз 38560358, секрет "default-secret")

Всичко работи. Внимателният читател ще отбележи, че нищо не ни пречи да се обърнем директно към бекенда, заобикаляйки шлюза (http://localhost:8081/requests). За да го поправим, услугите трябва да бъдат обединени в една мрежа, а навън да "стърчи" само шлюзът.
Също така, и двете услуги споделят една файлова система, създават потоци и в един момент могат да започнат да си пречат. Не би било лошо да изолираме нашите микросервизи. Това може да се постигне чрез разнасяне на приложенията на различни машини (много пари, сложно), използване на виртуални машини (ресурсоемко, бавен старт) или чрез контейнеризация. Очаквано, избираме третия вариант и Docker като инструмент за контейнеризация.

Docker

Ако накратко, Docker създава изолирани контейнери, по един за приложение. За да използвате Docker, трябва да напишете Dockerfile — инструкция за изграждане и стартиране на приложението. След това можете да изградите образ, да го качите в регистъра на образите (№ DockerHub) и с една команда да развернете своя микросервис в всяка контаминиризирана среда.

Dockerfile

Една от най-важните характеристики на образа е размерът му. Компактен образ по-бързо ще се изтегли от отдалечения репозиторий, ще заема по-малко място и услугата ви ще стартира по-бързо. Всеки образ се изгражда на основата на базов образ, и се препоръчва да изберете най-минималистичния вариант. Добър вариант е 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 сигнализира на Docker, че приложението в контейнера ще използва порта 8080 (това не прави приложението достъпно навън, но позволява достъп до приложението, например, от друг контейнер в същата Docker мрежа).

За да опаковате услугите в образи, трябва да изпълните командите от корена на всеки проект:

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 anshelen@yandex.ru"
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.

Първо, нашите услуги трябва да бъдат в една мрежа. В 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 за контейнерите по техните имена. Тоест, услугата за бекенд ще бъде достъпна вътре в мрежата на Docker на адрес 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 използваме за задаване на променливи на средата, които автоматично ще бъдат прочетени от Spring и ще заменят свойствата от application.properties.

След стартиране извикваме http://localhost/ и се уверяваме, че всичко работи, както и предишния път.

Заключение

В крайна сметка създадохме два прости микросервиса, опаковахме ги в Docker контейнери и ги стартирахме на една машина. В получената система обаче има редица недостатъци:

  • Лоша отказоустойчивост — всичко работи на един сървър
  • Лоша мащабируемост — при увеличаване на натоварването би било добре автоматично да се разгръщат допълнителни инстанции на услугите и да се балансира натоварването между тях
  • Сложност при стартиране — необходима е минимално 3 команди, всяка с определени параметри (това само за 2 услуги)

За решаване на горепосочените проблеми съществуват редица решения, като Docker Swarm, Nomad, Kubernetes или OpenShift. Ако цялата система е написана на Java, може да се разгледа на Spring Cloud (добра статия).

В следващата част ще разкажа как Настравах Kubernetes и деплойвах проекта в Google Kubernetes Engine.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster