
Ciao, Habr.
In questo articolo voglio raccontare la mia esperienza nella creazione di un ambiente di apprendimento per esperimenti con microservizi. Quando studio ogni nuovo strumento, ho sempre voluto provarlo non solo sulla macchina locale, ma anche in condizioni più realistiche. Per questo motivo ho deciso di creare una semplice applicazione a microservizi, che in seguito potrà essere "evoluta" con tecnologie interessanti. Il principale requisito del progetto è la massima funzionalità simile a un sistema reale.
Inizialmente ho suddiviso la creazione del progetto in diversi passaggi:
Creare due servizi — ‘backend’ (backend) e ‘gateway’ (gateway), impacchettarli in immagini docker e configurarne il funzionamento congiunto
Parole chiave: Java 11, Spring Boot, Docker, ottimizzazione delle immagini
Parole chiave: Kubernetes, GKE, gestione delle risorse, autoscaling, segreti
Creazione di un chart con Helm 3 per una gestione più efficace del cluster
Parole chiave: Helm 3, distribuzione del chart
Configurazione di Jenkins e pipeline per la consegna automatica del codice nel cluster
Parole chiave: configurazione di Jenkins, plugin, repository di configurazioni separate
Intendo dedicare a ciascun passaggio un articolo separato.
L'orientamento di questa serie di articoli non è su come scrivere microservizi, ma su come farli funzionare all'interno di un sistema unificato. Anche se tutte queste cose normalmente sono al di fuori delle responsabilità dello sviluppatore, penso sia comunque utile avere una familiarità con esse per almeno il 20% (che, si sa, portano all'80% del risultato). Alcuni temi indubbiamente importanti, come la sicurezza, saranno esclusi da questo progetto, poiché l'autore non ne sa molto; il sistema è creato esclusivamente per uso personale. Sarò lieto di ricevere opinioni e critiche costruttive.
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 (così da avere qualcosa da testare in Jenkins). Il codice sorgente dei servizi è disponibile su GitHub: e .
Per poter controllare lo stato di ciascun servizio, è stato aggiunto Spring Actuator alle loro dipendenze. Questo creerà un endpoint /actuator/health e restituirà lo stato 200 se il servizio è pronto a ricevere traffico, o 504 in caso di problemi. In questo caso, si tratta di un controllo piuttosto fittizio, poiché i servizi sono molto semplici e, in caso di emergenza, diventeranno probabilmente completamente non disponibili piuttosto che mantenere una parziale funzionalità. Ma nei sistemi reali, Actuator può aiutare a diagnosticare un problema prima che gli utenti inizino a riscontrarlo. Ad esempio, se ci sono problemi di accesso al DB, possiamo reagire automaticamente interrompendo l'elaborazione delle richieste con un'istanza di servizio non funzionante.
Servizio Backend
Il servizio backend conterà e restituerà semplicemente 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 sul 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 backend, aggiungendo le seguenti informazioni:
- id del gateway. Questo è necessario per distinguere un'istanza di gateway da un'altra in base alla risposta del server.
- Un "segreto" che fungerà da una password molto importante (n° chiave di crittografia di un cookie importante)
Configurazione in application.properties:
backend.url=http://localhost:8081
instance.id=${random.int}
secret="default-secret"Adapter per la comunicazione 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:
Avviando il backend:
./mvnw package -DskipTests
java -Dserver.port=8081 -jar target/microservices-backend-1.0.0.jarAvviando il gateway:
./mvnw package -DskipTests
java -jar target/microservices-gateway-1.0.0.jarControlliamo:
$ curl http://localhost:8080/
Numero di richieste 1 (gateway 38560358, segreto "default-secret")Tutto funziona. Il lettore attento noterà che nulla ci impedisce di contattare il backend direttamente bypassando il gateway (). Per risolvere ciò, i servizi devono essere uniti in una rete unica, e all'esterno deve
essere visibile solo il gateway. Inoltre, entrambi i servizi condividono un filesystem, generano flussi e a un certo punto possono iniziare a interferire l'uno con l'altro. Sarebbe utile isolare i nostri microservizi. Questo può essere raggiunto tramite la distribuzione delle applicazioni su macchine diverse (molti costi, complesso), usando macchine virtuali (richiodevo di molte risorse, avvio lungo) oppure attraverso la containerizzazione. Naturalmente scegliamo la terza opzione e come strumento per la containerizzazione.
Docker
In sintesi, Docker crea contenitori isolati, uno per applicazione. Per usare 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 (n° ) e con 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ù rapidamente da un repository remoto, occupa meno spazio e il tuo servizio si avvia più velocemente. Qualsiasi immagine è costruita su un'immagine di base, ed è consigliato scegliere la versione più minimalista. Una buona opzione è Alpine — una distribuzione Linux completa con minimo pacchetti.
Per cominciare proviamo a scrivere un Dockerfile "a mano" (dico subito che questo è un modo sbagliato, 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 utilizziamo un'immagine di base basata su Alpine con già installato JDK per costruire il nostro progetto. Con il comando ADD aggiungiamo all'immagine la directory corrente src, la impostiamo come directory 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 permette di contattare l'applicazione, ad esempio, da un altro contenitore nella stessa rete Docker).
Per impacchettare i servizi in immagini è necessario eseguire i comandi dalla radice di ciascun progetto:
docker image build . -t msvc-backend:1.0.0Di conseguenza, otteniamo un'immagine delle dimensioni di 456 MB (di cui l'immagine di base JDK 340 ha occupato MB). E tutto ciò nonostante i classi nel nostro progetto si possano contare sulle dita di una mano. Per ridurre le dimensioni della nostra immagine:
- Utilizziamo una build multi-passaggio. Nel primo passaggio compiliamo il progetto, nel secondo installiamo JRE e nel terzo copiamo tutto in una nuova immagine Alpine pulita. Quindi, nell'immagine finale ci saranno solo i componenti necessari.
- Utilizziamo la modularizzazione di java. A partire da Java 9, è possibile utilizzare lo strumento jlink per creare JRE solo dai moduli necessari.
Per i curiosi, ecco un buon articolo sui metodi per ridurre le dimensioni dell'immagine. .
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 è dimagrita di 6 volte, raggiungendo 77 MB. Non male. Successivamente, le immagini pronte possono essere caricate nel registro delle immagini, affinché le vostre immagini siano disponibili per il download da Internet.
Esecuzione congiunta dei servizi in Docker
Per iniziare, i nostri servizi devono essere sulla stessa rete. In Docker esistono diversi tipi di reti, e utilizziamo il più semplice di tutti: bridge, che consente di unire i contenitori eseguiti su un'unica host. Creiamo la rete con il seguente comando:
docker network create msvc-networkDopo, avviamo il contenitore del backend con il nome 'backend' utilizzando l'immagine microservices-backend:1.0.0:
docker run -dit --name backend --network msvc-net microservices-backend:1.0.0Vale la pena notare che la rete bridge offre di default il service discovery per i contenitori in base ai loro nomi. Quindi, il servizio del backend sarà accessibile all'interno della rete Docker all'indirizzo .
Avviando 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.0In questo comando indichiamo che stiamo inoltrando la porta 80 del nostro host sulla porta 8080 del contenitore. Utilizziamo le opzioni env per impostare le variabili di ambiente, che saranno automaticamente lette da Spring e sovrascriveranno le proprietà in application.properties.
Dopo aver avviato, chiamiamo e ci assicuriamo che tutto funzioni come nel caso precedente.
Conclusione
In questo modo, abbiamo creato due semplici microservizi, li abbiamo imballati in contenitori Docker e li abbiamo avviati insieme su un'unica macchina. Tuttavia, il sistema risultante presenta diversi svantaggi:
- Scarsa tolleranza ai guasti — tutto funziona su un unico server
- Scarsa scalabilità — con l'aumento del carico sarebbe utile distribuire automaticamente ulteriori istanze dei servizi e bilanciare il carico tra di esse
- Complessità di avvio — ci sono volute almeno 3 comandi, e con parametri specifici (questo solo per 2 servizi)
Per risolvere i problemi sopracitati, esistono diverse soluzioni come Docker Swarm, Nomad, Kubernetes o OpenShift. Se l'intero sistema è scritto in Java, si può considerare Spring Cloud ().
In scriverò di come ho configurato Kubernetes e distribuito il progetto su Google Kubernetes Engine.
Fonte: habr.com
