Učimo da postavimo mikroservise. Deo 1. Spring Boot i Docker

Učimo da postavimo mikroservise. Deo 1. Spring Boot i Docker

Pozdrav, Habr.

V tem članku želim deliti svoje izkušnje pri ustvarjanju učnega okolja za eksperimentiranje z mikroservisi. Pri učenju vsake nove orodja sem si vedno želel, da ga preizkusim ne le na lokalnem računalniku, temveč tudi v bolj realističnih pogojih. Zato sem se odločil ustvariti poenostavljeno mikroservisno aplikacijo, ki jo bom kasneje lahko 'opremil' z raznimi zanimivimi tehnologijami. Glavni zahtevek projekta je njegova maksimalna funkcionalna bližina realnemu sistemu.

Sprva sem proces ustvarjanja projekta razdelil na več korakov:

  1. Ustvariti dve storitvi - 'zadnji del' (backend) in 'prehod' (gateway), ju zapakirati v docker slike ter nastaviti njuno skupno delovanje

    Ključne besede: Java 11, Spring Boot, Docker, optimizacija slik

  2. Razvoj konfiguracije Kubernetes in uvedba sistema v Google Kubernetes Engine

    Ključne besede: Kubernetes, GKE, upravljanje virov, samodejno prilagajanje, skrivnosti

  3. Ustvarjanje beleženja s pomočjo Helm 3 za bolj učinkovito upravljanje zbora

    Ključne besede: Helm 3, uvedba beleženja

  4. Nastavitev Jenkins-a in pipeline-a za samodejno dostavo kode v zbor

    Ključne besede: konfiguracija Jenkins, vtičniki, ločen repozitorij konfiguracij

Vsakemu koraku nameravam posvetiti poseben članek.

Usmerjenost tega cikla člankov ni v tem, kako napisati mikroservise, temveč kako jih spraviti v delovanje v eni sami sistemu. Čeprav vse te zadeve običajno ležijo izven odgovornosti razvijalca, mislim, da je vseeno koristno biti seznanjen z njimi vsaj za 20% (kar naj bi, kot je znano, prineslo 80% rezultata). Nekatere nedvomno pomembne teme, kot je zagotavljanje varnosti, bodo izključene iz tega projekta, saj avtor o tem malo ve; sistem se ustvarja izključno za osebno uporabo. Vesel bom vseh mnenj in konstruktivne kritike.

Ustvarjanje mikroservisov

Storitve so bile napisane v Java 11 z uporabo Spring Boot. Medservisno sodelovanje je organizirano s pomočjo REST. Projekt bo vseboval minimalno število testov (da bo kasneje kaj testirati v Jenkins-u). Izvorna koda storitev je dostopna na GitHub-u: zadnji del in prehod.

Da bi bilo moguće provjeriti stanje svake od usluga, u njihovu zavisnost je dodan Spring Actuator. On će kreirati endpoint \/actuator\/health i vraćati 200 status ako je usluga spremna za primanje prometa, ili 504 u slučaju problema. U ovom slučaju, to je prilično lažna provjera, jer su usluge vrlo jednostavne, i u slučaju neke nesreće, vjerojatnije je da će postati potpuno nedostupne nego da će zadržati djelomičnu funkcionalnost. Međutim, u stvarnim sistemima Actuator može pomoći u dijagnosticiranju problema prije nego što se korisnici počnu žaliti. Na primjer, u slučaju problema s pristupom bazi podataka, možemo automatski reagirati prekinuvši obradu zahtjeva oštećenog primjerka usluge.

Backend usluga

Backend usluga ć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 usluga

Gateway će preusmjeravati zahtjev backend usluzi, dopunjujući ga sljedećim informacijama:

  • id gatewaya. Potreban je kako bi se moglo po odgovoru servera razlikovati jedan primjerak gatewaya od drugog
  • Neki "tajni" podatak, koji će igrati ulogu veoma važnog lozinku (br. ključa šifriranja važnog kolačića)

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

Nadzornik:

@RestController
@RequiredArgsConstructor
public class EndpointController {

    private final BackendAdapter backendAdapter;

    @Value("${instance.id}")
    private int instanceId;

    @Value("${secret}")
    private String secret;

