Вучымся разгортваць мікрасэрвісы. Частка 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). Каб гэта выправіць, паслугі павінны быць аб'яднаны ў адну сетку, а вонкі "тырчаць" павінен толькі шлюз.
Таксама абодва сэрвісу дзеляць адну файлавую сістэму, плодзяць струмені і адначасова могуць пачаць замінаць адзін аднаму. Было б нядрэнна ізаляваць нашы мікрасэрвісы. Гэтага можна дасягнуць з дапамогай разнясення прыкладанняў па розных машынах (шмат грошай, складана), выкарыстанні віртуальных машын (рэсурсаёмістасць, доўгі запуск) ці ж з дапамогай кантэйнерызацыі. Чакана выбіраемы трэці варыянт і Докер як інструмент для кантэйнерызацыі.

Докер

Калі сцісла, то докер стварае ізаляваныя кантэйнеры, па адным на дадатак. Каб выкарыстоўваць докер, патрабуецца напісаць 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

Дадаць каментар