Mikroszolgáltatások kiépítése. 1. rész. Spring Boot és Docker

Mikroszolgáltatások kiépítése. 1. rész. Spring Boot és Docker

Helló, Habr.

Ebben a cikkben szeretném megosztani tapasztalataimat a mikroszolgáltatásokkal való kísérletezéshez szükséges tanulási környezet létrehozásáról. Minden új eszköz megismerésekor mindig is szerettem volna kipróbálni nemcsak a helyi gépemen, hanem valósághűbb körülmények között is. Ezért úgy döntöttem, hogy létrehozok egy egyszerűsített mikroszolgáltatás alkalmazást, amelyet később "megspékelhetek" különféle érdekes technológiákkal. A projekt fő követelménye a lehető legnagyobb funkcionalitásbeli hasonlóság a valós rendszerhez.

Eredetileg a projekt létrehozását több lépésre osztottam:

  1. Két szolgáltatás létrehozása — ‘backend’ és ‘gateway’, azok Docker-képekbe csomagolása és együttműködésük beállítása

    Kulcsszavak: Java 11, Spring Boot, Docker, képkulcsoptimalizálás

  2. Kubernetes konfiguráció fejlesztése és a rendszer telepítése a Google Kubernetes Engine-be

    Kulcsszavak: Kubernetes, GKE, erőforrás-kezelés, automatikus skálázás, titkok

  3. Chart létrehozása a Helm 3 segítségével a klaszter hatékonyabb kezeléséhez

    Kulcsszavak: Helm 3, chart telepítés

  4. Jenkins beállítása és pipeline létrehozása a kód automatikus szállításához a klaszterbe

    Kulcsszavak: Jenkins konfiguráció, pluginok, külön konfigurációs tároló

Minden lépéshez külön cikket tervezek írni.

E cikksorozat célja nem az, hogy mikroszolgáltatásokat írjak, hanem hogy azokat egyetlen rendszerben működésre bírjam. Bár ezek a dolgok általában a fejlesztői felelősségen kívül esnek, úgy gondolom, hogy hasznos ismerni őket legalább 20%-ban (ami, mint tudjuk, a hatékonyság 80%-át adja). Néhány, feltétlenül fontos téma, mint a biztonság biztosítása, kívül marad a projekt keretein, mivel a szerző ebben nem jártas; a rendszer kizárólag személyes használatra készült. Örömmel látom az észrevételeket és az építő jellegű kritikát.

Mikroszolgáltatások létrehozása

A szolgáltatások Java 11-ben íródtak, Spring Boot használatával. A szolgáltatások közötti interakció REST segítségével kerül megszervezésre. A projekt minimális számú tesztet fog tartalmazni (hogy később legyen mit tesztelni a Jenkinsben). A szolgáltatások forráskódja elérhető a GitHub-on: backend és gateway.

A szolgáltatások állapotának ellenőrzéséhez a Spring Actuatort hozzáadtuk az összeszott függőségekhez. Ez létrehozza az /actuator/health végpontot, és 200-as státuszt fog visszaadni, ha a szolgáltatás kész a forgalom fogadására, vagy 504-et, ha problémák merülnek fel. Ebben az esetben ez egy meglehetősen formális ellenőrzés, mivel a szolgáltatások nagyon egyszerűek, és ha valami váratlan történik, valószínűbb, hogy teljesen leállnak, mint hogy részlegesen működjenek. De valós rendszerekben az Actuator segíthet diagnosztizálni a problémát, mielőtt a felhasználók elkezdenék azt tapasztalni. Például, ha problémák merülnek fel az adatbázis elérésével, automatikusan reagálhatunk rá, megszüntetve a hibás szolgáltatás által kezelt kéréseket.

Backend Szolgáltatás

A backend szolgáltatás egyszerűen számolja és visszaadja a fogadott kérések számát.

Controller kód:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

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

Controller teszt:

@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 Szolgáltatás

A gateway a háttérszolgáltatásra irányítja a kéréseket, kiegészítve az alábbi információval:

  • a gateway azonosítója. Szükséges, hogy az egyik gateway példány válaszából megkülönböztethessük a másiktól
  • Egy "titok", amely nagyon fontos jelszóként fog működni (az értékes süti titkosító kulcsának száma)

Konfiguráció az application.properties fájlban:

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

Adapter a backendhez való kapcsolódáshoz:

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

Controller:

@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("Kérések száma: %s (gateway %d, titok %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Indítás:

Backend indítása:

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

Gateway indítása:

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

Ellenőrizzük:

$ curl http://localhost:8080/
Kérések száma 1 (gateway 38560358, titkos "default-secret")

Minden működik. A figyelmes olvasó észreveszi, hogy semmi nem akadályozza meg, hogy közvetlenül a backendhez forduljunk a gateway megkerülésével (http://localhost:8081/requests). Ennek kijavításához a szolgáltatásokat egy hálózatba kell összekapcsolni, és csak a gateway-nek kell látszania kívül.
Ezenkívül mindkét szolgáltatás osztozik egy fájlrendszeren, szálakat generálnak, és egy adott pillanatban zavarhatják egymást. Jó lenne elkülöníteni a mikroszolgáltatásainkat. Ezt elérhetjük az alkalmazások különböző gépekre telepítésével (sok pénz, bonyolult), virtuális gépek használatával (erőforrásigényes, lassú indítás) vagy konténerizálás segítségével. Várhatóan a harmadik opciót választjuk, és Docker mint konténerizáló eszközt.

Docker

Röviden, a Docker izolált konténereket hoz létre, egyet az egyes alkalmazásokhoz. A Docker használatához Dockerfile-t kell írni - egy utasítást az alkalmazás összeállításához és indításához. Ezután létrehozhatunk egy képet, feltölthetjük egy képtárba (№ DockerHub) és egy parancs segítségével telepíthetjük mikroszolgáltatásunkat bármelyik dockerizált környezetbe.

Dockerfile

A kép egyik legfontosabb jellemzője a mérete. A kompakt kép gyorsabban letölthető a távoli tárolóból, kevesebb helyet foglal el, és az Ön szolgáltatása gyorsabban elindul. Bármely kép egy alap képből épül fel, és ajánlott a lehető legminimalistább változatot választani. Jó választás az Alpine - egy teljes Linux disztribúció minimális csomagokkal.

Először próbáljunk meg egy Dockerfile-t "lóbálni" (azonnal mondom, hogy ez rossz módszer, ne csináld így):

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

Itt egy Alpine alapú képet használunk, amely már telepítve van JDK-val a projektünk összeállításához. Az ADD parancs segítségével a jelenlegi src mappát hozzáadjuk a képhez, azt munkaként (WORKDIR) megjelöljük, és elindítjuk az összeállítást. Az EXPOSE 8080 parancs jelzi a dockernak, hogy az alkalmazás a konténerben az 8080-as portot fogja használni (ez nem teszi az alkalmazást elérhetővé kívülről, de lehetővé teszi, hogy például egy másik konténerből hozzáférjünk az alkalmazáshoz ugyanabban a docker hálózatban).

A szolgáltatások képekbe csomagolásához a projektek gyökeréből kell végrehajtani a parancsokat:

docker image build . -t msvc-backend:1.0.0

Ennek eredményeként 456 MB méretű képet kapunk (ebből a JDK alapképe 340 MB-ot foglalt el). És mindezek mellett a projektünkben alig néhány osztály található. A kép méretének csökkentéséhez:

  • Használjunk többlépcsős buildet. Az első lépésben összeállítjuk a projektet, a második lépésben telepítjük a JRE-t, a harmadik lépésben pedig mindezt át másoljuk egy új, tiszta Alpine képbe. Így végül csak a szükséges összetevők kerülnek a végső képbe.
  • Használjuk a Java modularizálását. A Java 9-től kezdve a jlink eszközzel csak a szükséges modulokból hozhatunk létre JRE-t.

A kíváncsiaknak itt van egy jó cikk a képméret-csökkentési megközelítésekről. https://habr.com/ru/company/ruvds/blog/485650/.

A végső 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"]

Újra létrehozzuk a képet, és végül 6-szor csökkentettük a méretét, 77 MB-ra. Nem rossz. Ezután a kész képeket feltölthetjük a képtárba, hogy az Ön képei elérhetők legyenek letöltésre az internetről.

Szolgáltatások együttfutása Dockerben.

Először is, szolgáltatásainknak egy hálózatban kell lenniük. A Dockerben többféle hálózattípus létezik, mi a legprimitívebb, a bridge-típust használjuk, amely lehetővé teszi a egy hoston futó konténerek hálózatba kötését. A következő parancs kiadásával létrehozzuk a hálózatot:

docker network create msvc-network

Ezután elindítjuk a backend konténert 'backend' névvel a microservices-backend:1.0.0 képből:

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

Érdemes megemlíteni, hogy a bridge-hálózat alapból service discovery-t biztosít a konténerek számára a neveik alapján. Tehát a backend szolgáltatás a Docker hálózaton belül a következő címen lesz elérhető: http://backend:8080.

Gateway indítása:

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

Ebben a csapatban megadjuk, hogy a gazdagépünk 80-as portját az 8080-as portjára irányítjuk a konténernek. Az env opciókat a környezeti változók beállítására használjuk, amelyeket a Spring automatikusan kiolvas és felülírja az application.properties fájlban található tulajdonságokat.

A futtatás után meghívjuk http://localhost/ és meggyőződünk róla, hogy minden úgy működik, mint az előző alkalommal.

Zárás

Ennek eredményeként két egyszerű mikroszolgáltatást hoztunk létre, becsomagoltuk őket Docker konténerekbe és közösen futtattuk őket egy gépen. Azonban a rendszernek van néhány hátránya:

  • Gyenge hibatűrés — minden az egy szerveren fut.
  • Gyenge skálázhatóság — terhelés növekedésekor jó lenne automatikusan további szolgáltatásmintákat telepíteni és terhelést egyensúlyozni közöttük.
  • Indítási nehézség — legalább 3 parancsot kellett bevinni, bizonyos paraméterekkel (ez csak 2 szolgáltatásra vonatkozik).

A fent említett problémák megoldására számos lehetőség létezik, mint például a Docker Swarm, Nomad, Kubernetes vagy OpenShift. Ha az egész rendszert Java nyelven írják, akkor érdemes megfontolni a Spring Cloud-ot (jó cikk).

A következő részben elmondom, hogyan állítottam be a Kubernetest és telepítettem a projektet a Google Kubernetes Engine-re.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster