
Hei, Habr.
I denne artikkelen vil jeg dele min erfaring med å opprette et læringsmiljø for eksperimentering med mikrotjenester. Når jeg lærte meg hvert nye verktøy, ønsket jeg alltid å prøve det ikke bare på min lokale maskin, men også under mer realistiske forhold. Derfor bestemte jeg meg for å lage en forenklet mikrotjeneste-applikasjon som senere kan "pynte" seg med forskjellige spennende teknologier. Hovedkravet til prosjektet er at det skal være så funksjonelt nært en ekte system som mulig.
Opprinnelig delte jeg opprettelsen av prosjektet inn i flere trinn:
Lage to tjenester – ‘backend’ og ‘gateway’, pakke dem inn i docker-bilder og sette opp samarbeid mellom dem.
Nøkkelord: Java 11, Spring Boot, Docker, bildeoptimalisering.
Nøkkelord: Kubernetes, GKE, ressursforvaltning, automatisk skalering, hemmeligheter.
Opprette et chart med Helm 3 for mer effektiv styring av klyngen.
Nøkkelord: Helm 3, chart-distribusjon.
Sette opp Jenkins og pipeline for automatisk levering av kode til klyngen.
Nøkkelord: Jenkins-konfigurasjon, plugins, separat konfigurasjonsrepository.
Hvert trinn planlegger jeg å bruke en egen artikkel på.
Fokuset for denne artikkelserien er ikke på hvordan man skriver mikrotjenester, men på hvordan man får dem til å fungere i et samlet system. Selv om alle disse tingene vanligvis ligger utenfor utviklerens ansvar, tror jeg det er nyttig å være kjent med dem, om ikke mer enn 20% (som kjent gir 80% av resultatet). Noen absolutte viktige emner, som sikkerhet, vil bli utelatt fra dette prosjektet, ettersom forfatteren har lite kunnskap om dette; systemet er laget utelukkende for personlig bruk. Jeg ser frem til meninger og konstruktiv kritikk.
Oppretting av mikrotjenester.
Tjenestene ble skrevet i Java 11 med bruk av Spring Boot. Tjenestekommunikasjon er organisert ved hjelp av REST. Prosjektet vil inkludere et minimum av tester (slik at det er noe å teste i Jenkins). Kildekoden til tjenestene er tilgjengelig på GitHub: og .
For being able to check the status of each service, Spring Actuator has been added to their dependencies. It will create an endpoint /actuator/health and return a 200 status if the service is ready to accept traffic, or a 504 in case of issues. In this case, it is quite a superficial check, as the services are very simple, and in an emergency, 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, if there are issues accessing the database, we can automatically react to this by stopping the requests handled by the broken instance of the service.
Backend Service
The backend service will simply count and return the number of received requests.
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, adding the following information:
- Gateway ID. This is needed to differentiate one gateway instance from another based on the server's response.
- A "secret" that will play the role of a very important password (key for encrypting 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();
}
}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);
}
}docker-compose -f sonar.yml up
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 sjekker:
$ curl http://localhost:8080/
Antall forespørselar 1 (gateway 38560358, secret "default-secret")Alt fungerer. Oppmerksomme lesere vil legge merke til at det ikke er noe som hindrer oss i å kontakte backend direkte, omgå gateway (). For å fikse dette, må tjenestene slås sammen i et nettverk, og bare gatewayen bør stikke "ut".
I tillegg deler begge tjenestene et felles filsystem, oppretter tråder, og på et tidspunkt kan de begynne å forstyrre hverandre. Det ville vært bra å isolere mikrotjenestene våre. Dette kan oppnås ved å spre applikasjonene på forskjellige maskiner (mye penger, komplisert), bruke virtuelle maskiner (ressurskrevende, lang oppstart) eller ved hjelp av containerisering. Som forventet velger vi det tredje alternativet og som verktøy for containerisering.
Docker
Kort fortalt, så oppretter Docker isolerte containere, én for hver applikasjon. For å bruke Docker, må du skrive en Dockerfile — en instruksjon for å bygge og kjøre applikasjonen. Deretter kan du bygge et bilde, laste det opp til et bilde-register (nr ) og med én kommando distribuere mikrotjenesten din i et hvilket som helst dockerisert miljø.
Dockerfile
En av de viktigste egenskapene ved et bilde er størrelsen. Et kompakt bilde lastes raskere ned fra et eksternt repository, tar mindre plass, og tjenesten din starter raskere. Ethvert bilde bygges på grunnlag av et basisbilde, og det anbefales å velge den mest minimalistiske varianten. Et godt alternativ er Alpine — et komplett Linux-distribusjon med minimum av pakker.
La oss begynne med å skrive en Dockerfile "rett ut" (jeg sier med en gang at dette er en dårlig måte, ikke gjør 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 bruker vi et basisbilde basert på Alpine med allerede installert JDK for å bygge prosjektet vårt. Med kommandoen ADD legger vi til mappen src i bildet, setter den som arbeidskatalog (WORKDIR) og starter byggingen. Kommandoen EXPOSE 8080 signaliserer til Docker at applikasjonen i containeren vil bruke port 8080 (det gjør ikke applikasjonen tilgjengelig utenfra, men tillater oss å kontakte applikasjonen fra for eksempel en annen container i samme Docker-nettverk).
For å pakke tjenestene i bilder må du utføre kommandoene fra roten av hvert prosjekt:
docker image build . -t msvc-backend:1.0.0Resultatet er et bilde på 456 MB (hvorav den grunnleggende JDK-bildet tok MB). Og alt dette til tross for at klassene i prosjektet vårt kan telles på fingrene. For å redusere størrelsen på bildet vårt:
- Vi bruker en flertrinnsbygging. I det første trinnet vil vi bygge prosjektet, i det andre vil vi installere JRE, og i det tredje trinnet vil vi kopiere alt dette til et nytt rent Alpine-bilde. Til slutt vil kun nødvendige komponenter være i det endelige bildet.
- Vi vil bruke Java-modularisering. Fra og med Java 9 kan vi bruke verktøyet jlink til å lage en JRE kun fra de nødvendige modulene.
For de nysgjerrige, her er en god artikkel om tilnærminger for å redusere størrelsen på bildet. .
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 oppretter bildet på nytt, og det har til slutt krympet med 6 ganger, til 77 MB. Ikke verst. Etterpå kan de ferdige bildene lastes opp til bilde-registeret, slik at bildene dine er tilgjengelige for nedlasting fra internett.
Samsøkt kjøring av tjenester i Docker
Først må tjenestene våre være i samme nettverk. Det finnes flere typer nettverk i Docker, og vi bruker den enkleste av dem — bridge, som lar containerne som kjører på samme vert være koblet i et nettverk. Vi oppretter nettverket med følgende kommando:
docker network create msvc-networkVidere vil vi starte backend-containeren med navnet 'backend' med bildet microservices-backend:1.0.0:
docker run -dit --name backend --network msvc-net microservices-backend:1.0.0Det er verdt å merke seg at bridge-nettverket tilbyr service discovery for containerne etter navn. Det vil si at backend-tjenesten vil være tilgjengelig innen Docker-nettverket 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 denne kommandoen angir vi at vi videresender port 80 fra vertsdatamaskinen til port 8080 i containeren. Vi bruker env-alternativene for å sette miljøvariabler som automatisk vil bli lest opp av Spring og overskrive egenskaper fra application.properties.
Etter oppstarten kaller vi og sørger for at alt fungerer som i forrige tilfelle.
Konklusjon
Til slutt har vi laget to enkle mikrotjenester, pakket dem i Docker-containere og startet dem sammen på én maskin. Det er imidlertid noen ulemper med det systemet vi har fått:
- Dårlig feilresiliens — alt fungerer på én server
- Dårlig skalerbarhet — ved økt belastning ville det vært fint å automatisk rulle ut flere instanser av tjenestene og balansere belastningen mellom dem
- Problematisk oppstart — vi måtte skrive inn minst 3 kommandoer, med bestemte parametere (dette bare for 2 tjenester)
For å løse de nevnte problemene finnes det flere løsninger, som Docker Swarm, Nomad, Kubernetes eller OpenShift. Hvis hele systemet er skrevet i Java, kan man se mot Spring Cloud ().
I jeg vil fortelle om hvordan jeg satte opp Kubernetes og distribuerte prosjektet i Google Kubernetes Engine.
Kilde: habr.com
