În ultimii ani, popularitatea microserviciilor a fost notabilă. O dată cu creșterea cererii de a îndeplini cerințe non funcționale precum performanță, toleranță la erori și scalabilitate, un pas natural a fost observarea și chiar experimentarea pe propria piele a diverselor povești de succes care au presupus ori migrarea monoliților către arhitecturi bazate pe microservicii, ori adoptarea lor încă de la primele etape ale proiectelor. Cu toate acestea, dacă ați ținut pasul cu trendurile și ați fost direct implicați în dezvoltarea microserviciilor, cu siguranță știți că nu totul e așa de simplu cum pare. O arhitectură compusă din servicii multiple care comunică și colaborează între ele pentru a atinge scopul produsului va complica activitățile care erau mai facile în cazul monoliților, precum gestionarea configurărilor, logarea centralizată sau monitorizarea. Vom aborda aceste complexități și vom demonstra cum unele componente Spring Cloud pot să vă facă viața mai ușoară, în special în cazul aplicațiilor care rulează în JVM, scrise în Java sau Kotlin.
Spring este frameworkul de referință când vine vorba de proiecte Java web, cu o rată de adoptare de peste 90%. Motivul e unul simplu: s-a concentrat de-a lungul anilor să ofere soluții lightweight, ușor de integrat pentru o varietate de probleme. Spring Cloud este un set de unelte care oferă soluții out of the box pentru cele mai întâlnite provocări din cadrul sistemelor distribuite, cum ar fi gestionarea configurărilor, service discovery, circuit breakers sau mesajele distribuite. Se bazează pe proiecte multiple, oferindu-ne flexibilitatea de a folosi doar ce e necesar.
Diagrama de mai jos prezintă o arhitectură bazată pe microservicii. Vom intra în detalii despre cum se poate ajunge la un sistem similar având un minim de configurări necesare, în special în faza inițială de dezvoltare, folosind Spring Cloud Config, Spring Cloud Stream și Spring Cloud Sleuth.
O dată ce aplicația este împărțită în microservicii multiple, fiecare având configurările specifice, corespunzătoare diferitelor tipuri de medii în care sunt lansate, ultimul lucru pe care ni-l dorim e să manipulăm și să actualizăm aceste configurări separat pentru fiecare instanță a aplicației. Aici intervine Spring Cloud Config care oferă suport client-side și server-side pentru externalizarea configurărilor în sisteme distribuite. Conceptul este bazat pe al treilea punct din twelve-factor app, care abordează necesitatea stocării configurărilor în environment. Printre acestea putem menționa setări corespunzătoare bazelor de date sau a altor servicii folosite, sau credențialele serviciilor externe.
Serverul Spring Config este așadar o aplicație externă care gestionează configurările pentru toți clienții săi, care pot fi la rândul lor alte aplicații Spring, dar și aplicații scrise în alte limbaje de programare. Acesta asigură că toate configurările vor fi gestionate într-un singur loc și că fiecare aplicație va avea configurările corespunzătoare aferente fiecărui mediu. Implementarea implicită a serverului utilizează Git, dar alte implementări pot fi de asemenea ușor integrate. De asemenea, serverul expune un API pentru aceste configurări. Un caz de bază de utilizare a lui se poate vedea mai jos.
Setarea serverului este destul de facilă. Tot ce trebuie este o aplicație Spring Boot căreia i se adaugă dependința la spring-cloud-config-server. Apoi clasei principale i se pune adnotarea de @EnableConfigServer. Urmează editarea proprietăților aplicației din fișierele application.properties sau application.yml corespunzătoare.
Portul pe care clienții vor căuta configurări externe, dacă există dependința în cadrul proiectului client va fi 8888. Ultimul pas este configurarea locației serverului spring.cloud.config.server.git.uri.
server.git.uri.
server:
port:8888
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://github.com/spring-cloud-samples/config-repo
În repository-ul configurat, se pot adăuga configurările corespunzătoare pentru aplicațiile client.
Pentru aplicațiile client, trebuie adăugată dependința la spring-cloud-conflig-client și setat fișierul bootstrap.yml în resurse unde vor fi definite numele aplicației client, cât și URI-ul serverului Spring Cloud, pentru fiecare profil. În final, configurările dorite pot fi injectat după procedeul normal, folosind adnotările cunoscute: @Configuration
, @ConfigurationProperties
sau @Value
.
Un mod din ce în ce mai popular de a aborda comunicarea între microservicii este utilizarea de evenimente. Aceasta strategie tehnică are specificitățile și chiar disciplina ei proprie. Comunicarea bazată pe evenimente este o temă abordată de Spring, iar o evoluție a abilităților expuse de Spring e reprezentată de Spring Cloud Stream. Acest framework permite un model de programare unificat care poate fi adoptat în scopul de a ajunge la o arhitectură bazată pe evenimente și la coreografia lor. Există o gamă largă de aspecte care trebuie să fie luate în considerare la o abordare bazată pe evenimente. Ce tehnologii folosim pentru a trata fluxul de evenimente? Cum vor fi tratate serializarea și structura mesajelor? Pe lângă aceste întrebări inițiale, există și alte subiecte tehnice care trebuie stabilite: tratarea excepțiilor, configurările librăriilor client care vor interacționa cu brokerul de mesaje, cât și alte cerințe non funcționale care ar putea să ne influențeze decizia către o tehnologie sau alta.
Spring Cloud Stream permite un mod unificat de a aborda publicarea și consumarea de evenimente. Acesta vine cu suport pentru cele mai populare tehnologii de mesaje (Kafka, RabbitMQ), dar și altele soluții bazate pe Cloud mai specifice, cum ar fi Amazon Kinesis. Framework-ul expune aplicației niște abstractizări care ascund specificitatea unei tehnologii particulare de mesaje. Un lucru important de menționat este faptul că frameworkul nu încearcă să reducă abilitățile diverselor tehnologii către un set comun, ci mai degrabă ne permite să le configurăm și să le folosim într-un mod unificat. De exemplu, dacă un broker de mesaje are o anumită abilitate specifică, aceasta poate fi folosită în continuare utilizând un set adițional de proprietăți, pe lângă cele de bază. Frameworkul reduce efortul de configurare și înlătura codul boilerplate, lăsând doar dezvoltarea logicii de producere și consumare a mesajelor. În acest mod, accelerează timpul de setare al proiectului și reduce necesitatea elaborării unor configurări complexe încă de la început.
Arhitectura internă a frameworkului urmează o paradigmă similară cu cea prezentată mai sus. Creează linkuri reprezentate de canale de intrare și ieșire între aplicații și binder. Binderul este un concept abstract, central frameworkului. Implementarea sa creează o comunicare cu lumea externă prin brokerul de mesaje. Specificitățile diferitelor tehnologii de mesaje sunt manipulate de implementarea binderului. Canalele reprezintă corelări către destinații externe (topicuri, cozi) și sunt folosite să paseze date între binder și aplicație. Implementările curente pentru binder sunt: RabbitMQ, Apache Kafka, Amazon Kinesis, Google PubSub, Solace PubSub+, Azure Event Hubs, Apache RocketMQ.
spring:
cloud:
stream:
kafka:
binder:
brokers:
- localhost:9092
bindings:
orderValidated:
destination: order_validated
content-type: application/json
group: group-1
orderCreated:
destination: order_created
group: group-1
content-type: application/json
Frameworkul permite utilizarea proprietăților pentru a crea configurările destinațiilor de mesaje. Exemplul de mai sus creează două legături, una către topicul order_created, cealaltă către order_validated. Acestea leagă aplicația de topicurile externe din Kafka. Pentru a crea un consumator, trebuie să fie configurat un canal de intrare. Acesta se va lega de o destinație specifică declarată în proprietăți. @EnableBinding marchează configurarea și trebuie să referențieze o clasă care conține definiția canalului de intrare. Canalul este declarat folosind adnotarea de @Input.
@EnableBinding(Configuration.class)
public class EventProcessor {
private Configuration configuration;
@StreamListener(EventProcessor.ORDER_VALIDATED)
public void eventHandler(OrderValidated event) {
log.info(event);
}
public void eventPublisher(OrderCreated event) {
configuration.orderCreated()
.send(MessageBuilder
.withPayload(event)
.build());
}
}
public interface Configuration {
String ORDER_VALIDATED = "orderValidated";
String ORDER_CREATED = "orderCreated";
@Input
SubscribableChannel orderValidated();
@Output
MessageChannel orderCreated();
}
Abordarea este similară la crearea canalelor de ieșire. Acestea sunt legături către destinațiile care vor fi folosite de emițătorii de evenimente. Frameworkul permite automat componentelor să paseze evenimentele către canalele specifice. Acestea variază de la publicări simple de evenimente până la cazuri mai complexe, care pot implica rutare dinamică și interpretarea payload-ului.
În ultimele iterații ale frameworkului, focusul a fost schimbat de la modelul bazat pe adnotări la paradigmele funcționale. Acestea permit declararea unor consumatori și producători utilizând componente funcționale, astfel reducând și mai mult numărul de configurări necesare anterior.
@Bean
public Consumer eventConsumer() {
return event -> {
log.info(event);
};
}
Frameworkul are de asemenea suport pentru programarea reactive și integrare cu Project Reactor. Poate fi folosit atât prin abordarea bazată pe adnotări, cât și cea funcțională.
@Bean
public Consumer> eventConsumer() {
return eventFlux -> {
eventFlux.subscribe(e -> log.info(e));
}
}
Modelul de programare reactive poate fi înglobat în totalitate în aplicații. În acest fel se profită de beneficiile aduse de acesta, cum ar fi manipularea backpressure. Frameworkul acționează ca un proxy între diferiți brokeri de mesaje, controlând fluxul și ritmul evenimentelor chiar și pentru modele push în care mesajele sunt livrate consumatorilor. Fiind construit în jurul a Enterprise Integration Patterns, permite o flexibilitate mare în a implementa scenarii complexe și se integrează bine cu alte concepte din ecosistemul Spring.
Unul din dezavantajele unei aplicații împărțite sub o structură de microservicii este complexitatea ridicată a monitorizării. Având componente multiple și diferite moduri de comunicare între ele face ca trasabilitatea unei operațiuni să fie mult mai dificilă. Spring Cloud Sleuth este un proiect care vine în ajutorul dezvoltatorilor prin implementarea unui sistem distribuit de urmărire.
Spring Cloud Sleuth împarte fiecare operație într-o unitate de lucru de bază numită Span. Un span are un id unic pe 64 de biți. Un set de spanuri aferent unei singure operații se numește Trace. Trimiterea unui request HTTP către un microserviciu alcătuiește un span în serviciul trimițător, iar răspunsul la request este alt span în serviciul receptor, dar ambele spanuri se află în același trace.
Pentru a activa Spring Cloud Sleuth, e nevoie ca dependința la spring-cloud-starter-sleuth să fie adăugată în aplicația Spring Boot. În momentul rulării aplicației se vor vedea în loguri ataşate ca prefix trace id si span id, iar prin selectarea unui anumit trace id, se pot vizualiza logurile unei întregi operații de-a lungul mai multor microservicii, requesturi și evenimente.
INFO [order-service,1,1,true] [SomeThread-1] First log in a request, order service
INFO [order-service,1,1,true] [SomeThread-4] Second log in the same request, order service
INFO [email-service,1,2,true] [SomeThread-7] Log in email service, request in same operation, same trace id, different span id
INFO [email-service,4,1,true] [SomeThread-5] Log in email service, request unrelated, different trace id, different span id
Folosind Spring Cloud Sleuth, trace-urile pot fi generate și colectate ca unități compatibile cu Zipkin prin HTTP. În mod implicit, aplicația care folosește Sleuth le trimite către un serviciu colector Zipkin pe localhost.
Spring Cloud Sleuth este totodată compatibil cu Spring Integration și va trimite automat date aferente trace-urilor ca headere în mesaje.
Acestea sunt doar câteva din componentele utile Spring Cloud și am acoperit astfel unele din conceptele care aduc dificultate arhitecturilor bazate pe microservicii: configurări, comunicare bazată pe mesaje și monitorizare. Alte componente ale Spring Cloud care pot ajuta în dezvoltarea unor sisteme sigure sunt: Spring Cloud Netflix, Spring Cloud Gateway, Spring Cloud OpenFeign sau Spring Cloud Bus. Frumusețea frameworkului Spring constă în varietatea mare de opțiuni pe care le da, totodată păstrând flexibilitatea care permite alegerea și integrarea componentelor care se potrivesc cel mai bine pentru aplicația curentă.
de Mircea Vădan
de Ovidiu Mățan
de Diana Țelman