TSM - Fault-Tolerant Microservices cu Netflix Hystrix

Radu Butnaru - Senior Developer @ SDL

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.

Problema

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.

Soluția

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.

Circuit Breaker-ul 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.

Starea Închis

Î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.

Starea Deschis

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:

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:

Starea Semi-deschis

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.

Folosire

Vom prezenta două modalități de a integra biblioteca Hystrix în proiecte:

  1. Direct folosind API-ul Hystrix - necesită implementarea și invocarea de comenzi Hystrix pentru fiecare apel de serviciu.
  2. Folosind bibliotecile Spring Cloud Netflix și Javanica - o modalitate de a folosi Hystrix cu impact mai redus asupra codului de proiect, prin adnotarea metodelor ce apelează servicii.

API Hystrix direct

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.

Spring Cloud Netflix / Javanica

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

Monitorizare cu Hystrix Dashboard / Turbine

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:

Circuit Breaker Închis

Circuit Breaker Deschis

Următoarele măsurători sunt arătate și actualizate în timp real:

Pe wiki-ul Hystrix Dashboard se poate consulta documentația necesară interpretării diagramelor și contoarelor.

Concluzie

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.

Bibliografie

  1. Pattern-ul Circuit Breaker- autor Martin Fowler
  2. Proiectul Hystrix
  3. Wiki-ul Hystrix
  4. Proiectul Spring Cloud Netflixh
  5. Proiectul Javanica
  6. Proiectul Hystrix Dashboard
  7. Wiki-ul Hystrix Dashboard
  8. Proiectul Turbine
  9. Proiectul Archaius
  10. Prezentare Hystrix JavaOne- autor Ben Christensen