Učimo kako razvijati mikroslužbe. Dio 1. Spring Boot i Docker

Učimo kako razvijati mikroslužbe. Dio 1. Spring Boot i Docker

Pozdrav, Habr.

U ovom članku želim podijeliti svoje iskustvo u stvaranju nastavnog okruženja za eksperimentiranje s mikroservisima. Kada učim svaki novi alat, uvijek sam želio isprobati ga ne samo na lokalnom računalu, već i u realnijim uvjetima. Stoga sam odlučio stvoriti pojednostavljenu mikroservisnu aplikaciju koja će se kasnije moći "opremiti" raznim zanimljivim tehnologijama. Glavni zahtjev za projekt je maksimalna funkcionalna bliskost s pravim sustavom.

Isprva sam podijelio stvaranje projekta na nekoliko koraka:

  1. Stvaranje dva servisa – 'backend' i 'gateway', pakiranje u docker slike i postavljanje njihove međusobne suradnje

    Ključne riječi: Java 11, Spring Boot, Docker, optimizacija slika

  2. Razvoj Kubernetes konfiguracije i deploy sustava u Google Kubernetes Engine

    Ključne riječi: Kubernetes, GKE, upravljanje resursima, automatsko skaliranje, tajne

  3. Izrada charta pomoću Helm 3 za učinkovitije upravljanje klasterom

    Ključne riječi: Helm 3, implementacija charta

  4. Postavljanje Jenkinsa i pipelinea za automatsko isporučivanje koda u klaster

    Ključne riječi: konfiguracija Jenkinsa, dodaci, odvojeni repozitorij konfiguracija

Svakom koraku planiram posvetiti poseban članak.

Usmjerenost ovog ciklusa članaka nije kako napisati mikroservise, već kako ih učiniti da rade unutar jedinstvenog sustava. Iako su sve te stvari obično izvan odgovornosti programera, mislim da je ipak korisno biti upoznat s njima barem 20% (koji, kako se zna, donosi 80% rezultata). Neki od neizostavnih tema, kao što su sigurnost, će biti izostavljeni iz ovog projekta, jer autor malo razumije, sustav se stvara isključivo za osobnu upotrebu. Bit ću zahvalan na svim mišljenjima i konstruktivnoj kritici.

Stvaranje mikroservisa

Servisi su napisani na Java 11 uz korištenje Spring Boota. Međuservisna interakcija organizirana je korištenjem REST-a. Projekt će uključivati minimalnu količinu testova (da bi se kasnije imalo što testirati u Jenkinsi). Izvorni kod servisa dostupan je na GitHubu: backend i gateway.

Kako bismo imali mogućnost provjeriti stanje svakog od servisa, njegovim zavisnostima je dodat Spring Actuator. On će stvoriti krajnju točku \/actuator\/health i vraćati status 200 ako je servis spreman za primanje prometa, ili 504 u slučaju problema. U ovom slučaju ovo je prilično lažna provjera, budući da su servisi vrlo jednostavni, a u slučaju nekog nepredviđenog događaja vjerojatnije je da će postati potpuno nedostupni nego zadržati djelomičnu funkcionalnost. No, u stvarnim sustavima Actuator može pomoći u dijagnosticiranju problema prije nego što korisnici počnu imati poteškoće. Na primjer, u slučaju problema s pristupom bazi podataka, možemo automatski reagirati na to tako da prekinemo obradu zahtjeva putem pokvarenog primjera servisa.

Backend servis

Backend servis će jednostavno brojati i vraćati broj primljenih zahtjeva.

Kod kontrolera:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

    @GetMapping("\/requests")
    public Long getRequestsCount() {
        return counter.incrementAndGet();
    }
}

Test za kontroler:

@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 servis

Gateway će preusmjeravati zahtjev backend servisu, dodajući mu sljedeće informacije:

  • id gatewaya. Potreban je za razlikovanje jednog primjera gatewaya od drugog po odgovoru servera.
  • Neki "tajni" ključ koji će igrati ulogu vrlo važnog lozinka (br. ključa šifrirane važne kolačiće).

Konfiguracija u application.properties:

backend.url=http:\/\/localhost:8081
instance.id=${random.int}
secret="default-secret"

Adapter za komunikaciju s backendom:

@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();
    }
}

Kontroler:

@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("Broj zahtjeva %s (gateway %d, tajna %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Pokretanje:

Pokrećemo backend:

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

Pokrećemo gateway:

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

Provjeravamo:

$ curl http://localhost:8080/
Broj zahtjeva 1 (gateway 38560358, tajna "default-secret")

Sve radi. Pažljiv čitatelj primijetit će da nas ništa ne sprječava da se izravno obratimo backendu zaobišavši gateway (http://localhost:8081/requests). Da bismo to ispravili, usluge moraju biti spojene u jednu mrežu, a vani "izgledati" mora samo gateway.
Također, obje usluge dijele jedan datotečni sustav, stvaraju procese i u jednom trenutku mogu početi ometati jedna drugu. Bilo bi dobro izolirati naše mikroservise. To možemo postići raspodjelom aplikacija po različitim strojevima (mnogo novca, složeno), korištenjem virtualnih strojeva (resursno zahtjevno, dugotrajno pokretanje) ili korištenjem kontejnerizacije. Očekivano biramo treću opciju i Docker kao alat za kontejnerizaciju.

Docker

Ukratko, Docker stvara izolirane kontejnere, po jedan na aplikaciju. Da bismo koristili Docker, potrebno je napisati Dockerfile — upute za izgradnju i pokretanje aplikacije. Zatim možemo izgraditi sliku, učitati je u registar slika (№ DockerHub) i jednim naredbom pokrenuti svoj mikroservis u bilo kojem dockeriziranom okruženju.

Dockerfile

Jedna od najvažnijih karakteristika slike — je njezina veličina. Kompaktna slika brže će se preuzeti s udaljenog repozitorija, zauzeće manje prostora, a vaša usluga brže će se pokrenuti. Svaka slika gradi se na temelju osnovne slike, i preporučuje se odabrati najminimalniju opciju. Dobar izbor je Alpine — potpuni distribucijski Linux s minimumom paketa.

Za početak, pokušajmo napisati Dockerfile "u potpunosti" (odmah ću reći da je to loš način, nemojte tako raditi):

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"]

Ovdje koristimo osnovnu sliku temeljenu na Alpine s već instaliranim JDK za izgradnju našeg projekta. Naredbom ADD dodajemo u sliku trenutni direktorij src, označavamo ga kao radni (WORKDIR) i pokrećemo izgradnju. Naredba EXPOSE 8080 signalizira Dockeru da će aplikacija u kontejneru koristiti njegov port 8080 (to neće učiniti aplikaciju dostupnom izvana, ali će omogućiti pristup aplikaciji, na primjer, iz drugog kontejnera u istoj Docker mreži).

Da bismo usluge zapakirali u slike, potrebno je izvršiti naredbe iz korijena svakog projekta:

docker image build . -t msvc-backend:1.0.0

Kao rezultat dobivamo sliku veličine 456 MB (od kojih je osnovna slika JDK 340 zauzela MB). I sve to, dok je klasa u našem projektu prebrojivo na prste. Kako bismo smanjili veličinu naše slike:

  • Koristimo višestupanjsko sastavljanje. U prvom koraku prikupljamo projekt, u drugom instaliramo JRE, a trećim kopiramo sve to u novu čistu Alpine sliku. Tako da će u konačnoj slici biti samo potrebne komponente.
  • Iskoristit ćemo modularizaciju java. Počevši od Jave 9, moguće je pomoću alata jlink stvoriti JRE samo iz potrebnih modula.

Za znatiželjne, evo dobrog članka o pristupima smanjenju veličine slike. https://habr.com/ru/company/ruvds/blog/485650/.

Konačni 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"]

Ponovno kreiramo sliku, i ona je na kraju smanjena 6 puta, na 77 MB. Nije loše. Nakon toga, gotove slike mogu se učitati u registar slika, tako da su vaše slike dostupne za preuzimanje s interneta.

Zajedničko pokretanje servisa u Dockeru

Prvo, naši servisi moraju biti u istoj mreži. U Dockeru postoji nekoliko tipova mreža, a mi koristimo najjednostavniji od njih — bridge, koji omogućava povezivanje kontejnera pokrenutih na istom hostu u mrežu. Kreirat ćemo mrežu sljedećom naredbom:

docker network create msvc-network

Zatim ćemo pokrenuti backend kontejner imenom 'backend' s slikom microservices-backend:1.0.0:

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

Valja napomenuti da bridge-mreža pruža iz kutije otkrivanje usluga za kontejnere po njihovim imenima. Dakle, usluga backend-a bit će dostupna unutar Docker mreže na adresi http://backend:8080.

Pokrećemo 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.0

U ovom timu navodimo da proslijedimo 80 port našeg hosta na 8080 port kontejnera. Opcije env koristimo za postavljanje varijabli okruženja, koje će se automatski očitati od Springa i prepisati svojstva iz application.properties.

Nakon pokretanja pozivamo http://localhost/ i uvjeravamo se da sve radi kao i prošli put.

Zaključak

Na kraju smo stvorili dva jednostavna mikrosustava, pakirali ih u Docker kontejnere i zajednički pokrenuli na jednoj mašini. Međutim, ovaj sustav ima nekoliko nedostataka:

  • Loša otpornost na greške — sve radi na jednom poslužitelju
  • Loša skalabilnost — pri povećanju opterećenja bilo bi dobro automatski raspraviti dodatne instance usluga i ravnomjerno rasporediti opterećenje među njima
  • Složenost pokretanja — trebali smo unijeti najmanje 3 naredbe, s određenim parametrima (to samo za 2 usluge)

Za rješavanje gore navedenih problema postoje različita rješenja, poput Docker Swarm, Nomad, Kubernetes ili OpenShift. Ako je cijeli sustav napisan u Javi, možete pogledati u smjeru Spring Clouda (dobrog članka).

U sljedeće dijela pričat ću o tome kako sam konfigurirao Kubernetes i implementirao projekt u Google Kubernetes Engine.

Izvor: habr.com

Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere 🔥 Kupite pouzdan hosting za web stranice s DDoS zaštitom, VPS VDS servere | ProHoster