Leren microservices implementeren. Deel 1. Spring Boot en Docker

Leren microservices implementeren. Deel 1. Spring Boot en Docker

Hallo, Habr.

In dit artikel wil ik mijn ervaring delen over het creƫren van een leeromgeving voor experimenten met microservices. Bij het leren van elk nieuw hulpmiddel wilde ik het altijd niet alleen op mijn lokale machine, maar ook onder meer realistische omstandigheden uitproberen. Daarom besloot ik een vereenvoudigde microservice-applicatie te creƫren, die later kan worden 'uitgerust' met allerlei interessante technologieƫn. De belangrijkste vereiste voor het project is de maximale functionele nabijheid tot een echt systeem.

In eerste instantie heb ik het creƫren van het project opgedeeld in verschillende stappen:

  1. Twee services creĆ«ren — 'backend' en 'gateway', deze in docker-afbeeldingen verpakken en hun samenwerking instellen

    Trefwoorden: Java 11, Spring Boot, Docker, image-optimalisatie

  2. Ontwikkeling van de Kubernetes-configuratie en implementatie van het systeem in Google Kubernetes Engine

    Trefwoorden: Kubernetes, GKE, resource management, autoscaling, secrets

  3. Een chart maken met behulp van Helm 3 voor efficiƫnter clusterbeheer

    Trefwoorden: Helm 3, chart deployment

  4. Configuratie van Jenkins en een pipeline voor automatische codelevering naar het cluster

    Trefwoorden: Jenkins-configuratie, plugins, aparte configuratierepository

Aan elke stap ben ik van plan een apart artikel te wijden.

De focus van deze reeks artikelen ligt niet op hoe je microservices schrijft, maar hoe je ze laat werken in een samenhangend systeem. Hoewel deze zaken meestal buiten de verantwoordelijkheid van de ontwikkelaar vallen, denk ik dat het nog steeds nuttig is om bekend te zijn met hen, al is het maar voor 20% (die, zoals bekend, 80% van het resultaat oplevert). Enkele zonder twijfel belangrijke onderwerpen, zoals beveiliging, zullen buiten dit project vallen, aangezien de auteur hier weinig van begrijpt en het systeem uitsluitend voor persoonlijk gebruik wordt gecreƫerd. Ik waardeer alle meningen en constructieve kritiek.

Microservices creƫren

De services zijn geschreven in Java 11 met gebruik van Spring Boot. De interactie tussen services is georganiseerd met behulp van REST. Het project zal een minimaal aantal tests bevatten (zodat er later iets te testen is in Jenkins). De broncode van de services is beschikbaar op GitHub: backend en gateway.

Om de status van elk van de services te kunnen controleren, is Spring Actuator aan hun afhankelijkheid toegevoegd. Dit zal een endpoint /actuator/health aanmaken en 200 status retourneren als de service klaar is om verkeer te ontvangen, of 504 in geval van problemen. In dit geval is dit een vrij fictieve controle, aangezien de services erg eenvoudig zijn en bij enige tegenslag ze eerder volledig onbeschikbaar zullen worden dan dat ze gedeeltelijke werking behouden. Maar in echte systemen kan Actuator helpen om problemen te diagnosticeren voordat gebruikers er tegenaan lopen. Bijvoorbeeld, als er problemen zijn met database-toegang, kunnen we automatisch hierop reageren door te stoppen met het verwerken van verzoeken door een defecte service-instantie.

Backend Service

De backend service zal simpelweg het aantal ontvangen verzoeken tellen en retourneren.

Controller Code:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

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

Test voor de 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 Service

De gateway zal verzoeken doorsturen naar de backend service, met de volgende informatie:

  • Gateway-id. Dit is nodig om een instant van de gateway van een andere te onderscheiden aan de hand van het serverantwoord.
  • Een soort 'wachtwoord' dat een zeer belangrijk wachtwoord zal zijn (nr. encryptiesleutel van een belangrijke cookie).

Configuratie in application.properties:

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

