
Cześć, Habr.
W tym artykule chcę podzielić się swoim doświadczeniem w tworzeniu środowiska edukacyjnego do eksperymentów z mikroserwisami. Podczas nauki każdego nowego narzędzia zawsze chciałem go wypróbować nie tylko na lokalnej maszynie, ale również w bardziej realistycznych warunkach. Dlatego postanowiłem stworzyć uproszczoną aplikację mikroserwisową, którą w przyszłości można będzie "wyposażać" w różne ciekawe technologie. Główne wymaganie dotyczące projektu to maksymalne funkcjonalne zbliżenie do rzeczywistego systemu.
Początkowo podzieliłem tworzenie projektu na kilka kroków:
Stworzenie dwóch serwisów – 'backend' i 'gateway', spakowanie ich w obrazy dockerowe i skonfigurowanie ich współpracy.
Słowa kluczowe: Java 11, Spring Boot, Docker, optymalizacja obrazów.
Słowa kluczowe: Kubernetes, GKE, zarządzanie zasobami, autoskalowanie, sekrety.
Stworzenie chartu za pomocą Helm 3 dla efektywniejszego zarządzania klastrem.
Słowa kluczowe: Helm 3, wdrożenie chartu.
Konfiguracja Jenkins i pipeline'u do automatycznego dostarczania kodu do klastra.
Słowa kluczowe: konfiguracja Jenkins, wtyczki, oddzielne repozytorium konfiguracji.
Każdemu krokowi planuję poświęcić osobny artykuł.
Skierowanie tego cyklu artykułów nie polega na tym, jak pisać mikroserwisy, lecz na tym, jak sprawić, by działały w jednym systemie. Chociaż wszystkie te kwestie zazwyczaj leżą poza odpowiedzialnością programisty, uważam, że warto być zaznajomionym z nimi przynajmniej w 20% (co jak wiadomo, daje 80% efektu). Niektóre niewątpliwie ważne tematy, takie jak zapewnienie bezpieczeństwa, zostaną pominięte w tym projekcie, ponieważ autor ma w tej dziedzinie niewiele do powiedzenia, system tworzony jest wyłącznie do użytku osobistego. Będę wdzięczny za wszelkie opinie i konstruktywną krytykę.
Tworzenie mikroserwisów
Serwisy zostały napisane w Java 11 z użyciem Spring Boot. Interakcja międzyserwisowa została zorganizowana za pomocą REST. Projekt będzie zawierał minimalną liczbę testów (aby później mieć co testować w Jenkins). Kod źródłowy serwisów jest dostępny na GitHubie: i .
Aby móc sprawdzić stan każdego z usług, do ich zależności dodano Spring Actuator. Utworzy on punkt końcowy /actuator/health i będzie zwracał status 200, jeśli usługa jest gotowa do przyjmowania ruchu, lub 504 w przypadku problemów. W tym przypadku jest to dość fikcyjna kontrola, ponieważ usługi są bardzo proste i w przypadku jakiegoś nieprzewidzianego zdarzenia prawdopodobnie staną się całkowicie niedostępne, niż zachowają częściową funkcjonalność. Jednak w rzeczywistych systemach Actuator może pomóc zdiagnozować problem, zanim użytkownicy zaczną się nim zajmować. Na przykład, w przypadku problemów z dostępem do bazy danych, będziemy mogli automatycznie zareagować, przerywając przetwarzanie żądań przez uszkodzony egzemplarz usługi.
Usługa Backend
Usługa backendu po prostu zliczy i zwróci liczbę odebranych żądań.
Kod kontrolera:
@RestController
public class RequestsCounterController {
private final AtomicLong counter = new AtomicLong();
@GetMapping("/requests")
public Long getRequestsCount() {
return counter.incrementAndGet();
}
}Test dla kontrolera:
@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"));
}
}Usługa Gateway
Zespół będzie przekazywać żądanie do usługi backendu, uzupełniając je następującymi informacjami:
- id bramy. Jest potrzebne, aby po odpowiedzi serwera odróżnić jeden egzemplarz bramy od drugiego
- Jakiś 'sekret', który będzie odgrywać rolę bardzo ważnego hasła (nr klucza szyfrowania ważnego cookie)
Konfiguracja w application.properties:
backend.url=http://localhost:8081
instance.id=${random.int}
secret="default-secret"Adapter do komunikacji z backendem:
@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();
}
}Kontroler:
@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("Liczba żądań %s (brama %d, sekret %s)", backendAdapter.getRequests(), instanceId, secret);
}
}Uruchomienie:
Uruchamiamy backend:
./mvnw package -DskipTests
java -Dserver.port=8081 -jar target/microservices-backend-1.0.0.jarUruchamiamy bramę:
./mvnw package -DskipTests
java -jar target/microservices-gateway-1.0.0.jarSprawdzamy:
$ curl http://localhost:8080/
Liczba żądań 1 (brama 38560358, sekret "default-secret")Wszystko działa. Uważny czytelnik zauważy, że nic nie stoi na przeszkodzie, aby połączyć się z backendem bezpośrednio, omijając bramę (). Aby to naprawić, usługi muszą być połączone w tę samą sieć, a na zewnątrz powinien "wystawać" tylko brama.
Oba serwisy dzielą także jeden system plików, generują wątki i w jednym momencie mogą zacząć sobie nawzajem przeszkadzać. Byłoby dobrze odizolować nasze mikrousługi. Można to osiągnąć poprzez rozdzielenie aplikacji na różne maszyny (dużo pieniędzy, trudne), użycie maszyn wirtualnych (zasobożerne, długi czas uruchamiania) lub za pomocą konteneryzacji. Oczekiwanie wybieramy trzecią opcję i jako narzędzie do konteneryzacji.
Docker
Krótko mówiąc, Docker tworzy izolowane kontenery, po jednym na aplikację. Aby używać Dockera, należy napisać Dockerfile — instrukcję do budowy i uruchamiania aplikacji. Następnie będzie można zbudować obraz, przesłać go do rejestru obrazów (nr ) i za pomocą jednej komendy wdrożyć swoją mikrousługę w dowolnym środowisku z Dockerem.
Dockerfile
Jednym z najważniejszych parametrów obrazu jest jego rozmiar. Kompaktowy obraz szybciej pobierze się z zdalnego repozytorium, zajmie mniej miejsca, a twój serwis uruchomi się szybciej. Każdy obraz jest budowany na podstawie obrazu bazowego i zaleca się wybieranie najminimalistycznej opcji. Dobrą opcją jest Alpine — pełnoprawna dystrybucja Linuxa z minimalną liczbą pakietów.
Na początek spróbujmy napisać Dockerfile "wprost" (od razu mówię, że to zły sposób, nie róbcie tak):
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"]Tutaj używamy obrazu bazowego opartego na Alpine z już zainstalowanym JDK do budowy naszego projektu. Komendą ADD dodajemy do obrazu bieżący katalog src, oznaczamy go jako roboczy (WORKDIR) i rozpoczynamy budowę. Komenda EXPOSE 8080 informuje Dockera, że aplikacja w kontenerze będzie używać portu 8080 (nie uczyni to aplikacji dostępną na zewnątrz, ale pozwoli na dostęp do aplikacji z innego kontenera w tej samej sieci Dockera).
Aby zapakować serwisy w obrazy, należy wykonać polecenia z katalogu głównego każdego projektu:
docker image build . -t msvc-backend:1.0.0W rezultacie uzyskujemy obraz o rozmiarze 456 MB (z czego podstawowy obraz JDK zajął MB). I to przy założeniu, że klas w naszym projekcie można policzyć na palcach jednej ręki. Aby zmniejszyć rozmiar naszego obrazu:
- Wykorzystujemy budowę wieloetapową. Na pierwszym etapie zbudujemy projekt, na drugim zainstalujemy JRE, a na trzecim kroku skopiujemy to wszystko do nowego czystego obrazu Alpine. W efekcie w finalnym obrazie znajdą się tylko niezbędne komponenty.
- Skorzystamy z modularności Javy. Począwszy od Javy 9, możemy za pomocą narzędzia jlink stworzyć JRE tylko z potrzebnych modułów.
Dla dociekliwych, oto dobry artykuł na temat metod zmniejszania rozmiarów obrazów. .
Finalny 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"]Przykład tworzenia obrazu został zredukowany sześciokrotnie, osiągając 77 MB. Niezły wynik. Następnie gotowe obrazy można załadować do rejestru obrazów, aby były dostępne do pobrania z Internetu.
Wspólne uruchomienie usług w Dockerze.
Na początku nasze usługi muszą być w tej samej sieci. W Dockerze istnieje kilka typów sieci, a my użyjemy najprostszej z nich — bridge, która pozwala na łączenie kontenerów uruchomionych na jednym hoście. Stworzymy sieć następującą komendą:
docker network create msvc-networkNastępnie uruchomimy kontener backendu o nazwie 'backend' z obrazem microservices-backend:1.0.0:
docker run -dit --name backend --network msvc-net microservices-backend:1.0.0Warto zauważyć, że sieć bridge zapewnia automatyczne odkrywanie usług dla kontenerów po ich nazwach. To znaczy, że usługa backendu będzie dostępna wewnątrz sieci Dockera pod adresem .
Uruchamiamy bramę:
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.0W tym zespole wskazujemy, że przekierowujemy port 80 naszego hosta na port 8080 kontenera. Opcje env wykorzystujemy do ustawiania zmiennych środowiskowych, które zostaną automatycznie odczytane przez Spring i nadpiszą właściwości z application.properties.
Po uruchomieniu wywołujemy i upewniamy się, że wszystko działa tak, jak w poprzednim przypadku.
Podsumowanie
W rezultacie stworzyliśmy dwa proste mikroserwisy, spakowaliśmy je w kontenery Docker i uruchomiliśmy jednocześnie na jednej maszynie. Otrzymany system ma jednak szereg wad:
- Słaba odporność na awarie — wszystko działa na jednym serwerze.
- Słaba skalowalność — przy zwiększonym obciążeniu dobrze by było automatycznie uruchamiać dodatkowe instancje usług i równoważyć obciążenie między nimi.
- Trudności w uruchomieniu — potrzebowaliśmy wprowadzić co najmniej 3 polecenia, przy czym z określonymi parametrami (to tylko dla 2 usług).
Aby rozwiązać wymienione powyżej problemy, istnieje szereg rozwiązań, takich jak Docker Swarm, Nomad, Kubernetes czy OpenShift. Jeśli cały system będzie napisany w Javie, warto spojrzeć w kierunku Spring Cloud ().
W opowiem o tym, jak skonfigurowałem Kubernetes i wdrożyłem projekt w Google Kubernetes Engine.
Źródło: habr.com