    @GetMapping("\/", produces = "text/plain")
    public String getRequestsCount() {
        return String.format("Broj zahtjeva %s (gateway %d, tajna %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Zagon:

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

Preverimo:

$ curl http://localhost:8080/
Število zahtevkov 1 (prehod 38560358, skrivnost "privzeta-skrivnost")

Vse deluje. Pozoren bralec bo opazil, da nam nič ne preprečuje, da se neposredno povezujemo z backendom, mimo prehoda (http://localhost:8081/requests). Da to odpravimo, morajo biti storitve združene v eno omrežje, na zunaj pa mora "štrleti" samo prehod.
Obe storitvi tudi delita enotni datotečni sistem, ustvarjata niti in v enem trenutku se lahko začnejo motiti. Bilo bi dobro izolirati naše mikrostoritve. To lahko dosežemo z razporeditvijo aplikacij na različne naprave (veliko denarja, težko), z uporabo virtualnih strojev (zahteva veliko virov, dolgotrajno zagon) ali pa s pomočjo kontejnerizacije. Očitno izbiramo tretjo možnost in Docker kot orodje za kontejnerizacijo.

Docker

Na kratko, Docker ustvari izolirane kontejnerje, enega za aplikacijo. Da bi uporabljali Docker, je treba napisati Dockerfile — navodilo za sestavljanje in zagon aplikacije. Nato lahko ustvarimo sliko, jo naložimo v register slik (št. DockerHub) in z eno ukazno vrstico razporedimo svojo mikrostoritev v katerem koli dockeriziranem okolju.

Dockerfile

Ena najpomembnejših značilnosti slike je njena velikost. Kompaktna slika se hitreje prenese s oddaljenega repozitorija, zasede manj prostora, vaša storitev pa se hitreje zažene. Vsaka slika se gradi na osnovi osnovne slike, priporočljivo pa je izbrati najbolj minimalistično različico. Dobra izbira je Alpine — popoln operacijski sistem Linux z minimalnim številom paketov.

Za začetek poskusimo napisati Dockerfile "na grobo" (takoj povem, da je to slabi način, ne delajte tako):

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

Tukaj uporabljamo osnovno sliko, ki temelji na Alpine z že nameščenim JDK za sestavljanje našega projekta. S komandami ADD dodajamo v sliko trenutno mapo src, jo označimo kot delovno (WORKDIR) in zaženemo sestavljanje. Komanda EXPOSE 8080 nakazuje Dockerju, da bo aplikacija v kontejnerju uporabljala njen port 8080 (to ne bo omogočilo dostopa do aplikacije od zunaj, vendar bo omogočilo dostop do aplikacije, na primer, iz drugega kontejnerja v istem Docker omrežju).

Za pakiranje storitev v slike je treba iz korena vsakega projekta izvesti ukaze:

docker image build . -t msvc-backend:1.0.0

Rezultatom dobivamo sliku veličine 456 MB (od kojih je osnovna slika JDK 340 zauzela MB). I sve to, s obzirom na to da se klase u našem projektu mogu izbrojati na prstima jedne ruke. Da bismo smanjili veličinu naše slike:

  • Koristimo višestepenu izgradnju. U prvom koraku gradimo projekt, u drugom instaliramo JRE, a trećim korakom sve to kopiramo u novu čistu Alpine sliku. Na kraju će u finalnoj slici biti samo potrebne komponente.
  • Iskoristit ćemo modularizaciju Jave. Počevši od Jave 9, moguće je pomoću alata jlink kreirati JRE samo od potrebnih modula

Za znatiželjne, evo dobre članka o pristupima smanjenju veličina slika 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, spremne slike mogu se učitati u registar slika kako bi vaše slike bile dostupne za preuzimanje s interneta.

Zajedničko pokretanje servisa u Dockeru

Na početku, 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 jednom hostu u mrežu. Kreirajmo mrežu sljedećom komandom:

docker network create msvc-network

Zatim ćemo pokrenuti backend kontejner nazvan '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 automatsko otkrivanje usluga za kontejnere prema njihovim imenima. To znači da će backend servis biti dostupan 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

V tem timu navedemo, da preusmerjamo 80. vrata našega gostitelja na 8080. vrata kontejnerja. Možnosti env uporabljamo za nastavitev okolijskih spremenljivk, ki jih bo Spring samodejno prebral in prepisal lastnosti iz application.properties.

Po zagonu pokličemo http://localhost/ in se prepričamo, da vse deluje, kot v prejšnjem primeru.

Zaključek

Na koncu smo ustvarili dva preprosta mikrostoritev, jih zapakirali v Docker kontejnerje in zagnali skupaj na eni napravi. Vendar pa ima pridobljeni sistem vrsto pomanjkljivosti:

  • Slaba odpornost na napake — vse deluje na enem strežniku
  • Slaba razširljivost — z naraščajočim pritiskom bi bilo dobro samodejno uvajati dodatne primerke storitev in uravnotežiti obremenitev med njimi
  • Zapletenost zagon — potrebovali smo vnesti vsaj 3 ukaze, in to z določenimi parametri (to je le za 2 storitvi)

Za odpravo zgoraj navedenih težav obstaja več rešitev, kot so Docker Swarm, Nomad, Kubernetes ali OpenShift. Če je celoten sistem napisan v Javi, lahko pogledamo v smeri Spring Cloud (dober članek).

V naslednjega dela vam bom povedal, kako sem nastavil Kubernetes in namestil projekt v Google Kubernetes Engine.

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster