Lär dig hur du distribuerar mikrotjänster. Del 1. Spring Boot och Docker

Lär dig hur du distribuerar mikrotjänster. Del 1. Spring Boot och Docker

Hej Habr.

I den här artikeln vill jag prata om min erfarenhet av att skapa en lärmiljö för att experimentera med mikrotjänster. När jag lärde mig varje nytt verktyg ville jag alltid prova det inte bara på den lokala maskinen, utan också under mer realistiska förhållanden. Därför bestämde jag mig för att skapa en förenklad mikrotjänstapplikation, som senare kan "täckas" med alla möjliga intressanta teknologier. Huvudkravet för projektet är dess maximala funktionella närhet till det verkliga systemet.

Till en början bröt jag upp skapandet av projektet i flera steg:

  1. Skapa två tjänster - 'backend' (backend) och 'gateway' (gateway), packa dem i docker-bilder och ställ in dem för att fungera tillsammans

    Nyckelord: Java 11, Spring Boot, Docker, bildoptimering

  2. Utveckling av Kubernetes-konfiguration och systemdistribution i Google Kubernetes Engine

    Nyckelord: Kubernetes, GKE, resurshantering, autoskalning, hemligheter

  3. Skapa ett diagram med Helm 3 för bättre klusterhantering

    Taggar: Helm 3, sjökortsutbyggnad

  4. Installation av Jenkins och pipeline för automatisk leverans av kod till klustret

    Nyckelord: Jenkins-konfiguration, plugins, separat konfigurationsförråd

Jag planerar att ägna en separat artikel till varje steg.

Fokus i den här artikelserien är inte hur man skriver mikrotjänster, utan hur man får dem att fungera i ett enda system. Även om alla dessa saker vanligtvis ligger utanför utvecklarens ansvar, tror jag att det fortfarande är användbart att vara bekant med dem minst 20% (vilket, som ni vet, ger 80% av resultatet). Vissa ovillkorligt viktiga ämnen, såsom säkerhet, kommer att utelämnas i detta projekt, eftersom författaren inte förstår mycket om att detta system är skapat enbart för personligt bruk. Jag välkomnar alla åsikter och konstruktiv kritik.

Skapar mikrotjänster

Tjänsterna skrevs i Java 11 med Spring Boot. Interaktion mellan tjänster organiseras med REST. Projektet kommer att innehålla ett minimum antal tester (så att det senare finns något att testa i Jenkins). Källkoden för tjänsterna är tillgänglig på GitHub: backend и Inkörsport.

För att kunna kontrollera status för var och en av tjänsterna har en fjäderställdon lagts till deras beroenden. Den kommer att skapa en /aktuator/hälsoslutpunkt och returnera en 200-status om tjänsten är redo att acceptera trafik, eller en 504 om det finns ett problem. I det här fallet är detta en ganska fiktiv kontroll, eftersom tjänsterna är mycket enkla och i händelse av force majeure är det mer sannolikt att de blir helt otillgängliga än att de förblir delvis i drift. Men i verkliga system kan Actuator hjälpa till att diagnostisera ett problem innan användarna börjar slåss om det. Om det till exempel finns problem med att komma åt databasen kan vi automatiskt svara på detta genom att stoppa bearbetningen av förfrågningar med en trasig tjänsteinstans.

Back end-tjänst

Backend-tjänsten kommer helt enkelt att räkna och returnera antalet accepterade förfrågningar.

Controllerkod:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

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

Controllertest:

@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

Gatewayen kommer att vidarebefordra begäran till backend-tjänsten och komplettera den med följande information:

  • gateway-id. Det behövs så att det är möjligt att skilja en instans av gatewayen från en annan genom serversvaret
  • Någon "hemlighet" som kommer att spela rollen som ett mycket viktigt lösenord (numret på krypteringsnyckeln för en viktig cookie)

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

Kontroller:

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

Lansera:

Vi startar backend:

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

Starta gatewayen:

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

Vi kontrollerar:

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

Allt fungerar. En uppmärksam läsare kommer att notera att ingenting hindrar oss från att komma åt backend direkt, förbi gatewayen (http://localhost:8081/requests). För att fixa detta måste tjänsterna kombineras till ett nätverk, och endast gatewayen ska "sticka ut" utanför.
Dessutom delar båda tjänsterna ett filsystem, producerar strömmar och kan vid ett ögonblick börja störa varandra. Det skulle vara trevligt att isolera våra mikrotjänster. Detta kan uppnås genom att distribuera applikationer på olika maskiner (mycket pengar, svårt), använda virtuella maskiner (resurskrävande, lång uppstart) eller använda containerisering. Som förväntat väljer vi det tredje alternativet och Hamnarbetare som ett verktyg för containerisering.

Hamnarbetare

Kort sagt, docker skapar isolerade containrar, en per applikation. För att använda docker måste du skriva en Dockerfil - instruktioner för att bygga och köra programmet. Därefter kan du bygga bilden, ladda upp den till bildregistret (nr. Dockerhub) och distribuera din mikrotjänst i valfri dockningsmiljö med ett kommando.

Dockerfile

En av de viktigaste egenskaperna hos en bild är dess storlek. En kompakt bild laddas ner snabbare från ett fjärrlager, tar mindre plats och din tjänst startar snabbare. Varje bild är byggd på grundval av basbilden, och det rekommenderas att välja det mest minimalistiska alternativet. Ett bra alternativ är Alpine, en komplett Linux-distribution med minimala paket.

Låt oss först försöka skriva en Dockerfil "på pannan" (jag ska genast säga att det här är ett dåligt sätt, gör inte det):

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

Här använder vi en alpinbaserad basbild med JDK redan installerad för att bygga vårt projekt. Med kommandot ADD lägger vi till den aktuella src-katalogen till bilden, markerar den som fungerande (WORKDIR) och startar bygget. EXPOSE 8080-kommandot signalerar docker att applikationen i behållaren kommer att använda sin port 8080 (detta gör inte applikationen tillgänglig från utsidan, men gör att applikationen kan nås, till exempel från en annan behållare i samma dockningsnätverk) .

För att paketera tjänster till bilder måste du köra kommandon från roten av varje projekt:

docker image build . -t msvc-backend:1.0.0

Resultatet är en 456 MB-bild (varav JDK-basbilden upptog 340 MB). Och allt trots att klasserna i vårt projekt kan räknas på ett finger. Så här minskar du storleken på vår bild:

  • Vi använder flerstegsmontering. I det första steget kommer vi att bygga projektet, i det andra steget kommer vi att installera JRE, och i det tredje steget kommer vi att kopiera allt till en ny ren alpin bild. Totalt kommer endast de nödvändiga komponenterna att finnas i den slutliga bilden.
  • Låt oss använda modulariseringen av java. Från och med Java 9 kan du använda jlink-verktyget för att skapa en JRE från bara de moduler du behöver

För den nyfikna, här är en bra artikel om tillvägagångssätt för bildminskning. https://habr.com/ru/company/ruvds/blog/485650/.

Slutlig dockerfil:

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

Vi återskapar bilden, och som ett resultat tappade den 6 gånger sin vikt, vilket uppgick till 77 MB. Inte dåligt. Därefter kan färdiga bilder laddas upp i bildregistret så att dina bilder är tillgängliga för nedladdning från Internet.

Samkörande tjänster i Docker

Till att börja med måste våra tjänster finnas på samma nätverk. Det finns flera typer av nätverk i docker, och vi använder den mest primitiva av dem - bridge, som låter dig nätverka behållare som körs på samma värd. Skapa ett nätverk med följande kommando:

docker network create msvc-network

Starta sedan backend-behållaren med namnet 'backend' med microservices-backend:1.0.0-bilden:

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

Det är värt att notera att bryggnätverket tillhandahåller direktupptäckt tjänster för containrar med deras namn. Det vill säga att backend-tjänsten kommer att vara tillgänglig i dockarnätverket kl http://backend:8080.

Starta gatewayen:

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

I det här kommandot indikerar vi att vi vidarebefordrar port 80 på vår värd till port 8080 på containern. Vi använder env-alternativen för att ställa in miljövariabler som automatiskt läses av spring och åsidosätter egenskaper från application.properties.

Efter start ringer vi http://localhost/ och se till att allt fungerar, som i föregående fall.

Slutsats

Som ett resultat skapade vi två enkla mikrotjänster, paketerade dem i dockningscontainrar och lanserade dem tillsammans på samma maskin. Det resulterande systemet har dock ett antal nackdelar:

  • Dålig feltolerans - allt fungerar för oss på en server
  • Dålig skalbarhet - när belastningen ökar skulle det vara trevligt att automatiskt distribuera ytterligare tjänsteinstanser och balansera belastningen mellan dem
  • Komplexiteten i lanseringen - vi behövde ange minst 3 kommandon och med vissa parametrar (detta är bara för 2 tjänster)

För att åtgärda ovanstående problem finns det ett antal lösningar som Docker Swarm, Nomad, Kubernetes eller OpenShift. Om hela systemet är skrivet i Java kan du titta mot Spring Cloud (bra artikel).

В nästa del Jag kommer att prata om hur jag satte upp Kubernetes och distribuerade projektet till Google Kubernetes Engine.

Källa: will.com

Lägg en kommentar