Adapter voor communicatie met de 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("Aantal verzoeken %s (gateway %d, secret %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Run:

Backend draaien:

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

We controleren:

$ curl http://localhost:8080/
Aantal verzoeken 1 (gateway 38560358, geheim "default-secret")

Alles werkt. De oplettende lezer zal opmerken dat er niets ons tegenhoudt om direct toegang te krijgen tot de backend, om de gateway heen (http://localhost:8081/requests). Om dit te verhelpen, moeten de services in ƩƩn netwerk worden samengevoegd, en alleen de gateway zou naar buiten moeten "uitsteken".
Ook delen beide services ƩƩn bestandssysteem, genereren ze streams en kunnen ze op een gegeven moment elkaar verstoren. Het zou mooi zijn om onze microservices te isoleren. Dit kan worden bereikt door de applicaties over verschillende machines te verspreiden (veel geld, moeilijk), door gebruik te maken van virtuele machines (zwaar, lange opstarttijd) of door middel van containerisatie. Verwachtbaar kiezen we de derde optie en Docker als een instrument voor containerisatie.

Docker

Kort gezegd, Docker creëert geïsoleerde containers, één voor elke applicatie. Om Docker te gebruiken, moet je een Dockerfile schrijven - een instructie voor het bouwen en starten van de applicatie. Vervolgens kan een afbeelding worden gebouwd, deze kan worden geüpload naar een image registry (nr DockerHub) en je kunt met één commando jouw microservice in elke gecontaineriseerde omgeving implementeren.

Dockerfile

Een van de belangrijkste kenmerken van een afbeelding is de grootte. Een compacte afbeelding wordt sneller gedownload van een externe repository, neemt minder ruimte in en je service start sneller op. Elke afbeelding wordt gebouwd op basis van een basisafbeelding, en het wordt aanbevolen om de meest minimalistische optie te kiezen. Een goede optie is Alpine - een volledige Linux-distributie met een minimum aan pakketten.

Laten we beginnen met het schrijven van een Dockerfile "in het wilde weg" (ik zeg dit meteen, het is een slechte aanpak, doe dit niet):

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 gebruiken we een basisafbeelding op basis van Alpine met al geĆÆnstalleerde JDK voor het bouwen van ons project. Met het commando ADD voegen we de huidige directory src toe aan de afbeelding, markeren deze als werkdirectory (WORKDIR) en starten de build. Het commando EXPOSE 8080 signaleert Docker dat de applicatie in de container zijn poort 8080 zal gebruiken (dit maakt de applicatie niet toegankelijk van buitenaf, maar stelt ons in staat om de applicatie bijvoorbeeld vanuit een andere container binnen hetzelfde Docker-netwerk te benaderen).

Om de services in afbeeldingen te verpakken, moet je de commando's vanuit de hoofdmap van elk project uitvoeren:

docker image build . -t msvc-backend:1.0.0

Hierdoor krijgen we een afbeelding van 456 MB (waarvan de basisafbeelding JDK 340 MB in beslag nam). En dat terwijl het aantal klassen in ons project met de vingers te tellen is. Om de grootte van onze afbeelding te verkleinen:

  • We gebruiken een multi-stage build. In de eerste fase bouwen we het project, in de tweede fase installeren we JRE en in de derde fase kopiĆ«ren we dit alles naar een nieuwe schone Alpine-afbeelding. Uiteindelijk zullen alleen de noodzakelijke componenten in de finale afbeelding komen.
  • We zullen gebruik maken van modularisatie van Java. Sinds Java 9 is het mogelijk om met behulp van het jlink-gereedschap een JRE te creĆ«ren uit alleen de benodigde modules.

Voor de nieuwsgierigen, hier is een goed artikel over benaderingen om de afbeeldingsgrootte te verkleinen. https://habr.com/ru/company/ruvds/blog/485650/.

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

We reconstrueren de afbeelding en deze is uiteindelijk met 6 keer verminderd, tot 77 MB. Niet slecht. Daarna kunnen de kant-en-klare afbeeldingen naar de afbeeldingsregistry worden geüpload, zodat uw afbeeldingen beschikbaar zijn voor download van het internet.

Gezamenlijke uitvoering van services in Docker.

Onze services moeten in hetzelfde netwerk zitten. In Docker zijn er verschillende soorten netwerken en we gebruiken de meest eenvoudige - bridge, die containeren die op dezelfde host draaien in een netwerk verbindt. We creƫren het netwerk met de volgende command:

docker network create msvc-network

Daarna starten we de backend-container met de naam 'backend' met de afbeelding microservices-backend:1.0.0:

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

Het is belangrijk op te merken dat het bridge-netwerk standaard service discovery biedt voor containers op basis van hun namen. Dit betekent dat de backend-service binnen het Docker-netwerk bereikbaar zal zijn op het adres 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 dit team geven we aan dat we poorten van onze host doorsturen van poort 80 naar poort 8080 van de container. We gebruiken de env-opties om omgevingsvariabelen in te stellen die automatisch door Spring worden gelezen en de eigenschappen uit application.properties zullen overschrijven.

Na het starten roepen we http://localhost/ en controleren we of alles werkt zoals in de vorige keer.

Conclusie

Uiteindelijk hebben we twee eenvoudige microservices gemaakt, deze verpakt in Docker-containers en gezamenlijk op ƩƩn machine gestart. Het resulterende systeem heeft echter een aantal nadelen:

  • Slechte fouttolerantie — alles draait op ƩƩn server.
  • Slechte schaalbaarheid — bij een toename van de belasting zou het handig zijn om automatisch extra exemplaren van services te starten en de belasting tussen hen te balanceren.
  • Moeilijkheid om te starten — we moesten minimaal 3 commando's invoeren, en dat met bepaalde parameters (dit is alleen voor 2 services).

Om de bovenstaande problemen op te lossen, zijn er verschillende oplossingen zoals Docker Swarm, Nomad, Kubernetes of OpenShift. Als het hele systeem in Java is geschreven, kan men kijken naar Spring Cloud (een goed artikel).

In in het volgende gedeelte zal ik uitleggen hoe ik Kubernetes configureerde en het project in Google Kubernetes Engine implementeerde.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers šŸ”„ Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster