Lernen wir, Mikrodienste bereitzustellen. Teil 1: Spring Boot und Docker

Lernen wir, Mikrodienste bereitzustellen. Teil 1: Spring Boot und Docker

Hallo, Habr.

In diesem Artikel möchte ich über meine Erfahrungen bei der Erstellung einer Lernumgebung für Experimente mit Microservices berichten. Wenn ich ein neues Tool lerne, möchte ich es nicht nur auf meinem lokalen Rechner ausprobieren, sondern auch in realistischeren Bedingungen. Daher habe ich beschlossen, eine vereinfachte Microservice-Anwendung zu erstellen, die ich später mit interessanten Technologien erweitern kann. Das Hauptziel des Projekts ist es, funktional so real wie möglich zu sein.

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

  1. Zwei Dienste erstellen – 'Backend' und 'Gateway', diese in Docker-Images verpacken und ihre Zusammenarbeit einrichten.

    Schlagwörter: Java 11, Spring Boot, Docker, Bildoptimierung

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

    Schlagwörter: Kubernetes, GKE, Ressourcenmanagement, Autoskalierung, Geheimnisse

  3. Erstellung eines Charts mit Helm 3 für eine effizientere Clusterverwaltung.

    Schlagwörter: Helm 3, Chart-Deployment

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

    Schlagwörter: Jenkins-Konfiguration, Plugins, separates Konfigurationsrepository

Ich plane, jeden Schritt in einem eigenen Artikel zu behandeln.

Der Fokus dieser Artikelreihe liegt nicht darin, wie man Mikrodienste schreibt, sondern wie man sie in einem einheitlichen System zum Laufen bringt. Auch wenn all diese Themen normalerweise außerhalb der Verantwortung des Entwicklers liegen, denke ich, dass es dennoch hilfreich ist, sie zumindest zu 20 % zu verstehen (die bekanntlich 80 % des Ergebnisses liefern). Einige absolut wichtigen Themen, wie die Sicherheit, werden in diesem Projekt ausgelassen, da der Autor nur begrenzte Kenntnisse hat und das System ausschließlich für den persönlichen Gebrauch erstellt wird. Über Meinungen und konstruktive Kritik freue ich mich sehr.

Erstellung von Mikrodiensten

Die Dienste wurden in Java 11 unter Verwendung von Spring Boot entwickelt. Die Interaktion zwischen den Diensten erfolgt über REST. Das Projekt wird eine minimale Anzahl von Tests beinhalten (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 zu überprüfen, wurde Spring Actuator hinzugefügt. Dies erstellt einen Endpunkt /actuator/health und gibt den Status 200 zurück, wenn der Dienst bereit ist, Datenverkehr zu empfangen, oder 504 im Falle von Problemen. In diesem speziellen Fall handelt es sich um eine eher oberflächliche Überprüfung, da die Dienste sehr einfach sind, und in einem kritischen Szenario wahrscheinlich komplett ausfallen würden, anstatt teilweise funktionsfähig zu bleiben. In echten Systemen kann Actuator jedoch helfen, Probleme zu diagnostizieren, bevor sie von den Benutzern bemerkt werden. Wenn beispielsweise Probleme mit dem Datenbankzugriff auftreten, können wir automatisch reagieren, indem wir fehlerhafte Anfragen an den Dienst stoppen.

Backend-Dienst

Der Backend-Dienst zählt einfach die Anzahl der eingegangenen Anfragen und gibt diese zurück.

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

Der Gateway wird die Anfrage an den Backend-Service weiterleiten und mit den folgenden Informationen ergänzen:

  • Die ID des Gateways. Sie wird benötigt, um anhand der Serverantwort zwischen den einzelnen Gateways unterscheiden zu können.
  • Ein "Geheimnis", das als sehr wichtiges Passwort fungiert (z. B. der Schlüssel für die Sicherung einer wichtigen Cookie).

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 wir:

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

Alles funktioniert. Aufmerksame Leser werden bemerken, dass nichts dagegen spricht, direkt auf das Backend zuzugreifen, bypassing das Gateway (http://localhost:8081/requests). Um das zu beheben, sollten die Dienste in ein Netzwerk integriert werden, während nur das Gateway nach außen sichtbar bleibt.
Außerdem teilen beide Dienste ein Dateisystem, was zur Entstehung von Threads führt, die sich gegenseitig stören können. Es wäre sinnvoll, unsere Microservices zu isolieren. Dies kann erreicht werden, indem man die Anwendungen auf verschiedene Maschinen verteilt (anfänglich kostenintensiv, komplex), virtuelle Maschinen nutzt (ressourcenintensiv, lange Startzeit) oder durch Containerisierung. Erwartungsgemäß wählen wir die dritte Option und Docker verwenden als Werkzeug zur Containerisierung.

Docker

Kurz gesagt, Docker erstellt isolierte Container, jeweils für eine Anwendung. Um Docker zu nutzen, müssen Sie eine Dockerfile schreiben – eine Anleitung zum Erstellen und Ausführen der Anwendung. Anschließend können Sie ein Abbild erstellen, es in ein Abbild-Repository hochladen (Nr. DockerHub) und mit einem einzigen Befehl Ihren Mikrodienst in jeder dockerisierten Umgebung bereitstellen.

Dockerfile

Eine der wichtigsten Eigenschaften eines Abbilds ist seine Größe. Ein kompaktes Abbild wird schneller aus einem entfernten Repository heruntergeladen, belegt weniger Speicherplatz und Ihr Dienst startet schneller. Jedes Abbild wird auf der Grundlage eines Basisabbilds erstellt, und es wird empfohlen, die minimalistischste Option zu wählen. Eine gute Wahl ist Alpine – eine vollständige Linux-Distribution mit minimalen Paketen.

Zum Starten versuchen wir, eine Dockerfile "direkt" zu schreiben (ich sage gleich, dass das eine schlechte Methode ist, machen Sie es nicht so):

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 Basis von Alpine, das bereits mit JDK für den Bau unseres Projekts ausgestattet ist. Mit dem Befehl ADD fügen wir das aktuelle Verzeichnis src zum Image hinzu, setzen es als Arbeitsverzeichnis (WORKDIR) und starten den Build. Der Befehl EXPOSE 8080 signalisiert Docker, dass die Anwendung im Container seinen Port 8080 nutzen wird (dies macht die Anwendung nicht von außen zugänglich, ermöglicht jedoch den Zugriff auf die Anwendung, z. B. von einem anderen Container im selben Docker-Netzwerk).

Um die Services in Images zu verpacken, müssen die Befehle aus dem Stammverzeichnis jedes Projekts ausgeführt werden:

docker image build . -t msvc-backend:1.0.0

Das Ergebnis ist ein Image mit einer Größe von 456 MB (davon entfallen 340 MB auf das Basis-Image JDK). Und das, obwohl die Klassen in unserem Projekt mit den Fingern zu zählen sind. Um die Größe unseres Images zu verringern:

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

Für Neugierige: hier ist ein informativer Artikel über Ansätze zur Reduzierung der Abbildgröße. https://habr.com/ru/company/ruvds/blog/485650/.

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

Das neu erstellte Abbild wiegt nun 77 MB, also ist es um den Faktor 6 geschrumpft. Ganz gut. Danach können die fertigen Abbildungen in ein Abbild-Register hochgeladen werden, damit sie im Internet zum Download bereitstehen.

Gleichzeitiges Ausführen von Diensten in Docker

Zunächst müssen unsere Dienste im selben Netzwerk sein. In Docker gibt es mehrere Netzwerktypen, und wir verwenden den einfachsten darunter – bridge, der es ermöglicht, Container, die auf demselben Host laufen, zu vernetzen. Erstellen wir das Netzwerk mit dem folgenden Befehl:

docker network create msvc-network

Lassen Sie uns als Nächstes den Backend-Container mit dem Namen ‘backend’ und dem Image microservices-backend:1.0.0 starten:

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

Es ist erwähnenswert, dass das bridge-Netzwerk standardmäßig eine Dienstentdeckung für Container über deren Namen bietet. Das bedeutet, dass der Backend-Dienst innerhalb des Docker-Netzwerks unter der 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 Befehl geben wir an, dass wir den Port 80 unseres Hosts auf den Port 8080 des Containers weiterleiten. Die env-Optionen verwenden wir zur Festlegung von Umgebungsvariablen, die automatisch von Spring gelesen werden und die Eigenschaften aus der application.properties überschreiben.

Nach dem Start rufen wir auf http://localhost/ und stellen sicher, dass alles wie im vorherigen Fall funktioniert.

Fazit

Insgesamt haben wir zwei einfache Mikrodienste erstellt, sie in Docker-Container verpackt und gemeinsam auf einer Maschine gestartet. Das resultierende System hat jedoch eine Reihe von Nachteilen:

  • Schlechte Fehlertoleranz – alles läuft auf einem einzigen Server
  • Schlechte Skalierbarkeit – bei zunehmender Last wäre es von Vorteil, zusätzliche Instanzen der Dienste automatisch bereitzustellen und die Last zwischen ihnen zu verteilen.
  • Die Komplexität des Starts – wir mussten mindestens 3 Befehle mit spezifischen Parametern eingeben (zumindest 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, könnte man auch Spring Cloud in Betracht ziehen.ein guter Artikel).

In Im nächsten Abschnitt werde ich erläutern, wie ich Kubernetes konfiguriert und ein Projekt in Google Kubernetes Engine deployt habe.

Quelle: habr.com

Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster