Вучымся разгортваць мікрасэрвісы. Частка 1. Spring Boot і Docker
Прывітанне, Хабр.
У гэтым артыкуле я хачу расказаць пра свой досвед стварэння навучальнага асяроддзя для эксперыментаў з мікрасэрвісамі. Пры вывучэнні кожнай новай прылады мне заўсёды жадалася яго паспрабаваць не толькі на лакальнай машыне, але і ў больш рэалістычных умовах. Таму я вырашыў стварыць спрошчанае мікрасэрвіснае прыкладанне, якое пасля можна будзе "абвешваць" усялякімі цікавымі тэхналогіямі. Асноўнае патрабаванне да праекту - яго максімальная функцыянальная набліжанасць да рэальнай сістэмы.
Першапачаткова я разбіў стварэнне праекту на некалькі крокаў:
Стварыць два сэрвісу - 'бекенд' (backend) і 'шлюз' (gateway), спакаваць іх у docker-вобразы і наладзіць іх сумесную працу
Ключавыя словы: Java 11, Spring Boot, Docker, image optimization
Стварэнне чарта з дапамогай Helm 3 для больш эфектыўнага кіравання кластарам
Ключавыя словы: Helm 3, chart deployment
Настройка 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 шлюза. Ён патрэбен, каб можна было па адказе сервера адрозніць адзін асобнік шлюза ад іншага
Нейкі "сакрэт", які будзе гуляць ролю вельмі важнага пароля (№ ключа шыфравання важнай печыва)
$ 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 толькі з патрэбных модуляў.
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.
У гэтай камандзе мы паказваем, што мы пракідваем 80 порт нашага хаста на 8080 порт кантэйнера. Опцыі env мы выкарыстоўваем для ўсталёўкі зменных асяроддзя, якія аўтаматычна будуць адыманыя спрынгам і перавызначаць уласцівасці з application.properties.
Пасля запуску выклікаем http://localhost/ і пераконваемся, што ўсё працуе, як і ў мінулым выпадку.
Заключэнне
У выніку мы стварылі два прасценькія мікрасэрвісы, спакавалі іх у докер-кантэйнеры і сумесна запусцілі на адной машыне. У атрыманай сістэмы, аднак, ёсць шэраг недахопаў:
Дрэнная адмоваўстойлівасць - у нас усё працуе на адным серверы
Дрэнная маштабаванасць - пры павелічэнні нагрузкі было б нядрэнна аўтаматычна разгортваць дадатковыя асобнікі сэрвісаў і балансаваць нагрузку паміж імі
Складанасць запуску - нам спатрэбілася ўвесці як мінімум 3 каманды, прычым з пэўнымі параметрамі (гэта толькі для 2 сэрвісаў)
Для ўхілення вышэйпералічаных праблем існуе шэраг рашэнняў, такіх як Docker Swarm, Nomad, Kubernetes або OpenShift. Калі ўся сістэма будзе напісана на Java можна паглядзець у бок Spring Cloud (добры артыкул).
В наступнай часткі я раскажу пра тое, як я наладжваў Kubernetes і дэплоіў праект у Google Kubernetes Engine.