Acest articol continuă seria destinată soluțiilor aplicate într-un sistem construit folosind o arhitectură bazată pe Microservicii. Articolul precedent a tratat (Micro)service Discovery cu Netflix Eureka. Articolul curent prezintă biblioteca Java Hystrix, dezvoltată în regim open-source de către compania Netflix. Hystrix oferă o implementare matură a pattern-ului Circuit Breaker, al cărui scop este să reducă impactul defectărilor și al timpilor mari de latență în sisteme distribuite.
O caracteristică principală a sistemelor construite pe bază de microservicii este utilizarea unui număr mare de componente distribuite. Pe măsură ce numărul de interacțiuni sincrone prin rețea crește, impactul unei defectări a unui serviciu poate deveni din ce în ce mai sever.
Enumerăm câteva din cazurile cele mai frecvente de comportament anormal ale unui serviciu:
Fără mecanisme de protecție, erorile și în mod special timpii mari de latență se vor propaga către clienții serviciului, unde se va putea ajunge în situația ca resurse de sistem limitate să fie epuizate (de exemplu, pool-ul de thread-uri al serverului web). Prin escaladarea erorilor, disponibilitatea (engl. availability) sistemului este afectată în mod semnificativ: întregul sistem poate deveni indisponibil din cauzei unei singure dependențe defecte, deși restul serviciilor de care depinde, funcționează corect.
Un Circuit Breaker (rom. Întrerupător de circuit) este folosit pentru a intermedia operațiile prin rețea dintre un client și un serviciu. Circuit Breaker-ul monitorizează și detectează când serviciul invocat se comportă anormal, respingând apelurile către serviciu până când acesta va deveni funcțional din nou. Întorcând o eroare imediată, se previne epuizarea resurselor din procesul client. În același timp, se reduce încărcarea serviciului invocat, crescând astfel șansele ca acesta să își revină din condiția defectă.
În secțiunile care urmează, vom analiza implementarea pattern-ului Circuit Breaker din biblioteca Hystrix.
Să presupunem că un client invocă un serviciu. Clientul va izola toate punctele de acces către serviciu prin efectuarea tuturor apelurilor prin intermediul unui Circuit Breaker (aceasta se realizează la nivel de cod prin extinderea de clase Hystrix sau prin adnotări - detalii în cele ce urmează). Circuit Breaker-ul va intercepta și monitoriza toate apelurile și va acționa în cazul unor condiții de eroare, efectuând tranzițiile de stare descrise mai jos.
În cazul de funcționare normal când nu există condiții de eroare, Circuit Breaker-ul este în starea închis. Toate apelurile sunt transmise în mod transparent către serviciu.
Circuit Breaker-ul consideră următoarele condiții drept simptome ale unei defectări și le va lua în calcul pentru a decide întreruperea circuitului:
O excepție este returnată (de ex., eroare de conectare sau serviciul întoarce codul HTTP 500).
Apelul durează mai mult decât timpul de expirare configurat (valoarea implicită este de 1 secundă).
Circuitul este întrerupt de îndată ce Hystrix determină că pragul de erori de pe parcursul unei ferestre de timp statistice a fost atins ( implicit, 50% erori pe parcursul unei perioade de timp de 10 secunde). În starea deschis, Circuit Breaker-ul va respinge apeluri prin:
întoarcerea unei excepții (comportamentul implicit, denumit și "fail fast"),
Pentru a permite recuperarea din condiția de eroare, atunci când Circuit Breaker-ul se află în starea deschis, el va permite în mod periodic câte un apel, la un interval configurabil (implicit, 5 secunde) - aceasta este starea semi-deschis. Dacă apelul se efectuează cu succes, circuitul se va închide din nou.
Vom prezenta două modalități de a integra biblioteca Hystrix în proiecte:
Pentru a folosi biblioteca Hystrix, trebuie adăugată următoare dependință în proiectul Maven:
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.3.20</version>
</dependency>
Pentru a proteja un apel de serviciu cu un Circuit Breaker, trebuie extinsă clasa HystrixCommand. Exemplul fictiv de mai jos apelează un serviciu de produse:
public class FindAllProductsCommand extends HystrixCommand> {
private RestTemplate restTemplate;
public FindAllProductsCommand(
RestTemplate restTemplate) {
super(HystrixCommandGroupKey.Factory
.asKey("ProductGroup"));
this.restTemplate = restTemplate;
}
@Override
protected List run() throws Exception {
// Apel serviciu HTTP
ResponseEntity responseEntity =
restTemplate.getForEntity(
"http://host/products", Product[].class);
Product[] products = responseEntity.getBody();
return Arrays.asList(products);
}
}
Pentru a invoca clasa comandă, ea trebuie instanțiată și apoi apelată metoda execute()
:
new FindAllProductsCommand(productService).execute();
Pentru a întoarce un rezultat predefinit în locul unei excepții atunci când Circuit Breaker-ul este deschis, suprascriem metoda getFallback() în implementarea comenzii:
public class FindAllProductsCommand extends HystrixCommand> {
...
@Override
protected List getFallback() {
return Collections.emptyList();
}
}
În cazul în care un anumit tip de eroare este considerat comportament așteptat/tratabil (de ex. validări de logică business), tipul excepției întoarse trebuie să fie HystrixBadRequestException. În caz contrar, excepția va fi tratată ca simptom al unui comportament defectuos.
public class FindAllProductsCommand extends HystrixCommand> {
...
@Override
protected List run() throws Exception {
try {
// Apel serviciu HTTP
...
} catch (IllegalArgumentException e) {
// Dacă se întoarce HystrixBadRequestException,
// Circuit Breaker-ul nu se va deschide
throw new HystrixBadRequestException(
"Bad request.", e);
}
}
}
Valorile specifice de configurări (timpi de expirare, capacitatea pool-ului de thread-uri, praguri procentuale de eroare, etc.), pot fi atribuite programatic la momentul instanțierii comenzii.
new FindAllProductsCommand(HystrixCommand.Setter.
withGroupKey(HystrixCommandGroupKey.Factory
.asKey("ProductGroup")).
andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(20)
.withCircuitBreakerErrorThresholdPercentage(50)
.withExecutionIsolationThreadTimeout-
InMilliseconds(1000)
.withMetricsRollingStatisticalWindow
InMilliseconds(10000)
.withMetricsRollingStatistical
WindowBuckets(10))
.andThreadPoolPropertiesDefaults(
HystrixThreadPoolProperties.Setter()
.withCoreSize(10)), restTemplate)
.execute();
Alternativ, pentru configurări se poate folosi biblioteca Netflix Archaius.
Biblioteca Spring Cloud a fost prezentată în articolul precedent din serie. Spring Cloud este construită pe baza Spring Boot și furnizează interfețe abstracte pentru tehnologia din stiva open-source Netflix. Suportul pentru Hystrix se bazează pe biblioteca third-party Javanica.
Pentru a utiliza suportul Spring Cloud Netflix / Javanica, trebuie adăugată următoarea dependință în proiectul Maven:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
<version>1.0.0.RELEASE</version>
</dependency>
În plus, adăugăm adnotarea EnableCircuitBreaker pe clasa principală de configurare Spring Boot:
@EnableCircuitBreaker
public class HystrixClientDemoApp {
...
}
Pentru a proteja un apel de serviciu cu un Circuit Breaker, este suficient să se adauge adnotarea HystrixCommand pe metoda respectivă:
@HystrixCommand
public List findAllProducts() {
// Apel serviciu HTTP
ResponseEntity responseEntity = restTemplate.getForEntity("http://host/products", Product[].class);
Product[] products = responseEntity.getBody();
return Arrays.asList(products);
}
Pentru a întoarce un rezultat predefinit în locul unei excepții atunci când Circuit Breaker-ul este deschis, trebuie menționată metoda de fallback în adnotare:
@HystrixCommand(fallbackMethod = "defaultProducts")
public List findAllProducts() {
// Apel serviciu HTTP
...
}
public List defaultProducts() {
return Collections.emptyList();
}
Dacă se dorește ca un anumit tip de excepție să nu fie considerat simptom al unei defectări, tipul excepției trebuie menționat în adnotare:
@HystrixCommand(ignoreExceptions = {IllegalArgumentException.class})
public List findAllProducts() {
// Apel serviciu HTTP
...
}
Pentru configurări (timpi de expirare, capacitatea pool-ului de thread-uri, praguri procentuale de eroare, etc.), se poate utiliza mecanismul standard Spring Boot de configurare în fișierul application.yml:
hystrix:
command:
findAllProducts:
execution:
isolation:
thread:
timeoutInMilliseconds: 1000
circuitBreaker:
requestVolumeThreshold: 20
errorThresholdPercentage: 50
metrics:
rollingStats:
timeInMilliseconds: 10000
numBuckets: 10
threadpool:
ProductService:
coreSize: 10
Hystrix oferă suport pentru vizualizarea și monitorizarea stării curente a Circuit Breaker-elor prin trimiterea continuă de măsurători către o aplicație web tip panou de comandă: Hystrix Dashboard. Pentru scenarii cu servere multiple (cluster) Hystrix oferă posibilitatea de a trimite măsurătorile unui agregator intermediar: Turbine, înainte ca acestea să ajungă la Hystrix Dashboard.
Capturile de ecran de mai jos prezintă Hystrix Dashboard:
Următoarele măsurători sunt arătate și actualizate în timp real:
Indicatori privind starea de sănătate și volumul de trafic;
Rata de cereri (la nivel de server și cluster);
Procentul de erori și contoare (apeluri cu succes, apeluri respinse, expirări thread-uri, respingeri din cauza capacității pool-ului de thread-uri, erori/excepții) cumulative pe perioada ferestrei de timp curente (în exemplele de capturi de ecran de mai sus, în ultimele 20 de secunde);
Starea Circuit-breaker-ului;
Procente de timpi de latență pe parcursul ultimului minut;
Pe wiki-ul Hystrix Dashboard se poate consulta documentația necesară interpretării diagramelor și contoarelor.
Netflix Hystrix este o implementare matură a pattern-ului Circuit Breaker, configurabilă în detaliu, cu suport solid pentru vizualizare și monitorizare. Bibliotecile Spring Cloud Netflix/Javanica oferă o alternativă de folosire bazată pe adnotări, cu un impact mai redus asupra codului de proiect.
de Ovidiu Deac