Učíme se rozvíjet mikroservisy. Část 1. Spring Boot a Docker

Učíme se rozvíjet mikroservisy. Část 1. Spring Boot a Docker

Ahoj, Habr.

V tomto článku se chci podělit o svou zkušenost s vytvářením výukového prostředí pro experimenty s mikroslužbami. Při zkoumání každého nového nástroje jsem vždy chtěl vyzkoušet nejen na místním stroji, ale také v realistickějších podmínkách. Proto jsem se rozhodl vytvořit zjednodušenou mikroslužbovou aplikaci, kterou následně mohu vybavit různými zajímavými technologiemi. Hlavním požadavkem na projekt je maximální funkční přiblížení k reálnému systému.

Původně jsem rozdělil vytváření projektu do několika kroků:

  1. Vytvořit dva služby — ‘backend’ a ‘gateway’, zabalit je do dockerových obrazů a nastavit jejich spolupráci

    Klíčová slova: Java 11, Spring Boot, Docker, optimalizace obrazu

  2. Vývoj Kubernetes konfigurace a nasazení systému do Google Kubernetes Engine

    Klíčová slova: Kubernetes, GKE, správa zdrojů, automatické škálování, tajemství

  3. Vytvoření chartu pomocí Helm 3 pro efektivnější správu clustru

    Klíčová slova: Helm 3, nasazení chartu

  4. Nastavení Jenkins a pipeline pro automatické dodávání kódu do clustru

    Klíčová slova: konfigurace Jenkins, pluginy, samostatné úložiště konfigurací

Každému kroku plánuji věnovat samostatný článek.

Zaměření této série článků není ani tak na to, jak psát mikroslužby, ale jak je přimět fungovat v jednotném systému. Přestože tyto věci obvykle leží mimo odpovědnost vývojáře, myslím, že je přesto užitečné být s nimi seznámen alespoň na 20 % (které, jak je známo, dávají 80 % výsledku). Některá bezesporu důležitá témata, jako je zajištění bezpečnosti, budou vynechána z tohoto projektu, protože autor v tom má málo znalostí — systém je vytvářen výhradně pro osobní potřebu. Rád uvítám jakékoli názory a konstruktivní kritiku.

Vytváření mikroslužeb

Služby byly napsány v Java 11 s využitím Spring Boot. Mezi službami je interakce organizována pomocí REST. Projekt bude zahrnovat minimální množství testů (aby bylo co testovat v Jenkins). Zdrojový kód služeb je dostupný na GitHubu: backend a gateway.

Aby sprawdzić stan każdego z serwisów, dodano zależność Spring Actuator. Utworzy on punkt końcowy /actuator/health i będzie zwracał status 200, jeśli serwis jest gotów do przyjmowania ruchu, lub 504 w przypadku problemów. W tym przypadku jest to dość fikcyjna kontrola, ponieważ serwisy są bardzo proste, a w przypadku jakiegoś force majeure raczej staną się zupełnie niedostępne, niż zachowają częściową funkcjonalność. Jednak w rzeczywistych systemach Actuator może pomóc w diagnozowaniu problemów, zanim zaczną o nie walczyć użytkownicy. Na przykład, w przypadku problemów z dostępem do bazy danych, możemy na to automatycznie zareagować, przerywając przetwarzanie żądań przez uszkodzony egzemplarz serwisu.

Serwis Backend

Serwis backendu będzie po prostu liczył i zwracał liczbę przyjętych żądań.

Kod kontrolera:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

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

Test kontrolera:

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

Serwis Gateway

Brama będzie przekierowywać żądanie do serwisu backendu, dodając do niego następujące informacje:

  • id bramy. Jest potrzebne, aby można było odróżnić jeden egzemplarz bramy od drugiego w odpowiedzi serwera.
  • Pewien "sekret", który będzie pełnił rolę bardzo ważnego hasła (nr klucza szyfrowania ważnego ciasteczka)

Konfiguracja w application.properties:

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

Adapter do komunikacji z backendem:

@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("Liczba żądań %s (brama %d, sekret %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Spuštění:

Uruchamiamy backend:

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

Uruchamiamy bramę:

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

Kontrolujeme:

$ curl http://localhost:8080/
Počet požadavků 1 (gateway 38560358, tajný "default-secret")

Všechno funguje. Pozorný čtenář si všimne, že nám nic nebrání obrátit se na backend přímo, obcházejíc bránu (http://localhost:8081/requests). Abychom to opravili, služby by měly být spojeny do jedné sítě, a venku by měla "vytékat" pouze brána.
Také obě služby sdílejí jeden souborový systém, vytvářejí vlákna a v jednu chvíli si mohou vzájemně překážet. Bylo by dobré izolovat naše mikroservisy. Toho lze dosáhnout umístěním aplikací na různé stroje (spousta peněz, složité), použitím virtuálních strojů (náročné na zdroje, dlouhý start) nebo pomocí kontejnerizace. Očekávaně vybíráme třetí variantu a Docker jak nástroj pro kontejnerizaci.

Docker

Stručně řečeno, Docker vytváří izolované kontejnery, jeden pro každou aplikaci. K tomu, abyste mohli používat Docker, musíte napsat Dockerfile — instrukci pro sestavení a spuštění aplikace. Dále můžete sestavit obraz, nahrát jej do registru obrazů (č. DockerHub) a jedním příkazem nasadit svou mikroservisu v jakémkoli dockerizovaném prostředí.

Dockerfile

Jedním z nejdůležitějších atributů obrazu je jeho velikost. Kompaktní obraz se rychleji stáhne z vzdáleného repozitáře, zabere méně místa a vaše služba se rychleji spustí. Každý obraz je postaven na základním obrazu, a doporučuje se vybírat nejminimalističtější variantu. Dobrou volbou je Alpine — plnohodnotná distribuce Linuxu s minimem balíčků.

Začneme psát Dockerfile "na tvrdo" (hned říkám, že to je špatný způsob, nedělejte to):

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

Zde používáme základní obraz na bázi Alpine s již nainstalovaným JDK pro sestavení našeho projektu. Příkazem ADD přidáváme do obrazu aktuální adresář src, označujeme jej jako pracovní (WORKDIR) a spouštíme sestavení. Příkaz EXPOSE 8080 signalizuje Dockeru, že aplikace v kontejneru bude používat jeho port 8080 (to nezpřístupní aplikaci zvenku, ale umožní se na aplikaci obrátit, například z jiného kontejneru ve stejné síti Dockeru).

Abychom zabalili služby do obrazů, musíme spustit příkazy z kořene každého projektu:

docker image build . -t msvc-backend:1.0.0

Výsledkem je obraz s velikostí 456 MB (z toho základní obraz JDK 340 zabral MB). A to vše přitom, že třídy v našem projektu lze napočítat na prstech. Abychom zmenšili velikost našeho obrazu:

  • Použijeme víceúrovňové sestavení. V prvním kroku vytvoříme projekt, ve druhém nainstalujeme JRE a ve třetím kroku vše zkopírujeme do nového čistého Alpine obrazu. Nakonec se ve finálním obraze ocitnou pouze potřebné komponenty.
  • Využijeme modularizaci Java. Od verze Java 9 lze pomocí nástroje jlink vytvořit JRE pouze z potřebných modulů.

Pro zvědavé, zde je dobrý článek o přístupech ke zmenšení velikosti obrazu. https://habr.com/ru/company/ruvds/blog/485650/.

Konečný 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"]

Obraz znovu vytvoříme a nakonec zhubnul šestkrát, když dosáhl velikosti 77 MB. Nic špatného. Poté lze hotové obrazy nahrát do registru obrazů, aby byly vaše obrazy dostupné k stažení z internetu.

Společný běh služeb v Dockeru

Nejdříve musí být naše služby ve stejné síti. V Dockeru existuje několik typů sítí, a my použijeme nejprimitivnější z nich — bridge, který umožňuje spojit kontejnery běžící na jednom hostiteli do sítě. Síť vytvoříme následujícím příkazem:

docker network create msvc-network

Dále spustíme kontejner backendu s názvem 'backend' s obrazem microservices-backend:1.0.0:

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

Je třeba poznamenat, že bridge-síť poskytuje out-of-the-box zjištění služby pro kontejnery podle jejich názvů. To znamená, že služba backendu bude dostupná uvnitř sítě Docker pod adresou http://backend:8080.

Uruchamiamy bramę:

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 tomto týmu uvádíme, že přesměrováváme port 80 našeho hostitele na port 8080 kontejneru. Možnost env používáme k nastavení proměnných prostředí, které budou automaticky vyčteny Springem a přepíší vlastnosti z application.properties.

Po spuštění voláme http://localhost/ a ujistíme se, že vše funguje jako v minulém případě.

Závěr

Nakonec jsme vytvořili dva jednoduché mikroservisy, zabalili je do Docker kontejnerů a společně je spustili na jednom stroji. Systém však má několik nedostatků:

  • Špatná odolnost proti chybám — všechno běží na jednom serveru.
  • Špatná škálovatelnost — při zvýšení zatížení by bylo dobré automaticky nasadit další instance služeb a vyvažovat zátěž mezi nimi.
  • Složitost spuštění — potřebovali jsme zadat minimálně 3 příkazy s určitými parametry (to jen pro 2 služby).

Pro odstranění výše uvedených problémů existuje řada řešení, jako jsou Docker Swarm, Nomad, Kubernetes nebo OpenShift. Pokud bude celý systém napsán v Javě, můžete se podívat na Spring Cloud (dobrý článek).

V v další části řeknu o tom, jak jsem nastavoval Kubernetes a nasazoval projekt do Google Kubernetes Engine.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster