Wir lernen, Mikrodienste zu implementieren. Teil 1. Spring Boot und Docker

Wir lernen, Mikrodienste zu implementieren. Teil 1. Spring Boot und Docker

Hallo, Habr.

In diesem Artikel möchte ich über meine Erfahrungen bei der Schaffung einer Lernumgebung für Experimente mit Mikrodiensten berichten. Bei der Erlernung jedes neuen Werkzeugs wollte ich es nicht nur auf meinem lokalen Rechner ausprobieren, sondern auch unter realistischeren Bedingungen. Daher habe ich beschlossen, eine vereinfachte Mikrodienstanwendung zu erstellen, die später mit verschiedenen interessanten Technologien "aufgerüstet" werden kann. Das Hauptziel des Projekts ist es, eine maximale funktionale Ähnlichkeit mit einem echten System zu erreichen.

Ursprünglich habe ich die Erstellung des Projekts in mehrere Schritte unterteilt:

  1. Zwei Dienste erstellen — ‘Backend’ (backend) und ‘Gateway’ (gateway), sie in Docker-Images verpacken und ihre gemeinsame Arbeit einrichten

    Schlüsselwörter: Java 11, Spring Boot, Docker, Bildoptimierung

  2. Entwicklung der Kubernetes-Konfiguration und Deployment des Systems in Google Kubernetes Engine

    Schlüsselwörter: Kubernetes, GKE, Ressourcenmanagement, Autoskalierung, Geheimnisse

  3. Erstellung eines Charts mit Helm 3 für eine effizientere Verwaltung des Clusters

    Schlüsselwörter: Helm 3, Chart-Deployment

  4. Einrichtung von Jenkins und einer Pipeline für die automatische Bereitstellung von Code im Cluster

    Schlüsselwörter: Jenkins-Konfiguration, Plugins, separates Konfigurationsrepository

Jeden Schritt möchte ich einem eigenen Artikel widmen.

Der Fokus dieser Artikelreihe liegt nicht darauf, wie man Mikrodienste schreibt, sondern wie man sie in einem einheitlichen System zum Laufen bringt. Obwohl all diese Dinge normalerweise nicht in den Verantwortungsbereich des Entwicklers fallen, denke ich, dass es dennoch hilfreich ist, wenigstens zu 20% damit vertraut zu sein (was bekanntlich 80% der Ergebnisse bringt). Einige unbestreitbar wichtige Themen, wie Sicherheit, werden in diesem Projekt außen vor gelassen, da der Autor darin wenig Erfahrung hat; das System wird ausschließlich für den persönlichen Gebrauch erstellt. Ich freue mich über jede Meinung und konstruktive Kritik.

Erstellung von Mikrodiensten

Die Dienste wurden in Java 11 unter Verwendung von Spring Boot geschrieben. Die zwischen den Diensten kommunikation wurde über REST organisiert. Das Projekt wird eine minimale Anzahl von Tests umfassen (damit es später etwas gibt, das in Jenkins getestet werden kann). Der Quellcode der Dienste ist auf GitHub verfügbar: Backend und Gateway.

Um den Status jedes Dienstes überprüfen zu können, wurde Spring Actuator als Abhängigkeit hinzugefügt. Er wird einen Endpunkt /actuator/health erstellen und den Status 200 zurückgeben, wenn der Dienst bereit ist, Traffic zu empfangen, oder 504 im Falle von Problemen. In diesem Fall handelt es sich um eine recht fiktive Überprüfung, da die Dienste sehr einfach sind, und bei einem Notfall wahrscheinlich ganz ausfallen werden, anstatt teilweise funktionsfähig zu bleiben. In echten Systemen kann Actuator jedoch helfen, Probleme zu diagnostizieren, bevor die Benutzer beginnen, darauf zu stoßen. Wenn beispielsweise Probleme mit dem Zugriff auf die Datenbank auftreten, können wir automatisch darauf reagieren, indem wir aufhören, Anfragen mit einer defekten Dienstinstanz zu bearbeiten.

Backend-Dienst

Der Backend-Dienst wird einfach die Anzahl der empfangenen Anfragen zählen und zurückgeben.

Controller-Code:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

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

Test für den Controller:

@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-Dienst

Das Gateway wird die Anfrage an den Backend-Dienst weiterleiten und sie mit den folgenden Informationen ergänzen:

  • Gateway-ID. Sie wird benötigt, um einen Gateway-Instanz anhand der Serverantwort zu unterscheiden.
  • Ein gewisses "Geheimnis", das als sehr wichtiges Passwort fungiert (Nr. Schlüssel für die Verschlüsselung eines wichtigen Cookies)

Konfiguration in application.properties:

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

Adapter zur Kommunikation mit dem Backend:

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

Controller:

@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("Anzahl der Anfragen %s (Gateway %d, Geheimnis %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Start:

Backend starten:

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

Gateway starten:

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

Überprüfen:

$ curl http://localhost:8080/
Anzahl der Anfragen 1 (gateway 38560358, geheim "default-secret")

Alles funktioniert. Ein aufmerksamer Leser wird bemerken, dass es uns nicht daran hindert, direkt auf das Backend zuzugreifen, um den Gateway zu umgehen (http://localhost:8081/requests). Um dies zu beheben, müssen die Dienste in einem Netzwerk zusammengefasst werden, und nach außen sollte nur der Gateway sichtbar sein.
Außerdem teilen sich beide Dienste ein Dateisystem, erzeugen Streams und können sich zu einem bestimmten Zeitpunkt gegenseitig stören. Es wäre wünschenswert, unsere Mikrodienste zu isolieren. Dies kann durch die Verteilung der Anwendungen auf verschiedene Maschinen (sehr teuer, kompliziert), durch die Verwendung virtueller Maschinen (ressourcenintensiv, lange Startzeit) oder durch Containerisierung erreicht werden. Erwartungsgemäß wählen wir die dritte Option und Docker als Werkzeug für die Containerisierung.

Docker

Kurz gesagt, Docker erstellt isolierte Container, jeweils einen pro Anwendung. Um Docker zu nutzen, muss man ein Dockerfile schreiben - eine Anleitung zum Bauen und Ausführen der Anwendung. Anschließend kann ein Image erstellt, in ein Registry geladen werden (Nr. DockerHub) und in einem einzigen Befehl sollte Ihr Mikrodienst in jeder dockerisierten Umgebung bereitgestellt werden.

Dockerfile

Eine der wichtigsten Eigenschaften eines Images ist dessen Größe. Ein kompaktes Image kann schneller von einem entfernten Repository heruntergeladen werden, benötigt weniger Platz und Ihr Dienst startet schneller. Jedes Image basiert auf einem Basis-Image und es wird empfohlen, die minimalistischste Option zu wählen. Eine gute Wahl ist Alpine - eine vollständige Linux-Distribution mit einem Minimum an Paketen.

Zunächst versuchen wir, ein Dockerfile "von der Pike auf" zu schreiben (gleich zu Beginn, das ist eine schlechte Methode, machen Sie das nicht):

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

Hier verwenden wir ein Basis-Image auf Alpine-Basis mit bereits installiertem JDK zum Bauen unseres Projekts. Mit dem Befehl ADD fügen wir das aktuelle Verzeichnis src in das Image ein, kennzeichnen es als Arbeitsverzeichnis (WORKDIR) und starten den Build-Prozess. Der Befehl EXPOSE 8080 signalisiert Docker, dass die Anwendung im Container den Port 8080 verwenden wird (das macht die Anwendung nicht von außen zugänglich, erlaubt aber den Zugriff auf die Anwendung aus einem anderen Container im selben Docker-Netzwerk).

Um die Dienste in Images zu packen, müssen die Befehle aus dem Wurzelverzeichnis jedes Projekts ausgeführt werden:

docker image build . -t msvc-backend:1.0.0

Das Ergebnis ist ein Image von 456 MB (wobei das Basis-Image JDK 340 MB beansprucht hat). Und dabei sind die Klassen in unserem Projekt nur mit den Fingern abzählbar. Um die Größe unseres Images zu verringern:

  • Wir verwenden einen mehrstufigen Build-Prozess. Im ersten Schritt bauen wir das Projekt, im zweiten installieren wir das JRE, und im dritten Schritt kopieren wir alles in ein neues, sauberes Alpine-Image. Im finalen Image befinden sich nur die notwendigen Komponenten.
  • Wir nutzen die Modularisierung von Java. Seit Java 9 kann man mit dem Tool jlink ein JRE nur aus den benötigten Modulen erstellen.

Für Neugierige, hier ist ein guter Artikel über Ansätze zur Verringerung der Bildgrößen. https://habr.com/ru/company/ruvds/blog/485650/.

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

Wir erstellen das Image neu, und es hat sich letztendlich auf 77 MB reduziert, was einer Reduzierung um das Sechsfache entspricht. Nicht schlecht. Anschließend können die fertigen Images in ein Registry hochgeladen werden, damit Ihre Images zum Download über das Internet verfügbar sind.

Gemeinsame Ausführung von Services in Docker

Zunächst müssen unsere Services im selben Netzwerk sein. In Docker gibt es mehrere Netzwerktypen, und wir verwenden den grundlegendsten von ihnen – bridge, der Containern, die auf demselben Host ausgeführt werden, einen Netzwerkverbund ermöglicht. Wir erstellen das Netzwerk mit folgendem Befehl:

docker network create msvc-network

Danach starten wir den Backend-Container mit dem Namen „backend“ mit dem Image microservices-backend:1.0.0:

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

Es ist erwähnenswert, dass das bridge-Netzwerk von Haus aus eine Service-Discovery für Container anhand ihrer Namen bietet. Das bedeutet, dass der Backend-Service innerhalb des Docker-Netzwerks unter folgender Adresse verfügbar sein wird: http://backend:8080.

Gateway starten:

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

In diesem Team geben wir an, dass wir den Port 80 unseres Hosts auf den Port 8080 des Containers durchleiten. Die Umgebungsoptionen verwenden wir, um Umgebungsvariablen festzulegen, die automatisch von Spring gelesen werden und die Eigenschaften aus der application.properties überschreiben.

Nach dem Start rufen wir auf http://localhost/ und vergewissern uns, dass alles funktioniert, wie im letzten Fall.

Fazit

Insgesamt haben wir zwei einfache Mikrodienste erstellt, sie in Docker-Container gepackt und gemeinsam auf einer Maschine gestartet. Das erhaltene System hat jedoch einige Nachteile:

  • Schlechte Ausfallsicherheit – bei uns läuft alles auf einem Server
  • Schlechte Skalierbarkeit – bei höherer Last wäre es gut, zusätzliche Instanzen der Dienste automatisch bereitzustellen und die Last zwischen ihnen zu verteilen
  • Komplexität beim Start – wir mussten mindestens 3 Befehle eingeben, und zwar mit bestimmten Parametern (das ist nur für 2 Dienste)

Um die oben genannten Probleme zu lösen, gibt es eine Reihe von Lösungen wie Docker Swarm, Nomad, Kubernetes oder OpenShift. Wenn das gesamte System in Java geschrieben ist, kann man sich in Richtung Spring Cloud orientieren (ein guter Artikel).

Im dem nächsten Teil ich werde darüber erzählen, wie ich Kubernetes konfiguriert und ein Projekt in Google Kubernetes Engine bereitgestellt habe.

Quelle: habr.com

Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server 🔥 Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server - ProHoster