Impariamo a implementare microservizi. Parte 1. Spring Boot e Docker

Impariamo a implementare microservizi. Parte 1. Spring Boot e Docker

Ciao, Habr.

In questo articolo voglio raccontare la mia esperienza nella creazione di un ambiente di formazione per esperimenti con microservizi. Quando esploro ogni nuovo strumento, ho sempre voluto provarlo non solo sulla macchina locale, ma anche in condizioni più realistiche. Così ho deciso di creare un'applicazione microservizi semplificata, che in seguito potrà essere "arricchita" con tecnologie interessanti. Il requisito principale del progetto è la sua massima funzionalità in prossimità a un sistema reale.

Inizialmente ho suddiviso la creazione del progetto in più fasi:

  1. Creare due servizi – ‘backend’ e ‘gateway’, impacchettarli in immagini Docker e configurare il loro funzionamento congiunto

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

  2. Sviluppo della configurazione di Kubernetes e distribuzione del sistema su Google Kubernetes Engine

    Parole chiave: Kubernetes, GKE, gestione delle risorse, autoscaling, segreti

  3. Creazione di un chart utilizzando Helm 3 per una gestione più efficace del cluster

    Parole chiave: Helm 3, distribuzione del chart

  4. Impostazione di Jenkins e pipeline per la consegna automatica del codice nel cluster

    Parole chiave: configurazione di Jenkins, plugin, repository di configurazioni separate

Intendo dedicare un articolo a ciascun passaggio.

L'obiettivo di questo ciclo di articoli non è solo su come scrivere microservizi, ma su come farli funzionare all'interno di un sistema integrato. Anche se queste questioni solitamente vanno oltre le responsabilità dello sviluppatore, penso sia utile conoscerle almeno per il 20% (che, come si sa, produce l'80% dei risultati). Alcuni temi indubbiamente importanti, come la sicurezza, saranno esclusi da questo progetto, poiché l'autore ha poca esperienza in materia; il sistema è creato esclusivamente per uso personale. Accolgo con favore 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 tramite REST. Il progetto includerà un numero minimo di test (in modo da avere qualcosa da testare in Jenkins). Il codice sorgente dei servizi è disponibile su GitHub: backend e gateway.

Per verificare lo stato di ciascun servizio, è stato aggiunto Spring Actuator come dipendenza. Questo creerà un endpoint /actuator/health e restituirà un stato 200 se il servizio è pronto a ricevere traffico, oppure 504 in caso di problemi. In questo contesto, si tratta di un controllo piuttosto superficiale, poiché i servizi sono molto semplici e in caso di emergenza potrebbero diventare completamente non disponibili piuttosto che mantenere una parziale operatività. Tuttavia, in sistemi reali, l'Actuator può aiutare a diagnosticare un problema prima che gli utenti inizino a lamentarsi. Ad esempio, in caso di problemi di accesso al database, possiamo reagire automaticamente interrompendo l'elaborazione delle richieste da una istanza difettosa del servizio.

Servizio Backend

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

Codice del controller:

@RestController
public class RequestsCounterController {

    private final AtomicLong counter = new AtomicLong();

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

Test del controller:

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

Servizio Gateway

Il gateway reindirizzerà la richiesta al servizio di backend, aggiungendo le seguenti informazioni:

  • id del gateway. È necessario per distinguere un'istanza di gateway da un'altra in base alla risposta del server.
  • Un "segreto" che fungerà da password molto importante (n° chiave di cifratura di un cookie importante).

Configurazione in application.properties:

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

Adapter per comunicare con il backend:

@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("Numero di richieste %s (gateway %d, segreto %s)", backendAdapter.getRequests(), instanceId, secret);
    }
}

Avvio:

Avviamo il backend:

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

Avviamo il gateway:

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

Controllando:

$ curl http://localhost:8080/
Numero di richieste 1 (gateway 38560358, segreto "default-secret")

Tutto funziona. Il lettore attento noterà che non c'è nulla che ci impedisca di accedere direttamente al backend bypassando il gateway (http://localhost:8081/requests). Per correggere questo, i servizi devono essere uniti in una sola rete, e all'esterno deve "apparire" solo il gateway.
Inoltre, entrambi i servizi condividono un'unica filesystem, creano processi e in un dato momento possono iniziare a interferire l'uno con l'altro. Sarebbe utile isolare i nostri microservizi. Questo può essere ottenuto distribuendo le applicazioni su diverse macchine (molti soldi, difficile), usando macchine virtuali (richiede molte risorse, avvio lento) oppure attraverso la containerizzazione. Come previsto, scegliamo la terza opzione e Docker come strumento per la containerizzazione.

Docker

In sintesi, Docker crea contenitori isolati, uno per ogni applicazione. Per utilizzare Docker, è necessario scrivere un Dockerfile, un'istruzione per la costruzione e l'avvio dell'applicazione. Successivamente, sarà possibile creare un'immagine, caricarla nel registro delle immagini (№ DockerHub) e in un solo comando distribuire il proprio microservizio in qualsiasi ambiente dockerizzato.

Dockerfile

Una delle caratteristiche più importanti di un'immagine è la sua dimensione. Un'immagine compatta si scarica più velocemente dal repository remoto, occupa meno spazio e il tuo servizio si avvia più rapidamente. Ogni immagine è costruita su una base di immagine, ed è consigliabile scegliere l'opzione più minimalista. Una buona scelta è Alpine, una distribuzione Linux completa con il minimo di pacchetti.

Iniziamo tentando di scrivere un Dockerfile "in modo diretto" (dirò subito che questo è un cattivo approccio, non fatelo):

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 usiamo un'immagine base basata su Alpine con JDK già installato per costruire il nostro progetto. Con il comando ADD aggiungiamo la directory src corrente all'immagine, la impostiamo come cartella di lavoro (WORKDIR) e avviamo la costruzione. Il comando EXPOSE 8080 informa Docker che l'applicazione nel contenitore utilizzerà la sua porta 8080 (questo non renderà l'applicazione accessibile dall'esterno, ma permetterà di accedere all'applicazione, ad esempio, da un altro contenitore nella stessa rete Docker).

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

docker image build . -t msvc-backend:1.0.0

Il risultato è un'immagine di 456 MB (di cui l'immagine base JDK 340 ha occupato MB). Considerando che le classi nel nostro progetto sono poche. Per ridurre le dimensioni della nostra immagine:

  • Utilizziamo una build multi-step. Nel primo passo, costruiremo il progetto, nel secondo installeremo JRE e nel terzo copiamo tutto in una nuova immagine Alpine pulita. Alla fine, nell'immagine finale ci saranno solo i componenti necessari.
  • Sfruttiamo la modularizzazione di Java. A partire da Java 9, è possibile utilizzare lo strumento jlink per creare un JRE solo dai moduli necessari.

Per gli interessati, ecco un buon articolo sui metodi per ridurre le dimensioni dell'immagine https://habr.com/ru/company/ruvds/blog/485650/.

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

Ricreiamo l'immagine e alla fine ha ridotto le dimensioni di 6 volte, arrivando a 77 MB. Non male. Dopo, le immagini pronte possono essere caricate nel registro delle immagini, rendendo disponibili le vostre immagini per il download da internet.

Esecuzione congiunta dei servizi in Docker

Innanzitutto, i nostri servizi devono trovarsi nella stessa rete. In Docker ci sono diversi tipi di reti, e noi utilizziamo la più semplice di esse — bridge, che consente di collegare in rete i contenitori eseguiti su un unico host. Creeremo la rete con il seguente comando:

docker network create msvc-network

Successivamente avvieremo un contenitore per il backend chiamato ‘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 offre out-of-the-box service discovery per i contenitori tramite i loro nomi. Quindi, il servizio backend sarà accessibile all'interno della rete Docker all'indirizzo http://backend:8080.

Avviamo il 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 specifichiamo che stiamo inoltrando la porta 80 del nostro host sulla porta 8080 del contenitore. Le opzioni env vengono utilizzate per impostare le variabili d'ambiente, che saranno automaticamente lette da Spring e sovrascriveranno le proprietà in application.properties.

Dopo l'avvio chiamiamo http://localhost/ e ci assicuriamo che tutto funzioni come nel caso precedente.

Conclusione

In definitiva, abbiamo creato due semplici microservizi, li abbiamo impacchettati in contenitori Docker e li abbiamo avviati insieme su una sola macchina. Tuttavia, il sistema ottenuto presenta alcuni svantaggi:

  • Scarsa resilienza ai guasti – tutto funziona su un solo server
  • Scarsa scalabilità – con l'aumento del carico sarebbe utile distribuire automaticamente ulteriori istanze dei servizi e bilanciare il carico tra di esse
  • La complessità di avvio - è necessario inserire almeno 3 comandi, con parametri specifici (questo solo per 2 servizi)

Per risolvere i problemi sopra menzionati, ci sono diverse soluzioni come Docker Swarm, Nomad, Kubernetes o OpenShift. Se l'intero sistema sarà scritto in Java, si può considerare Spring Cloud (un buon articolo).

In prossima parte parlerò di come ho configurato Kubernetes e distribuito un progetto in Google Kubernetes Engine.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster