
Bună, Habr.
În acest articol, vreau să împărtășesc experiența mea în crearea unui mediu de învățare pentru experimente cu microservicii. Atunci când studiez fiecare nou instrument, am dorit întotdeauna să-l încerc nu doar pe mașina locală, ci și în condiții mai realiste. De aceea, am decis să creez o aplicație microservicii simplificată, care ulterior poate fi "imbrăcată" cu diverse tehnologii interesante. Cerința principală pentru proiect este să fie cât mai aproape de funcționalitatea unei sisteme reale.
Inițial, am împărțit crearea proiectului în mai mulți pași:
Crearea a două servicii — ‘backend’ și ‘gateway’, ambalarea acestora în imagini Docker și configurarea colaborării lor
Cuvinte cheie: Java 11, Spring Boot, Docker, optimizarea imaginilor
Cuvinte cheie: Kubernetes, GKE, managementul resurselor, scalarea automată, secrete
Crearea unui chart cu ajutorul Helm 3 pentru o gestionare mai eficientă a clusterului
Cuvinte cheie: Helm 3, desfășurarea chart-ului
Configurarea Jenkins și a pipeline-ului pentru livrarea automată a codului în cluster
Cuvinte cheie: configurarea Jenkins, pluginuri, repository de configurații separate
Fiecărui pas planific să-i dedic un articol separat.
Direcția acestui ciclu de articole nu este de a învăța cum să scrii microservicii, ci cum să le faci să funcționeze într-un sistem unitar. Deși toate aceste lucruri sunt de obicei în afara responsabilității dezvoltatorului, cred că este totuși util să fii familiarizat cu ele măcar în proporție de 20% (ceea ce, după cum se știe, oferă 80% din rezultat). Unele subiecte esențiale, cum ar fi asigurarea securității, vor fi lăsate deoparte în acest proiect, deoarece autorul nu are o cunoaștere profundă în acest domeniu — sistemul este creat exclusiv pentru uz personal. Aștept cu interes orice opinii și critici constructive.
Crearea microserviciilor
Serviciile au fost scrise în Java 11 folosind Spring Boot. Interacțiunea între servicii este organizată prin REST. Proiectul va include un număr minim de teste (pentru a avea ceva de testat în Jenkins). Codul sursă al serviciilor este disponibil pe GitHub: și .
Pentru a putea verifica starea fiecărui serviciu, s-a adăugat Spring Actuator la dependențele sale. Acesta va crea un endpoint /actuator/health și va returna un cod de status 200 dacă serviciul este pregătit să primească trafic sau 504 în caz de probleme. În acest caz, aceasta este o verificare destul de fictivă, deoarece serviciile sunt foarte simple și, în caz de forță majoră, este mai probabil să devină complet indisponibile decât să păstreze o funcționalitate parțială. Dar, în sistemele reale, Actuator poate ajuta la diagnosticarea problemei înainte ca utilizatorii să înceapă să se confrunte cu ea. De exemplu, în cazul unor probleme de acces la baza de date, vom putea reacționa automat, încetând să procesăm solicitările printr-o instanță defectă a serviciului.
Serviciul Backend
Serviciul de backend va număra pur și simplu și va returna numărul de solicitări primite.
Codul controlerului:
@RestController
public class RequestsCounterController {
private final AtomicLong counter = new AtomicLong();
@GetMapping("/requests")
public Long getRequestsCount() {
return counter.incrementAndGet();
}
}Test pentru controler:
@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"));
}
}Serviciul Gateway
Gateway-ul va redirecționa solicitarea către serviciul de backend, adăugând următoarele informații:
- id-ul gateway-ului. Acesta este necesar pentru a putea deosebi o instanță a gateway-ului de alta în funcție de răspunsul serverului
- Un "secret" care va juca rolul unei parole foarte importante (nr. cheia de criptare a unei cookie-uri importante)
Configurarea în application.properties:
backend.url=http://localhost:8081
instance.id=${random.int}
secret="default-secret"Adapter pentru conectarea la 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();
}
}Controlerul:
@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("Numărul solicitărilor %s (gateway %d, secret %s)", backendAdapter.getRequests(), instanceId, secret);
}
}Pornire:
Pornim backend-ul:
./mvnw package -DskipTests
java -Dserver.port=8081 -jar target/microservices-backend-1.0.0.jarPornim gateway-ul:
./mvnw package -DskipTests
java -jar target/microservices-gateway-1.0.0.jarVerificăm:
$ curl http://localhost:8080/
Numărul de solicitări 1 (gateway 38560358, secret "default-secret")Totul funcționează. Cititorul atent va observa că nu avem nicio restricție în a ne conecta direct la backend, ocolind gateway-ul (). Pentru a remedia acest lucru, serviciile trebuie combinate într-o rețea unică, iar exteriorul ar trebui să fie vizibil doar pentru gateway.
De asemenea, ambele servicii împărtășesc același sistem de fișiere, generează fire de execuție și, la un moment dat, pot începe să se deranjeze reciproc. Ar fi bine să izolăm microserviciile noastre. Acest lucru poate fi realizat prin distribuirea aplicațiilor pe mașini diferite (costisitor, complicat), utilizarea mașinilor virtuale (consumatoare de resurse, timp de pornire lung) sau prin containerizare. Așa că, este de la sine înțeles, alegem a treia opțiune și ca instrument pentru containerizare.
Docker
Pe scurt, Docker creează containere izolate, câte unul pentru fiecare aplicație. Pentru a folosi Docker, trebuie să scrii un Dockerfile - o instrucțiune pentru construirea și rularea aplicației. Apoi, poți construi o imagine, o poți încărca în registrul de imagini (nr. ) și cu o singură comandă poți desfășura microserviciul tău în orice mediu containerizat.
Dockerfile
Una dintre cele mai importante caracteristici ale imaginii este dimensiunea acesteia. O imagine compactă se va descărca mai repede dintr-un repo extern, va ocupa mai puțin spațiu și serviciul tău va porni mai repede. Orice imagine este construită pe baza unei imagini de bază și se recomandă alegerea celei mai minimaliste opțiuni. O opțiune bună este Alpine - o distribuție Linux completă cu un minim de pachete.
Pentru început, să încercăm să scriem un Dockerfile "direct" (spun din start că aceasta este o metodă proastă, nu faceți așa):
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"]Aici folosim o imagine de bază bazată pe Alpine cu JDK deja instalat pentru a construi proiectul nostru. Comanda ADD adaugă directorul curent src în imagine, îl marchează ca director de lucru (WORKDIR) și demarează construcția. Comanda EXPOSE 8080 informează Docker că aplicația din container va utiliza portul său 8080 (acest lucru nu va face aplicația accesibilă din exterior, dar va permite accesul la aplicație, de exemplu, din alt container din aceeași rețea Docker).
Pentru a împacheta serviciile în imagini, trebuie să executați comenzile din rădăcina fiecărui proiect:
docker image build . -t msvc-backend:1.0.0Prin urmare, obținem o imagine de 456 MB (dintre care imaginea de bază JDK 340 a ocupat MB). Și toate acestea în condițiile în care clasele din proiectul nostru pot fi numărate pe degete. Pentru a micșora dimensiunea imaginii noastre:
- Folosim o compilare în mai multe etape. În primul pas, vom construi proiectul, în al doilea vom instala JRE, iar în al treilea pas vom copia toate acestea într-o nouă imagine curată Alpine. Astfel, în imaginea finală vor apărea doar componentele necesare.
- Vom folosi modularizarea java. Începând cu Java 9, putem utiliza instrumentul jlink pentru a crea un JRE doar din modulele necesare.
Pentru cei curioși, iată un articol bun despre abordările de micșorare a dimensiunii imaginii. .
Dockerfile-ul final:
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"]Recreem imaginea și, în cele din urmă, aceasta a scăzut cu 6 ori, ajungând la 77 MB. Nu e rău. După aceea, imaginile finale pot fi încărcate în registrul imaginilor, pentru ca imaginile tale să fie disponibile pentru descărcare din internet.
Executarea comună a serviciilor în Docker
Pentru început, serviciile noastre trebuie să fie într-o rețea comună. În Docker există mai multe tipuri de rețele, iar noi folosim cel mai simplu dintre ele — bridge, care permite conectarea containerelor rulate pe același host. Vom crea rețeaua cu următoarea comandă:
docker network create msvc-networkApoi, vom rula containerul backend cu numele ‘backend’ cu imaginea microservices-backend:1.0.0:
docker run -dit --name backend --network msvc-net microservices-backend:1.0.0Merită menționat că rețeaua bridge oferă din cutie descoperirea serviciilor pentru containere în funcție de numele lor. Asta înseamnă că serviciul backend va fi accesibil în interiorul rețelei Docker la adresa .
Pornim gateway-ul:
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În această echipă, specificăm că redirecționăm portul 80 al gazdei noastre către portul 8080 al containerului. Opțiunile env sunt folosite pentru a stabili variabile de mediu, care vor fi citite automat de Spring și vor suprascrie proprietățile din application.properties.
După ce pornim, apelăm și ne asigurăm că totul funcționează, la fel ca în cazul anterior.
Concluzie
În cele din urmă, am creat două microservicii simple, le-am ambalat în containere Docker și le-am pornit împreună pe aceeași mașină. Totuși, sistemul obținut are câteva dezavantaje:
- Rezistență slabă la defecte — totul funcționează pe un singur server
- Scalabilitate redusă — cu creșterea încărcării, ar fi util să desfășurăm automat instanțe suplimentare ale serviciilor și să echilibrăm sarcina între ele
- Complexitate la pornire — a fost necesar să introducem cel puțin 3 comenzi, fiecare cu parametrii specifici (aceasta doar pentru 2 servicii)
Pentru a aborda problemele menționate anterior, există o serie de soluții, precum Docker Swarm, Nomad, Kubernetes sau OpenShift. Dacă întreaga sistem este scris în Java, se poate opta pentru Spring Cloud ().
În vă voi povesti despre cum am configurat Kubernetes și am desfășurat proiectul în Google Kubernetes Engine.
Sursa: habr.com
