
Hej, Habr.
I denne artikel vil jeg dele min erfaring med at skabe et læringsmiljø til eksperimenter med mikrotjenester. Når jeg udforsker hvert nyt værktøj, ønsker jeg altid at prøve det ikke kun på min lokale maskine, men også under mere realistiske forhold. Derfor besluttede jeg at oprette en forenklet mikrotjenesteapplikation, som senere kan 'udstyres' med forskellige interessante teknologier. Det primære krav til projektet er dets maksimale funktionelle lighed med et reelt system.
Først delte jeg oprettelsen af projektet op i flere trin:
Oprette to tjenester — 'backend' og 'gateway', pakke dem i docker-billeder og konfigurere deres samarbejde
Nøgleord: Java 11, Spring Boot, Docker, billedoptimering
Nøgleord: Kubernetes, GKE, ressourcehåndtering, autoskalering, secrets
Oprettelse af et chart ved hjælp af Helm 3 for mere effektiv cluster-administration
Nøgleord: Helm 3, chart distribution
Opsætning af Jenkins og pipeline til automatisk levering af kode til clustret
Nøgleord: Jenkins konfiguration, plugins, separat konfigurationsrepository
Jeg planlægger at dedikere en separate artikel til hvert trin.
Fokus i denne artikelcyklus er ikke på hvordan man skriver mikrotjenester, men snarere hvordan man får dem til at fungere i et samlet system. Selvom alle disse emner normalt ligger uden for udviklerens ansvar, tror jeg, det stadig er nyttigt at være bekendt med dem, i det mindste på 20% (som menes at give 80% af resultatet). Nogle bestemt vigtige emner, såsom sikkerhed, vil blive udeladt af dette projekt, da forfatteren har lidt viden om det; systemet oprettes udelukkende til personlig brug. Jeg vil sætte pris på enhver feedback og konstruktiv kritik.
Oprettelse af mikrotjenester
Tjenesterne blev skrevet i Java 11 med brug af Spring Boot. Mellemservicerelationen er organiseret ved hjælp af REST. Projektet vil indeholde et minimalt antal tests (så der senere er noget at teste i Jenkins). Kildekoden til tjenesterne er tilgængelig på GitHub: og .
For the ability to check the status of each service, Spring Actuator has been added to their dependencies. It will create an endpoint /actuator/health and will return a 200 status if the service is ready to accept traffic, or a 504 in case of problems. In this case, it is quite a fictitious check, as the services are very simple, and in the event of some force majeure, they are more likely to become completely unavailable than to maintain partial functionality. However, in real systems, Actuator can help diagnose the problem before users start struggling with it. For example, in case of database access issues, we can automatically respond by stopping requests from a broken service instance.
Backend Service
The backend service will simply count and return the number of requests received.
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"));
}
}Gateway Service
The gateway will redirect requests to the backend service, supplementing it with the following information:
- Gateway ID. This is necessary to distinguish one instance of the gateway from another based on the server's response.
- A "secret" that will serve as a very important password (encryption key number for an important cookie).
Configuration in application.properties:
backend.url=http://localhost:8081
instance.id=${random.int}
secret="default-secret"Adapter for communication with the 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("Number of requests %s (gateway %d, secret %s)", backendAdapter.getRequests(), instanceId, secret);
}
}Aktivering:
Starting the backend:
./mvnw package -DskipTests
java -Dserver.port=8081 -jar target/microservices-backend-1.0.0.jarStarting the gateway:
./mvnw package -DskipTests
java -jar target/microservices-gateway-1.0.0.jarVi tjekker:
$ curl http://localhost:8080/
Antal forespørgsler 1 (gateway 38560358, secret "default-secret")Alt fungerer. Den opmærksomme læser vil bemærke, at der intet forhindrer os i at henvende os til backend direkte uden om gatewayen (). For at løse dette problem skal tjenesterne være samlet i ét netværk, og kun gatewayen skal eksponeres udadtil.
Desuden deler begge tjenester et filsystem, de skaber tråde og kan i ét øjeblik begynde at forstyrre hinanden. Det ville være en god idé at isolere vores mikrotjenester. Dette kan opnås ved at sprede applikationerne på forskellige maskiner (meget dyrt, svært), bruge virtuelle maskiner (ressourcekrævende, lang opstart) eller ved at bruge containerisering. Som forventet vælger vi den tredje mulighed og som værktøj til containerisering.
Docker
Kort sagt, så opretter Docker isolerede containere, én pr. applikation. For at bruge Docker skal der skrives en Dockerfile — en instruktion til bygning og kørsel af applikationen. Derefter kan man samle et billede, uploade det til et billedregister (№ ) og med én kommando implementere sin mikrotjeneste i ethvert dockeriseret miljø.
Dockerfile
En af de vigtigste egenskaber ved et billede er dets størrelse. Et kompakt billede downloades hurtigere fra det eksterne repository, optager mindre plads, og din tjeneste starter hurtigere. Enhver billede bygges på baggrund af et grundbillede, og det anbefales at vælge den mest minimalistiske variant. Et godt valg er Alpine — en fuld Linux-distribution med minimum af pakker.
For at begynde prøver vi at skrive en Dockerfile "direkte" (jeg siger straks, at dette er en dårlig metode, gør ikke sådan):
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 grundbillede baseret på Alpine med allerede installeret JDK til at bygge vores projekt. Med ADD-kommandoen tilføjer vi den nuværende mappe src til billedet, markerer den som arbejdsmappen (WORKDIR) og starter bygningen. EXPOSE 8080-kommandoen signalerer til Docker, at applikationen i containeren vil bruge port 8080 (dette gør ikke applikationen tilgængelig udefra, men tillader at henvende sig til applikationen, f.eks. fra en anden container i det samme Docker-netværk).
For at pakke tjenesterne i billeder skal kommandoerne udføres fra roden af hvert projekt:
docker image build . -t msvc-backend:1.0.0Som vi kan se, får vi et billede, der fylder 456 MB (hvoraf den grundlæggende JDK-billed er 340 MB). Og alt dette, selvom vi kun kan tælle klasserne i vores projekt på fingrene. For at reducere størrelsen på vores billede:
- Vi bruger multi-stage build. I første trin bygger vi projektet, i andet trin installerer vi JRE, og i det tredje trin kopierer vi alt dette til et nyt rent Alpine-billede. I det endelige billede vil der kun være de nødvendige komponenter.
- Vi vil drage fordel af Java-modularisering. Fra og med Java 9 kan vi ved hjælp af værktøjet jlink oprette en JRE kun ud fra de nødvendige moduler.
Til de nysgerrige, her er en god artikel om metoder til at reducere størrelse på billeder. .
Det endelige 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"]Vi genopretter billedet, og det er efterfølgende blevet 6 gange mindre, kun 77 MB. Ikke dårligt. Derefter kan de færdige billeder uploades til et billedarkiv, så dine billeder er tilgængelige for download fra internettet.
Fælles kørsel af tjenester i Docker
For det første skal vores tjenester være på det samme netværk. I Docker findes der flere typer netværk, og vi bruger den mest primitive af dem – bridge, som gør det muligt at samle containere, der kører på samme vært, i et netværk. Vi opretter et netværk med følgende kommando:
docker network create msvc-networkVi starter derefter backend-containeren med navnet ‘backend’ med billedet microservices-backend:1.0.0:
docker run -dit --name backend --network msvc-net microservices-backend:1.0.0Det er værd at bemærke, at bridge-netværket automatisk giver serviceopdagelse for containere efter deres navne. Det vil sige, at backend-tjenesten vil være tilgængelig inden for Docker-netværket på adressen .
Starting the gateway:
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.0I dette kommando angiver vi, at vi videresender 80-porten på vores vært til 8080-porten på containeren. Vi bruger env-mulighederne til at opsætte miljøvariabler, der automatisk vil blive læst af Spring og overskrive indstillinger fra application.properties.
Efter lanceringen kalder vi og sikrer os, at alt fungerer, som det gjorde sidste gang.
Konklusion
Resultatet er, at vi har oprettet to enkle mikrotjenester, pakket dem i Docker-containere og kørt dem sammen på én maskine. Det resulterende system har dog nogle ulemper:
- Dårlig fejltolerance — alt kører på én server
- Dårlig skalerbarhed — ved øget belastning ville det være fordelagtigt at automatisk implementere yderligere instanser af tjenesterne og balancere belastningen imellem dem
- Sværhedsgraden ved opstart — vi skulle indtaste mindst 3 kommandoer, hver med specifikke parametre (dette kun for 2 tjenester)
For at løse de ovenstående problemer findes der en række løsninger som Docker Swarm, Nomad, Kubernetes eller OpenShift. Hvis hele systemet er skrevet i Java, kan man se mod Spring Cloud ().
I jeg vil fortælle om, hvordan jeg konfigurerede Kubernetes og deployede projektet i Google Kubernetes Engine.
Kilde: habr.com
