Scopri come distribuire i microservizi. Parte 1. Spring Boot e Docker

Scopri come distribuire i microservizi. Parte 1. Spring Boot e Docker

Ciao Habr.

In questo articolo, voglio parlare della mia esperienza nella creazione di un ambiente di apprendimento per sperimentare i microservizi. Quando ho imparato ogni nuovo strumento, ho sempre voluto provarlo non solo sulla macchina locale, ma anche in condizioni più realistiche. Pertanto, ho deciso di creare un'applicazione di microservizi semplificata, che in seguito può essere "coperta" con ogni sorta di tecnologie interessanti. Il requisito principale per il progetto è la sua massima vicinanza funzionale al sistema reale.

Inizialmente, ho suddiviso la creazione del progetto in diversi passaggi:

  1. Crea due servizi: "backend" (backend) e "gateway" (gateway), impacchettali in immagini docker e configurali per lavorare insieme

    Parole chiave: Java 11, Spring Boot, Docker, ottimizzazione delle immagini

  2. Sviluppo della configurazione di Kubernetes e deployment del sistema in Google Kubernetes Engine

    Parole chiave: Kubernetes, GKE, gestione delle risorse, scalabilità automatica, segreti

  3. Creazione di un grafico con Helm 3 per una migliore gestione dei cluster

    Tag: Helm 3, distribuzione della carta

  4. Configurazione di Jenkins e della pipeline per la consegna automatica del codice al cluster

    Parole chiave: configurazione di Jenkins, plug-in, repository di configurazione separato

Ho intenzione di dedicare un articolo separato a ogni passaggio.

L'obiettivo di questa serie di articoli non è come scrivere microservizi, ma come farli funzionare in un unico sistema. Sebbene tutte queste cose siano solitamente al di fuori della responsabilità dello sviluppatore, penso che sia comunque utile conoscerle almeno il 20% (che, come sai, danno l'80% del risultato). Alcuni argomenti incondizionatamente importanti, come la sicurezza, saranno lasciati fuori da questo progetto, poiché l'autore capisce poco di questo sistema creato esclusivamente per uso personale. Accetto qualsiasi opinione e critica costruttiva.

Creazione di microservizi

I servizi sono stati scritti in Java 11 utilizzando Spring Boot. L'interazione tra i servizi è organizzata utilizzando REST. Il progetto includerà un numero minimo di test (in modo che in seguito ci sia qualcosa da testare in Jenkins). Il codice sorgente dei servizi è disponibile su GitHub: back-end и porta.

Per poter controllare lo stato di ciascuno dei servizi, è stato aggiunto un attuatore a molla alle loro dipendenze. Creerà un endpoint /actuator/health e restituirà uno stato 200 se il servizio è pronto ad accettare il traffico o un 504 se c'è un problema. In questo caso si tratta di un controllo piuttosto fittizio, poiché i servizi sono molto semplici e, in caso di forza maggiore, è più probabile che diventino completamente non disponibili piuttosto che rimanere parzialmente operativi. Ma nei sistemi reali, Actuator può aiutare a diagnosticare un problema prima che gli utenti inizino a combatterlo. Ad esempio, se ci sono problemi di accesso al database, possiamo rispondere automaticamente interrompendo l'elaborazione delle richieste con un'istanza di servizio interrotta.

Servizio di back-end

Il servizio di backend conterà e restituirà semplicemente il numero di richieste accettate.

Codice controllore:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

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

Prova del controllore:

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

Portale di servizio

Il gateway inoltrerà la richiesta al servizio di backend integrandola con le seguenti informazioni:

  • id gateway. È necessario affinché sia ​​possibile distinguere un'istanza del gateway da un'altra dalla risposta del server
  • Alcuni "segreti" che svolgeranno il ruolo di una password molto importante (numero della chiave di crittografia di un cookie importante)

Configurazione in application.properties:

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

Adattatore back-end:

@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<String> response = restTemplate.getForEntity(
backendUrl + REQUESTS_ENDPOINT, String.class);
        return response.getBody();
    }
}

Controllore:

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

Lanciare:

Avviamo il backend:

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

Avvio del gateway:

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

Controlliamo:

$ curl http://localhost:8080/
Number of requests 1 (gateway 38560358, secret "default-secret")

Funziona tutto. Un lettore attento noterà che nulla ci impedisce di accedere direttamente al backend, bypassando il gateway (http://localhost:8081/requests). Per risolvere questo problema, i servizi devono essere combinati in un'unica rete e solo il gateway dovrebbe "sporgere" all'esterno.
Inoltre, entrambi i servizi condividono un file system, producono flussi e in un momento possono iniziare a interferire tra loro. Sarebbe bello isolare i nostri microservizi. Ciò può essere ottenuto distribuendo applicazioni su macchine diverse (molto denaro, difficile), utilizzando macchine virtuali (ad alta intensità di risorse, avvio lungo) o utilizzando la containerizzazione. Come previsto, scegliamo la terza opzione e docker come strumento per la containerizzazione.

docker

In breve, docker crea contenitori isolati, uno per applicazione. Per utilizzare la finestra mobile, è necessario scrivere un Dockerfile - istruzioni per la creazione e l'esecuzione dell'applicazione. Successivamente, puoi creare l'immagine, caricarla nel registro delle immagini (n. Mozzo mobile) e distribuisci il tuo microservizio in qualsiasi ambiente dockerizzato con un solo comando.

Dockerfile

Una delle caratteristiche più importanti di un'immagine è la sua dimensione. Un'immagine compatta verrà scaricata più velocemente da un repository remoto, occuperà meno spazio e il tuo servizio si avvierà più velocemente. Qualsiasi immagine è costruita sulla base dell'immagine di base e si consiglia di scegliere l'opzione più minimalista. Una buona opzione è Alpine, una distribuzione Linux completa con pacchetti minimi.

Per cominciare, proviamo a scrivere un Dockerfile "sulla fronte" (dirò subito che questo è un brutto modo, non farlo):

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

Qui stiamo usando un'immagine di base basata su Alpine con il JDK già installato per costruire il nostro progetto. Con il comando ADD, aggiungiamo la directory src corrente all'immagine, contrassegniamola come funzionante (WORKDIR) e avviamo la build. Il comando EXPOSE 8080 segnala al docker che l'applicazione nel container utilizzerà la sua porta 8080 (questo non renderà l'applicazione accessibile dall'esterno, ma consentirà l'accesso all'applicazione, ad esempio, da un altro container sulla stessa rete docker ).

Per impacchettare i servizi in immagini, devi eseguire i comandi dalla radice di ogni progetto:

docker image build . -t msvc-backend:1.0.0

Il risultato è un'immagine da 456 MB (di cui l'immagine JDK di base occupava 340 MB). E tutto nonostante il fatto che le classi del nostro progetto si contino su un dito. Per ridurre le dimensioni della nostra immagine:

  • Usiamo l'assemblaggio in più passaggi. Nella prima fase costruiremo il progetto, nella seconda installeremo il JRE e nella terza fase copieremo tutto in una nuova immagine Alpine pulita. In totale, solo i componenti necessari saranno nell'immagine finale.
  • Usiamo la modularizzazione di java. A partire da Java 9, puoi utilizzare lo strumento jlink per creare un JRE solo dai moduli di cui hai bisogno

Per i curiosi, ecco un buon articolo sugli approcci di riduzione dell'immagine. https://habr.com/ru/company/ruvds/blog/485650/.

File Docker finale:

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 [email protected]"
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"]

Ricreiamo l'immagine e, di conseguenza, ha perso 6 volte il suo peso, pari a 77 MB. Non male. Successivamente, le immagini già pronte possono essere caricate nel registro delle immagini in modo che le tue immagini siano disponibili per il download da Internet.

Servizi di co-esecuzione in Docker

Per cominciare, i nostri servizi devono essere sulla stessa rete. Esistono diversi tipi di reti nella finestra mobile e ne utilizziamo la più primitiva: bridge, che consente di collegare in rete i container in esecuzione sullo stesso host. Crea una rete con il seguente comando:

docker network create msvc-network

Successivamente, avvia il contenitore di backend denominato "backend" con l'immagine microservices-backend:1.0.0:

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

Vale la pena notare che la rete bridge fornisce il rilevamento del servizio predefinito per i contenitori in base ai loro nomi. Cioè, il servizio di backend sarà disponibile all'interno della rete docker all'indirizzo http://backend:8080.

Avvio del 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

In questo comando, indichiamo che stiamo inoltrando la porta 80 del nostro host alla porta 8080 del container. Utilizziamo le opzioni env per impostare le variabili d'ambiente che verranno lette automaticamente da Spring e sovrascriveranno le proprietà da application.properties.

Dopo aver iniziato, chiamiamo http://localhost/ e assicurati che tutto funzioni, come nel caso precedente.

conclusione

Di conseguenza, abbiamo creato due semplici microservizi, li abbiamo impacchettati in contenitori docker e li abbiamo lanciati insieme sulla stessa macchina. Il sistema risultante, tuttavia, presenta una serie di svantaggi:

  • Scarsa tolleranza ai guasti: tutto funziona per noi su un server
  • Scarsa scalabilità: quando il carico aumenta, sarebbe utile distribuire automaticamente ulteriori istanze del servizio e bilanciare il carico tra di esse
  • La complessità del lancio: dovevamo inserire almeno 3 comandi e con determinati parametri (questo è solo per 2 servizi)

Per risolvere i problemi di cui sopra, esistono diverse soluzioni come Docker Swarm, Nomad, Kubernetes o OpenShift. Se l'intero sistema è scritto in Java, puoi guardare verso Spring Cloud (buon articolo).

В parte successiva Parlerò di come ho configurato Kubernetes e distribuito il progetto su Google Kubernetes Engine.

Fonte: habr.com

Aggiungi un commento