Dowiedz się, jak wdrażać mikrousługi. Część 1. Spring Boot i Docker

Dowiedz się, jak wdrażać mikrousługi. Część 1. Spring Boot i Docker

Hej Habr.

W tym artykule chcę opowiedzieć o moim doświadczeniu w tworzeniu środowiska edukacyjnego do eksperymentowania z mikroserwisami. Kiedy poznawałem każde nowe narzędzie, zawsze chciałem je wypróbować nie tylko na lokalnej maszynie, ale także w bardziej realistycznych warunkach. Dlatego zdecydowałem się na stworzenie uproszczonej aplikacji mikroserwisowej, którą później można „obejmować” najróżniejszymi ciekawymi technologiami. Głównym wymaganiem dla projektu jest jego maksymalna bliskość funkcjonalna do rzeczywistego systemu.

Początkowo podzieliłem tworzenie projektu na kilka etapów:

  1. Utwórz dwie usługi - „backend” (backend) i „bramę” (bramę), spakuj je w obrazy dokera i skonfiguruj do współpracy

    Słowa kluczowe: Java 11, Spring Boot, Docker, optymalizacja obrazu

  2. Rozwój konfiguracji Kubernetes i wdrożenie systemu w Google Kubernetes Engine

    Słowa kluczowe: Kubernetes, GKE, zarządzanie zasobami, autoskalowanie, sekrety

  3. Tworzenie wykresu za pomocą Helm 3 w celu lepszego zarządzania klastrami

    Tagi: Helm 3, wdrażanie wykresów

  4. Konfigurowanie Jenkinsa i potoku do automatycznego dostarczania kodu do klastra

    Słowa kluczowe: konfiguracja Jenkinsa, wtyczki, osobne repozytorium konfiguracji

Każdemu krokowi planuję poświęcić osobny artykuł.

Celem tej serii artykułów nie jest to, jak napisać mikrousługi, ale jak sprawić, by działały w jednym systemie. Chociaż wszystkie te rzeczy są zwykle poza odpowiedzialnością programisty, myślę, że nadal warto znać je przynajmniej w 20% (co, jak wiadomo, daje 80% wyniku). Niektóre bezwarunkowo ważne tematy, takie jak bezpieczeństwo, zostaną pominięte w tym projekcie, ponieważ autor niewiele rozumie, że ten system jest tworzony wyłącznie do użytku osobistego. Przyjmę wszelkie opinie i konstruktywną krytykę.

Tworzenie mikroserwisów

Usługi zostały napisane w Javie 11 przy użyciu Spring Boot. Interakcja między usługami jest zorganizowana przy użyciu usług REST. Projekt będzie zawierał minimalną ilość testów (żeby później było co testować w Jenkinsie). Kod źródłowy usług jest dostępny na GitHub: zaplecze и Wejście.

Aby móc sprawdzić status każdej z usług, do ich zależności dodano Siłownik sprężynowy. Utworzy punkt końcowy /actuator/health i zwróci stan 200, jeśli usługa jest gotowa do przyjęcia ruchu, lub 504, jeśli wystąpi problem. W tym przypadku jest to raczej fikcyjne sprawdzenie, ponieważ usługi są bardzo proste, aw przypadku jakiejś siły wyższej bardziej prawdopodobne jest, że staną się całkowicie niedostępne niż pozostaną częściowo sprawne. Ale w rzeczywistych systemach Actuator może pomóc zdiagnozować problem, zanim użytkownicy zaczną o niego walczyć. Na przykład, jeśli występują problemy z dostępem do bazy danych, możemy automatycznie zareagować na to, zatrzymując przetwarzanie żądań z niedziałającą instancją usługi.

Obsługa zaplecza

Usługa backendu po prostu policzy i zwróci liczbę zaakceptowanych żądań.

Kod kontrolera:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

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

Test 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"));
    }
}

Brama serwisowa

Bramka przekaże żądanie do usługi backendu, uzupełniając je o następujące informacje:

  • identyfikator bramy. Jest to potrzebne, aby po odpowiedzi serwera można było odróżnić jedną instancję bramy od drugiej
  • Jakiś „sekret”, który będzie pełnić rolę bardzo ważnego hasła (numer klucza szyfrującego ważnego pliku cookie)

Konfiguracja w application.properties:

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

Adapter zaplecza:

@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();
    }
}

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("Number of requests %s (gateway %d, secret %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Uruchomić:

Uruchamiamy backend:

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

Uruchomienie bramki:

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

Sprawdzanie:

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

Wszystko działa. Uważny czytelnik zauważy, że nic nie stoi na przeszkodzie, aby uzyskać bezpośredni dostęp do backendu z pominięciem bramki (http://localhost:8081/requests). Aby to naprawić, usługi muszą być połączone w jedną sieć, a tylko bramka powinna „wystawać” na zewnątrz.
Ponadto obie usługi współdzielą jeden system plików, generują strumienie iw pewnym momencie mogą zacząć sobie przeszkadzać. Byłoby miło wyizolować nasze mikroserwisy. Można to osiągnąć poprzez dystrybucję aplikacji na różnych maszynach (duże pieniądze, trudne), przy użyciu maszyn wirtualnych (intensywne zasoby, długie uruchamianie) lub przy użyciu konteneryzacji. Zgodnie z oczekiwaniami wybieramy trzecią opcję i Doker jako narzędzie do konteneryzacji.

Doker

Krótko mówiąc, docker tworzy izolowane kontenery, po jednym na aplikację. Aby korzystać z dokera, należy napisać plik Dockerfile - instrukcję budowania i uruchamiania aplikacji. Następnie możesz zbudować obraz, przesłać go do rejestru obrazów (nr. Dockerhub) i wdrażaj swoją mikrousługę w dowolnym środowisku dokerowanym za pomocą jednego polecenia.

Dockerfile

Jedną z najważniejszych cech obrazu jest jego wielkość. Kompaktowy obraz szybciej pobierze się ze zdalnego repozytorium, zajmie mniej miejsca, a Twoja usługa uruchomi się szybciej. Każdy obraz jest budowany na podstawie obrazu podstawowego i zaleca się wybranie najbardziej minimalistycznej opcji. Dobrą opcją jest Alpine, kompletna dystrybucja Linuksa z minimalnymi pakietami.

Najpierw spróbujmy napisać plik Dockerfile „na czole” (od razu powiem, że to zły sposób, nie róbcie tego):

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 zbudowania naszego projektu. Komendą ADD dodajemy bieżący katalog src do obrazu, oznaczamy go jako działający (WORKDIR) i rozpoczynamy budowę. Komenda EXPOSE 8080 sygnalizuje dockerowi, że aplikacja w kontenerze użyje swojego portu 8080 (nie spowoduje to udostępnienia aplikacji z zewnątrz, ale umożliwi dostęp do aplikacji na przykład z innego kontenera w tej samej sieci dokerów ).

Aby spakować usługi w obrazy, musisz uruchomić polecenia z katalogu głównego każdego projektu:

docker image build . -t msvc-backend:1.0.0

Rezultatem jest obraz o wielkości 456 MB (z czego podstawowy obraz JDK zajmował 340 MB). A wszystko to pomimo tego, że zajęcia w naszym projekcie można policzyć na palcach. Aby zmniejszyć rozmiar naszego obrazu:

  • Stosujemy montaż wieloetapowy. W pierwszym kroku zbudujemy projekt, w drugim zainstalujemy JRE, aw trzecim skopiujemy wszystko do nowego czystego obrazu Alpine. W sumie na ostatecznym obrazie będą tylko niezbędne komponenty.
  • Użyjmy modularyzacji java. Począwszy od Javy 9, możesz użyć narzędzia jlink, aby utworzyć środowisko JRE tylko z potrzebnych modułów

Dla dociekliwych tutaj jest dobry artykuł na temat metod redukcji obrazu. https://habr.com/ru/company/ruvds/blog/485650/.

Ostateczny plik Docker:

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"]

Odtworzyliśmy obraz, w wyniku czego stracił on 6-krotną wagę do 77 MB. Nie jest zły. Następnie gotowe obrazy można przesłać do rejestru obrazów, dzięki czemu można je pobrać z Internetu.

Współprowadzenie usług w Dockerze

Na początek nasze usługi muszą być w tej samej sieci. Istnieje kilka rodzajów sieci w dockerze, a my używamy najbardziej prymitywnego z nich - bridge, który pozwala na sieciowanie kontenerów działających na tym samym hoście. Utwórz sieć za pomocą następującego polecenia:

docker network create msvc-network

Następnie uruchom kontener zaplecza o nazwie „backend” z obrazem microservices-backend:1.0.0:

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

Warto zauważyć, że sieć pomostowa zapewnia natychmiastowe wykrywanie usług dla kontenerów według ich nazw. Oznacza to, że usługa zaplecza będzie dostępna w sieci dokerów o godz http://backend:8080.

Uruchomienie bramki:

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

W tym poleceniu wskazujemy, że przekazujemy port 80 naszego hosta do portu 8080 kontenera. Używamy opcji env do ustawiania zmiennych środowiskowych, które będą automatycznie odczytywane przez wiosnę i nadpisywania właściwości z application.properties.

Po uruchomieniu dzwonimy http://localhost/ i upewnij się, że wszystko działa, tak jak w poprzednim przypadku.

wniosek

W rezultacie stworzyliśmy dwa proste mikroserwisy, spakowaliśmy je w kontenery docker i uruchomiliśmy razem na tej samej maszynie. Powstały system ma jednak szereg wad:

  • Słaba odporność na awarie - wszystko działa u nas na jednym serwerze
  • Słaba skalowalność – przy wzroście obciążenia dobrze byłoby automatycznie wdrażać dodatkowe instancje usługi i równoważyć obciążenie między nimi
  • Złożoność uruchomienia - musieliśmy wprowadzić co najmniej 3 polecenia i z określonymi parametrami (dotyczy to tylko 2 usług)

Aby rozwiązać powyższe problemy, istnieje szereg rozwiązań, takich jak Docker Swarm, Nomad, Kubernetes czy OpenShift. Jeśli cały system jest napisany w Javie, możesz spojrzeć w stronę Spring Cloud (dobry artykuł).

В następna część Opowiem o tym, jak skonfigurowałem Kubernetes i wdrożyłem projekt w Google Kubernetes Engine.

Źródło: www.habr.com

Dodaj komentarz