
Hej, Habr.
I den här artikeln vill jag berätta om min erfarenhet av att skapa en utbildningsmiljö för experiment med mikrotjänster. När jag lärde mig varje nytt verktyg ville jag alltid prova det inte bara på min lokala maskin, 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 skulle kunna "utsmyckas" med olika intressanta teknologier. Huvudkravet för projektet är att det ska vara så funktionellt som möjligt i förhållande till ett faktiskt system.
Inledningsvis delade jag upp skapandet av projektet i flera steg:
Skapa två tjänster – 'backend' och 'gateway', paketera dem i docker-bilder och konfigurera deras samarbete.
Nyckelord: Java 11, Spring Boot, Docker, bildoptimering.
Nyckelord: Kubernetes, GKE, resursförvaltning, autoskalning, hemligheter.
Skapa ett chart med hjälp av Helm 3 för mer effektiv hantering av klustret.
Nyckelord: Helm 3, chartdistribution.
Konfigurera Jenkins och en pipeline för automatisk kodleverans till klustret.
Nyckelord: Jenkins-konfiguration, plugins, separat konfigurationsarkiv.
Jag planerar att ägna ett eget inlägg åt varje steg.
Syftet med denna artikelserie handlar inte om hur man skriver mikrotjänster, utan om hur man får dem att fungera som ett enhetligt system. Även om alla dessa saker vanligtvis ligger utanför utvecklarens ansvar, tror jag att det fortfarande är nyttigt att vara bekant med dem åtminstone till 20% (vilket, som bekant, ger 80% av resultatet). Vissa helt avgörande ämnen, som säkerhet, kommer att lämnas utanför detta projekt, eftersom författaren har lite kunskap om det; systemet skapas uteslutande för privat bruk. Jag ser fram emot alla åsikter och konstruktiv kritik.
Skapa mikrotjänster.
Tjänsterna är skrivna i Java 11 med hjälp av Spring Boot. Inter-tjänsteinteraktionen organiseras med hjälp av REST. Projektet kommer att innehålla ett minimalt antal tester (så att det finns något att testa i Jenkins). Källkoden för tjänsterna är tillgänglig på GitHub: och .
För att kunna kontrollera statusen för varje tjänst har Spring Actuator lagts till som ett beroende. Den kommer att skapa en endpoint /actuator/health och returnera status 200 om tjänsten är redo att ta emot trafik, eller 504 i händelse av problem. I detta fall är det en ganska överflödig kontroll, eftersom tjänsterna är mycket enkla, och vid någon form av nödsituation blir de sannolikt helt otillgängliga snarare än att behålla delvis funktionalitet. Men i verkliga system kan Actuator hjälpa till att diagnostisera problemen innan användarna börjar klaga. Till exempel, vid problem med åtkomst till databasen, kan vi automatiskt reagera på detta genom att sluta behandla förfrågningar från den trasiga tjänstinstansen.
Backend-tjänst
Backend-tjänsten kommer helt enkelt att räkna och returnera antalet mottagna förfrågningar.
Kontrollerkod:
@RestController
public class RequestsCounterController {
private final AtomicLong counter = new AtomicLong();
@GetMapping("/requests")
public Long getRequestsCount() {
return counter.incrementAndGet();
}
}Test på kontroller:
@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-tjänst
Gatewayen kommer att vidarebefordra förfrågningar till backend-tjänsten och komplettera den med följande information:
- gateway-id. Det behövs för att kunna skilja mellan en gateway-instans och en annan utifrån serverns svar
- En "hemlighet" som kommer att fungera som ett mycket viktigt lösenord (nyckel till kryptering av en viktig cookie)
Konfiguration i application.properties:
backend.url=http://localhost:8081
instance.id=${random.int}
secret="default-secret"Adapter för kommunikation med 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();
}
}Kontrollern:
@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("Antal förfrågningar %s (gateway %d, hemlighet %s)", backendAdapter.getRequests(), instanceId, secret);
}
}Starta:
Starta backend:
./mvnw package -DskipTests
java -Dserver.port=8081 -jar target/microservices-backend-1.0.0.jarStarta gateway:
./mvnw package -DskipTests
java -jar target/microservices-gateway-1.0.0.jarKontrollerar:
$ curl http://localhost:8080/
Antal förfrågningar 1 (gateway 38560358, hemlighet "default-secret")Allt fungerar. Den uppmärksamma läsaren kommer att märka att vi inget hindras från att direkt kontakta backend och gå förbi gatewayn (). För att åtgärda detta måste tjänsterna slås ihop i ett nätverk, och endast gatewayn bör synas utåt.
Båda tjänsterna delar också ett filsystem, vilket kan leda till att de stör varandra. Det vore bra att isolera våra mikrotjänster. Detta kan uppnås genom att särplacera applikationerna på olika maskiner (mycket kostnad, svårt), använda virtuella maskiner (resurskrävande, lång start) eller genom containerisering. Vi väljer förväntat det tredje alternativet och som ett verktyg för containerisering.
Docker
Kort sagt, Docker skapar isolerade containrar, en per applikation. För att använda Docker måste man skriva en Dockerfile – en instruktion för att bygga och köra applikationen. Sedan kan man bygga en bild, ladda upp den till ett bildregister (nr ) och med ett kommando distribuera sin mikrotjänst i vilken dockeriserad miljö som helst.
Dockerfile
En av de viktigaste egenskaperna hos en bild är dess storlek. En kompakt bild laddas ner snabbare från det fjärrregister och tar mindre plats, vilket gör att din tjänst startar snabbare. Varje bild byggs på basis av en basbild, och det rekommenderas att välja det mest minimaliska alternativet. Ett bra alternativ är Alpine – en komplett Linux-distribution med minimalt med paket.
För att börja ska vi försöka skriva en Dockerfile "rakt på" (jag säger direkt att detta är ett dåligt sätt, gör det inte):
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 basbild baserad på Alpine med redan installerad JDK för att bygga vårt projekt. Med kommandot ADD lägger vi till den aktuella katalogen src i bilden, markerar den som arbetskatalog (WORKDIR) och kör bygget. Kommandot EXPOSE 8080 signalerar till Docker att applikationen i containern kommer att använda dess port 8080 (detta gör inte applikationen tillgänglig utåt, men gör det möjligt att nå applikationen, till exempel från en annan container i samma Docker-nätverk).
För att paketera tjänsterna i bilder måste man köra kommandona från roten av varje projekt:
docker image build . -t msvc-backend:1.0.0Som ett resultat får vi en bildstorlek på 456 MB (varav den grundläggande bilden JDK 340 upptog MB). Och allt detta med tanke på att klasserna i vårt projekt kan räknas på ena handen. För att minska storleken på vår bild:
- Vi använder en flertrinsbygge. I det första steget bygger vi projektet, i det andra installerar vi JRE, och i det tredje steget kopierar vi allt detta till en ny, ren Alpine-bild. Således kommer endast nödvändiga komponenter att finnas i den slutliga bilden.
- Vi kommer att utnyttja modularisering av java. Från och med Java 9 kan man med hjälp av verktyget jlink skapa en JRE endast från de moduler som behövs.
För de nyfikna, här är en bra artikel om metoder för att minska storleken på bilder. .
Slutlig 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 återskapar bilden, och den minskade i finalen sex gånger, till 77 MB. Inte illa. Efteråt kan vi ladda upp de färdiga bilderna till ett bildregister så att dina bilder är tillgängliga för nedladdning från internet.
Gemensam körning av tjänster i Docker.
För att börja måste våra tjänster finnas i samma nätverk. Det finns flera typer av nätverk i Docker, och vi använder den mest grundläggande av dem — bridge, som låter oss koppla samman containrar som körs på samma värddator. Vi skapar nätverket med följande kommando:
docker network create msvc-networkDärefter startar vi backend-container med namnet ’backend’ med bilden microservices-backend:1.0.0:
docker run -dit --name backend --network msvc-net microservices-backend:1.0.0Det är värt att notera att bridge-nätverket erbjuder service discovery för containrar per deras namn. Det vill säga, backend-tjänsten kommer att vara tillgänglig inom Docker-nätverket på adressen .
Starta 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 det här steget anger vi att vi vidarebefordrar port 80 från vår värd till port 8080 i containern. Alternativen env använder vi för att ställa in miljövariabler som automatiskt kommer att läsas av Spring och åsidosätta egenskaperna från application.properties.
Efter att ha startat anropar vi och försäkrar oss om att allt fungerar som förra gången.
Slutsats
Som resultat har vi skapat två enkla mikrotjänster, packat dem i Docker-containrar och kört dem tillsammans på en maskin. Det finns dock ett antal nackdelar med det här systemet:
- Dålig felåterställning – allt körs på en server
- Dålig skalbarhet – när belastningen ökar skulle det vara bra att automatiskt rulla ut ytterligare instanser av tjänsterna och balansera belastningen mellan dem
- Komplexitet vid start – vi behövde köra minst tre kommandon, varav några med specifika parametrar (detta är bara för två tjänster)
För att åtgärda dessa problem finns det flera lösningar, såsom Docker Swarm, Nomad, Kubernetes eller OpenShift. Om hela systemet är skrivet i Java kan vi titta på Spring Cloud ().
I jag ska berätta hur jag konfigurerade Kubernetes och distribuerade projektet i Google Kubernetes Engine.
Källa: habr.com
