Lær, hvordan du implementerer mikrotjenester. Del 1. Spring Boot og Docker

Lær, hvordan du implementerer mikrotjenester. Del 1. Spring Boot og Docker

Hej Habr.

I denne artikel vil jeg fortælle om min erfaring med at skabe et læringsmiljø til at eksperimentere med mikrotjenester. Når jeg lærte hvert nyt værktøj, ville jeg altid prøve det ikke kun på den lokale maskine, men også under mere realistiske forhold. Derfor besluttede jeg at lave en forenklet mikroserviceapplikation, som senere kan "dækkes" med alle mulige interessante teknologier. Hovedkravet til projektet er dets maksimale funktionelle nærhed til det virkelige system.

I første omgang delte jeg oprettelsen af ​​projektet op i flere trin:

  1. Opret to tjenester - 'backend' (backend) og 'gateway' (gateway), pak dem ind i docker-billeder og sæt dem op til at arbejde sammen

    Nøgleord: Java 11, Spring Boot, Docker, billedoptimering

  2. Udvikling af Kubernetes-konfiguration og systemimplementering i Google Kubernetes Engine

    Nøgleord: Kubernetes, GKE, ressourcestyring, autoskalering, hemmeligheder

  3. Oprettelse af et diagram med Helm 3 for bedre klyngestyring

    Tags: Helm 3, diagramimplementering

  4. Opsætning af Jenkins og pipeline til automatisk levering af kode til klyngen

    Nøgleord: Jenkins-konfiguration, plugins, separat konfigurationslager

Jeg planlægger at afsætte en separat artikel til hvert trin.

Fokus i denne serie af artikler er ikke, hvordan man skriver mikrotjenester, men hvordan man får dem til at fungere i et enkelt system. Selvom alle disse ting normalt ligger uden for udviklerens ansvar, tror jeg, at det stadig er nyttigt at være bekendt med dem mindst 20% (hvilket, som du ved, giver 80% af resultatet). Nogle ubetinget vigtige emner, såsom sikkerhed, vil blive udeladt af dette projekt, da forfatteren ikke forstår meget om, at dette system er skabt udelukkende til personlig brug. Jeg glæder mig over enhver mening og konstruktiv kritik.

Oprettelse af mikrotjenester

Tjenesterne blev skrevet i Java 11 ved hjælp af Spring Boot. Interservice interaktion organiseres ved hjælp af REST. Projektet vil indeholde et minimum antal tests (så der senere er noget at teste i Jenkins). Kildekoden til tjenesterne er tilgængelig på GitHub: bagende и Gateway.

For at kunne kontrollere status for hver af tjenesterne er der tilføjet en fjederaktuator til deres afhængigheder. Den vil oprette et /aktuator/sundhedsslutpunkt og returnere en 200-status, hvis tjenesten er klar til at acceptere trafik, eller 504, hvis der er et problem. I dette tilfælde er dette en ret fiktiv kontrol, da tjenesterne er meget enkle, og i tilfælde af force majeure er der større sandsynlighed for, at de bliver fuldstændig utilgængelige end forbliver delvist operationelle. Men i rigtige systemer kan Actuator hjælpe med at diagnosticere et problem, før brugere begynder at slås om det. For eksempel, hvis der er problemer med at få adgang til databasen, kan vi automatisk reagere på dette ved at stoppe behandlingen af ​​anmodninger med en ødelagt tjenesteinstans.

Back end service

Backend-tjenesten vil blot tælle og returnere antallet af accepterede anmodninger.

Controller kode:

@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

Gatewayen videresender anmodningen til backend-tjenesten og supplerer den med følgende information:

  • gateway-id. Det er nødvendigt, så det er muligt at skelne en forekomst af gatewayen fra en anden ved hjælp af serversvaret
  • En eller anden "hemmelighed", der vil spille rollen som en meget vigtig adgangskode (nummeret på krypteringsnøglen til en vigtig 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();
    }
}

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("Number of requests %s (gateway %d, secret %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Lancering:

Vi starter backend:

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

Start af gatewayen:

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

Vi tjekker:

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

Alt fungerer. En opmærksom læser vil bemærke, at intet forhindrer os i at få direkte adgang til backend og omgå gatewayen (http://localhost:8081/requests). For at rette op på dette skal tjenesterne kombineres til ét netværk, og kun gatewayen skal "stikke ud" udenfor.
Begge tjenester deler også ét filsystem, producerer streams og kan på et tidspunkt begynde at forstyrre hinanden. Det ville være rart at isolere vores mikrotjenester. Dette kan opnås ved at distribuere applikationer på forskellige maskiner (mange penge, vanskeligt), ved at bruge virtuelle maskiner (ressourcekrævende, lang opstart) eller bruge containerisering. Som forventet vælger vi den tredje mulighed og Docker som et værktøj til containerisering.

Docker

Kort sagt, docker skaber isolerede containere, en pr. applikation. For at bruge docker skal du skrive en Dockerfile - instruktioner til at bygge og køre programmet. Dernæst kan du bygge billedet, uploade det til billedregistret (nr. Dockerhub) og implementer din mikrotjeneste i et hvilket som helst dockeriseret miljø med én kommando.

Dockerfil

En af de vigtigste egenskaber ved et billede er dets størrelse. Et kompakt billede downloades hurtigere fra et fjernlager, fylder mindre, og din tjeneste starter hurtigere. Ethvert billede er bygget på basis af basisbilledet, og det anbefales at vælge den mest minimalistiske mulighed. En god mulighed er Alpine, en komplet Linux-distribution med minimale pakker.

Til at begynde med, lad os prøve at skrive en Dockerfile "på panden" (jeg siger med det samme, at dette er en dårlig måde, lad være med at gøre 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"]

Her bruger vi et alpinbaseret basisbillede med JDK allerede installeret til at bygge vores projekt. Med ADD-kommandoen tilføjer vi det aktuelle src-bibliotek til billedet, markerer det som fungerende (WORKDIR) og starter opbygningen. EXPOSE 8080-kommandoen signalerer til docker, at applikationen i containeren vil bruge sin port 8080 (dette vil ikke gøre applikationen tilgængelig udefra, men vil tillade, at applikationen kan tilgås, for eksempel fra en anden container på det samme docker-netværk ).

For at pakke tjenester til billeder skal du køre kommandoer fra roden af ​​hvert projekt:

docker image build . -t msvc-backend:1.0.0

Resultatet er et 456 MB billede (hvoraf basis JDK-billedet optog 340 MB). Og alt på trods af, at klasserne i vores projekt kan tælles på en finger. Sådan reducerer du størrelsen på vores billede:

  • Vi bruger multi-trin montage. I det første trin vil vi bygge projektet, i det andet trin vil vi installere JRE, og i det tredje trin vil vi kopiere det hele til et nyt rent Alpine-billede. I alt vil kun de nødvendige komponenter være på det endelige billede.
  • Lad os bruge modularisering af java. Fra og med Java 9 kan du bruge jlink-værktøjet til at oprette en JRE fra netop de moduler, du har brug for

For den nysgerrige er her en god artikel om billedreduktionstilgange. https://habr.com/ru/company/ruvds/blog/485650/.

Endelig 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 genskaber billedet, og som et resultat tabte det 6 gange sin vægt, svarende til 77 MB. Ikke dårligt. Derefter kan færdige billeder uploades til billedregistret, så dine billeder er tilgængelige til download fra internettet.

Samkørende tjenester i Docker

Til at begynde med skal vores tjenester være på samme netværk. Der er flere typer netværk i docker, og vi bruger den mest primitive af dem - bridge, som giver dig mulighed for at netværke containere, der kører på den samme vært. Opret et netværk med følgende kommando:

docker network create msvc-network

Start derefter backend-beholderen med navnet 'backend' med microservices-backend:1.0.0-billedet:

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

Det er værd at bemærke, at bronetværket leverer ud af boksen serviceopdagelse for containere ved deres navne. Det vil sige, at backend-tjenesten vil være tilgængelig inde i docker-netværket kl http://backend:8080.

Start af 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 denne kommando angiver vi, at vi videresender port 80 på vores vært til port 8080 på containeren. Vi bruger env-indstillingerne til at indstille miljøvariabler, der automatisk læses af spring og tilsidesætter egenskaber fra application.properties.

Efter start ringer vi http://localhost/ og sørg for, at alt fungerer, som i det foregående tilfælde.

Konklusion

Som et resultat skabte vi to simple mikrotjenester, pakkede dem i docker-containere og lancerede dem sammen på den samme maskine. Det resulterende system har dog en række ulemper:

  • Dårlig fejltolerance - alt fungerer for os på én server
  • Dårlig skalerbarhed - når belastningen stiger, ville det være rart automatisk at implementere yderligere serviceforekomster og afbalancere belastningen mellem dem
  • Kompleksiteten af ​​lanceringen - vi skulle indtaste mindst 3 kommandoer og med visse parametre (dette er kun for 2 tjenester)

For at løse ovenstående problemer er der en række løsninger såsom Docker Swarm, Nomad, Kubernetes eller OpenShift. Hvis hele systemet er skrevet i Java, kan du se mod Spring Cloud (god artikel).

В næste del Jeg vil tale om, hvordan jeg konfigurerede Kubernetes og implementerede projektet til Google Kubernetes Engine.

Kilde: www.habr.com

Tilføj en kommentar