Erfahren Sie, wie Sie Microservices bereitstellen. Teil 1. Spring Boot und Docker

Erfahren Sie, wie Sie Microservices bereitstellen. Teil 1. Spring Boot und Docker

Hallo Habr.

In diesem Artikel möchte ich über meine Erfahrungen bei der Erstellung einer Lernumgebung zum Experimentieren mit Microservices sprechen. Als ich jedes neue Werkzeug kennenlernte, wollte ich es immer nicht nur auf der lokalen Maschine, sondern auch unter realistischeren Bedingungen ausprobieren. Daher habe ich beschlossen, eine vereinfachte Microservice-Anwendung zu erstellen, die später mit allerlei interessanten Technologien „überzogen“ werden kann. Die Hauptanforderung an das Projekt ist die maximale funktionale Nähe zum realen System.

Zunächst habe ich die Erstellung des Projekts in mehrere Schritte unterteilt:

  1. Erstellen Sie zwei Dienste – „backend“ (Backend) und „gateway“ (Gateway), packen Sie sie in Docker-Images und richten Sie sie für die Zusammenarbeit ein

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

  2. Entwicklung der Kubernetes-Konfiguration und Systembereitstellung in Google Kubernetes Engine

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

  3. Erstellen eines Diagramms mit Helm 3 für eine bessere Clusterverwaltung

    Schlagworte: Helm 3, Kartenbereitstellung

  4. Einrichten von Jenkins und Pipeline für die automatische Bereitstellung von Code an den Cluster

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

Ich habe vor, jedem Schritt einen eigenen Artikel zu widmen.

Der Schwerpunkt dieser Artikelserie liegt nicht darauf, wie man Microservices schreibt, sondern wie man sie in einem einzigen System zum Laufen bringt. Obwohl all diese Dinge normalerweise außerhalb der Verantwortung des Entwicklers liegen, halte ich es dennoch für nützlich, mindestens zu 20 % damit vertraut zu sein (was, wie Sie wissen, 80 % des Ergebnisses ausmacht). Einige unbedingt wichtige Themen, wie z. B. Sicherheit, werden in diesem Projekt außen vor gelassen, da der Autor wenig darüber versteht, dass dieses System ausschließlich für den persönlichen Gebrauch erstellt wurde. Ich freue mich über jede Meinung und konstruktive Kritik.

Microservices erstellen

Die Dienste wurden in Java 11 mit Spring Boot geschrieben. Die Interaktion zwischen Diensten wird mithilfe von REST organisiert. Das Projekt wird eine Mindestanzahl an Tests umfassen (damit es später in Jenkins etwas zu testen gibt). Der Quellcode für die Dienste ist auf GitHub verfügbar: Backend и Tor.

Um den Status der einzelnen Dienste überprüfen zu können, wurde ihren Abhängigkeiten ein Spring Actuator hinzugefügt. Es erstellt einen /actuator/health-Endpunkt und gibt den Status 200 zurück, wenn der Dienst bereit ist, Datenverkehr zu akzeptieren, oder 504, wenn ein Problem vorliegt. In diesem Fall handelt es sich um eine eher fiktive Prüfung, da die Dienste sehr einfach sind und im Falle höherer Gewalt die Wahrscheinlichkeit höher ist, dass sie vollständig nicht verfügbar sind, als dass sie teilweise betriebsbereit bleiben. Aber in realen Systemen kann Actuator dabei helfen, ein Problem zu diagnostizieren, bevor Benutzer anfangen, sich darum zu kümmern. Wenn es beispielsweise Probleme beim Zugriff auf die Datenbank gibt, können wir automatisch darauf reagieren, indem wir die Verarbeitung von Anfragen mit einer defekten Dienstinstanz stoppen.

Back-End-Service

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

Controller-Code:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

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

Controller-Test:

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

Service-Gateway

Das Gateway leitet die Anfrage an den Backend-Dienst weiter und ergänzt sie mit den folgenden Informationen:

  • Gateway-ID. Es wird benötigt, damit anhand der Serverantwort eine Instanz des Gateways von einer anderen unterschieden werden kann
  • Ein „Geheimnis“, das die Rolle eines sehr wichtigen Passworts spielt (Nummer des Verschlüsselungsschlüssels eines wichtigen Cookies)

Konfiguration in application.properties:

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

Backend-Adapter:

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

Regler:

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

Start:

Wir starten das Backend:

./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

Wir prüfen:

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

Alles arbeitet. Ein aufmerksamer Leser wird feststellen, dass uns nichts daran hindert, direkt auf das Backend zuzugreifen und das Gateway zu umgehen (http://localhost:8081/requests). Um dies zu beheben, müssen die Dienste zu einem Netzwerk zusammengefasst werden und nur das Gateway sollte nach außen „herausragen“.
Außerdem teilen sich beide Dienste ein Dateisystem, erzeugen Streams und können irgendwann beginnen, sich gegenseitig zu stören. Es wäre schön, unsere Microservices zu isolieren. Dies kann durch die Verteilung von Anwendungen auf verschiedene Maschinen (viel Geld, schwierig), den Einsatz virtueller Maschinen (ressourcenintensiv, langer Start) oder den Einsatz von Containerisierung erreicht werden. Wie erwartet wählen wir die dritte Option und Docker als Werkzeug zur Containerisierung.

Docker

Kurz gesagt: Docker erstellt isolierte Container, einen pro Anwendung. Um Docker verwenden zu können, müssen Sie eine Docker-Datei schreiben – Anweisungen zum Erstellen und Ausführen der Anwendung. Als nächstes können Sie das Image erstellen, es in die Image-Registrierung hochladen (Nr. Docker-Hub) und stellen Sie Ihren Microservice in jeder Docker-Umgebung mit einem Befehl bereit.

Dockerfile

Eine der wichtigsten Eigenschaften eines Bildes ist seine Größe. Ein kompaktes Image wird schneller von einem Remote-Repository heruntergeladen, nimmt weniger Platz ein und Ihr Dienst startet schneller. Jedes Bild wird auf der Grundlage des Basisbildes erstellt und es wird empfohlen, die minimalistischste Option zu wählen. Eine gute Option ist Alpine, eine vollständige Linux-Distribution mit minimalen Paketen.

Versuchen wir zunächst, eine Docker-Datei „auf die Stirn“ zu schreiben (ich sage gleich, dass dies ein schlechter Weg ist, tun Sie es 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 Alpine-basiertes Basisimage mit bereits installiertem JDK, um unser Projekt zu erstellen. Mit dem ADD-Befehl fügen wir das aktuelle src-Verzeichnis zum Image hinzu, markieren es als funktionierend (WORKDIR) und starten den Build. Der Befehl EXPOSE 8080 signalisiert Docker, dass die Anwendung im Container ihren Port 8080 verwenden wird (dies macht die Anwendung nicht von außen zugänglich, ermöglicht aber den Zugriff auf die Anwendung beispielsweise von einem anderen Container im selben Docker-Netzwerk). ).

Um Dienste in Bilder zu packen, müssen Sie Befehle im Stammverzeichnis jedes Projekts ausführen:

docker image build . -t msvc-backend:1.0.0

Das Ergebnis ist ein 456-MB-Image (wovon das Basis-JDK-Image 340 MB belegte). Und das, obwohl die Klassen in unserem Projekt an einem Finger abgezählt werden können. Um die Größe unseres Bildes zu reduzieren:

  • Wir verwenden eine mehrstufige Montage. Im ersten Schritt erstellen wir das Projekt, im zweiten Schritt installieren wir die JRE und im dritten Schritt kopieren wir alles in ein neues, sauberes Alpine-Image. Insgesamt werden im endgültigen Bild nur die notwendigen Komponenten enthalten sein.
  • Lassen Sie uns die Modularisierung von Java verwenden. Ab Java 9 können Sie mit dem jlink-Tool eine JRE nur aus den Modulen erstellen, die Sie benötigen

Für Neugierige gibt es hier einen guten Artikel über Bildreduzierungsansätze. https://habr.com/ru/company/ruvds/blog/485650/.

Endgültige Docker-Datei:

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

Wir erstellen das Image neu und haben dadurch das Sechsfache an Gewicht verloren, was einer Größe von 6 MB entspricht. Nicht schlecht. Anschließend können fertige Bilder in die Bilddatenbank hochgeladen werden, sodass Ihre Bilder im Internet zum Download zur Verfügung stehen.

Gemeinsame Ausführung von Diensten in Docker

Zunächst müssen sich unsere Dienste im selben Netzwerk befinden. Es gibt verschiedene Arten von Netzwerken in Docker, und wir verwenden die primitivste davon – Bridge, mit der Sie Container, die auf demselben Host laufen, vernetzen können. Erstellen Sie ein Netzwerk mit dem folgenden Befehl:

docker network create msvc-network

Starten Sie als Nächstes 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 eine standardmäßige Serviceerkennung für Container anhand ihres Namens bereitstellt. Das heißt, der Backend-Dienst wird im Docker-Netzwerk unter verfügbar sein 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 Port 80 unseres Hosts an Port 8080 des Containers weiterleiten. Wir verwenden die env-Optionen, um Umgebungsvariablen festzulegen, die automatisch von Spring gelesen werden, und Eigenschaften aus application.properties zu überschreiben.

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

Abschluss

Als Ergebnis haben wir zwei einfache Microservices erstellt, sie in Docker-Container verpackt und gemeinsam auf derselben Maschine gestartet. Das resultierende System weist jedoch eine Reihe von Nachteilen auf:

  • Schlechte Fehlertoleranz – bei uns funktioniert alles auf einem Server
  • Schlechte Skalierbarkeit – wenn die Last steigt, wäre es schön, automatisch zusätzliche Dienstinstanzen bereitzustellen und die Last zwischen ihnen auszugleichen
  • Die Komplexität des Starts – wir mussten mindestens 3 Befehle und bestimmte Parameter eingeben (dies gilt nur für 2 Dienste)

Um die oben genannten Probleme zu beheben, gibt es eine Reihe von Lösungen wie Docker Swarm, Nomad, Kubernetes oder OpenShift. Wenn das gesamte System in Java geschrieben ist, können Sie sich für Spring Cloud entscheiden (guter Artikel).

В nächster Teil Ich werde darüber sprechen, wie ich Kubernetes eingerichtet und das Projekt in Google Kubernetes Engine bereitgestellt habe.

Source: habr.com

Kommentar hinzufügen