Introduzione a Hystrix

1. Panoramica

Un tipico sistema distribuito è costituito da molti servizi che collaborano insieme.

Questi servizi sono soggetti a errori o risposte ritardate. Se un servizio non riesce, potrebbe influire su altri servizi che influiscono sulle prestazioni e possibilmente rendere inaccessibili altre parti dell'applicazione o, nel peggiore dei casi, arrestare l'intera applicazione.

Naturalmente, sono disponibili soluzioni che aiutano a rendere le applicazioni resilienti e tolleranti ai guasti: uno di questi framework è Hystrix.

La libreria del framework Hystrix aiuta a controllare l'interazione tra i servizi fornendo tolleranza ai guasti e tolleranza alla latenza. Migliora la resilienza complessiva del sistema isolando i servizi in errore e bloccando l'effetto a cascata dei guasti.

In questa serie di post inizieremo osservando come Hystrix viene in soccorso quando un servizio o un sistema si guasta e cosa può realizzare Hystrix in queste circostanze.

2. Semplice esempio

Il modo in cui Hystrix fornisce tolleranza agli errori e alla latenza è isolare e avvolgere le chiamate ai servizi remoti.

In questo semplice esempio racchiudiamo una chiamata nel metodo run () di HystrixCommand:

class CommandHelloWorld extends HystrixCommand { private String name; CommandHelloWorld(String name) { super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")); this.name = name; } @Override protected String run() { return "Hello " + name + "!"; } }

ed eseguiamo la chiamata come segue:

@Test public void givenInputBobAndDefaultSettings_whenCommandExecuted_thenReturnHelloBob(){ assertThat(new CommandHelloWorld("Bob").execute(), equalTo("Hello Bob!")); }

3. Installazione di Maven

Per utilizzare Hystrix in un progetto Maven, dobbiamo avere una dipendenza hystrix-core e rxjava-core da Netflix nel progetto pom.xml :

 com.netflix.hystrix hystrix-core 1.5.4  

L'ultima versione è sempre disponibile qui.

 com.netflix.rxjava rxjava-core 0.20.7 

L'ultima versione di questa libreria è sempre disponibile qui.

4. Configurazione del servizio remoto

Cominciamo simulando un esempio del mondo reale.

Nell'esempio seguente , la classe RemoteServiceTestSimulator rappresenta un servizio su un server remoto. Ha un metodo che risponde con un messaggio dopo un determinato periodo di tempo. Possiamo immaginare che questa attesa sia una simulazione di un processo che richiede tempo nel sistema remoto che si traduce in una risposta ritardata al servizio chiamante:

class RemoteServiceTestSimulator { private long wait; RemoteServiceTestSimulator(long wait) throws InterruptedException { this.wait = wait; } String execute() throws InterruptedException { Thread.sleep(wait); return "Success"; } }

Ed ecco il nostro client di esempio che chiama RemoteServiceTestSimulator .

La chiamata al servizio è isolata e racchiusa nel metodo run () di un HystrixCommand. È questo involucro che fornisce la resilienza di cui abbiamo parlato sopra:

class RemoteServiceTestCommand extends HystrixCommand { private RemoteServiceTestSimulator remoteService; RemoteServiceTestCommand(Setter config, RemoteServiceTestSimulator remoteService) { super(config); this.remoteService = remoteService; } @Override protected String run() throws Exception { return remoteService.execute(); } }

La chiamata viene eseguita chiamando il metodo execute () su un'istanza dell'oggetto RemoteServiceTestCommand .

Il test seguente mostra come eseguire questa operazione:

@Test public void givenSvcTimeoutOf100AndDefaultSettings_whenRemoteSvcExecuted_thenReturnSuccess() throws InterruptedException { HystrixCommand.Setter config = HystrixCommand .Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroup2")); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(100)).execute(), equalTo("Success")); }

Finora abbiamo visto come eseguire il wrapping delle chiamate del servizio remoto nell'oggetto HystrixCommand . Nella sezione seguente vediamo come affrontare una situazione in cui il servizio remoto inizia a deteriorarsi.

5. Lavorare con il servizio remoto e la programmazione difensiva

5.1. Programmazione difensiva con timeout

È prassi di programmazione generale impostare i timeout per le chiamate ai servizi remoti.

Cominciamo osservando come impostare il timeout su HystrixCommand e come aiuta cortocircuitando:

@Test public void givenSvcTimeoutOf5000AndExecTimeoutOf10000_whenRemoteSvcExecuted_thenReturnSuccess() throws InterruptedException { HystrixCommand.Setter config = HystrixCommand .Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest4")); HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter(); commandProperties.withExecutionTimeoutInMilliseconds(10_000); config.andCommandPropertiesDefaults(commandProperties); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(), equalTo("Success")); }

Nel test precedente, stiamo ritardando la risposta del servizio impostando il timeout a 500 ms. Stiamo anche impostando il timeout di esecuzione su HystrixCommand a 10.000 ms, consentendo così un tempo sufficiente per la risposta del servizio remoto.

Ora vediamo cosa succede quando il timeout di esecuzione è inferiore alla chiamata di timeout del servizio:

@Test(expected = HystrixRuntimeException.class) public void givenSvcTimeoutOf15000AndExecTimeoutOf5000_whenRemoteSvcExecuted_thenExpectHre() throws InterruptedException { HystrixCommand.Setter config = HystrixCommand .Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest5")); HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter(); commandProperties.withExecutionTimeoutInMilliseconds(5_000); config.andCommandPropertiesDefaults(commandProperties); new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(15_000)).execute(); }

Notare come abbiamo abbassato la barra e impostato il timeout di esecuzione a 5.000 ms.

Ci aspettiamo che il servizio risponda entro 5.000 ms, mentre abbiamo impostato il servizio in modo che risponda dopo 15.000 ms. Se noti quando esegui il test, il test terminerà dopo 5.000 ms invece di attendere 15.000 ms e genererà un'eccezione HystrixRuntimeException.

Ciò dimostra come Hystrix non attenda più del timeout configurato per una risposta. Ciò contribuisce a rendere il sistema protetto da Hystrix più reattivo.

Nelle sezioni seguenti esamineremo l'impostazione della dimensione del pool di thread che impedisce l'esaurimento dei thread e ne discuteremo i vantaggi.

5.2. Programmazione difensiva con pool di thread limitato

L'impostazione dei timeout per le chiamate di servizio non risolve tutti i problemi associati ai servizi remoti.

Quando un servizio remoto inizia a rispondere lentamente, un'applicazione tipica continuerà a chiamare quel servizio remoto.

L'applicazione non sa se il servizio remoto è integro o meno e vengono generati nuovi thread ogni volta che arriva una richiesta. Ciò causerà l'utilizzo di thread su un server già in difficoltà.

Non vogliamo che ciò accada poiché abbiamo bisogno di questi thread per altre chiamate o processi remoti in esecuzione sul nostro server e vogliamo anche evitare che l'utilizzo della CPU aumenti.

Vediamo come impostare la dimensione del pool di thread in HystrixCommand :

@Test public void givenSvcTimeoutOf500AndExecTimeoutOf10000AndThreadPool_whenRemoteSvcExecuted _thenReturnSuccess() throws InterruptedException { HystrixCommand.Setter config = HystrixCommand .Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupThreadPool")); HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter(); commandProperties.withExecutionTimeoutInMilliseconds(10_000); config.andCommandPropertiesDefaults(commandProperties); config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter() .withMaxQueueSize(10) .withCoreSize(3) .withQueueSizeRejectionThreshold(10)); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(), equalTo("Success")); }

Nel test precedente, stiamo impostando la dimensione massima della coda, la dimensione della coda principale e la dimensione del rifiuto della coda. Hystrix inizierà a rifiutare le richieste quando il numero massimo di thread avrà raggiunto 10 e la coda delle attività avrà raggiunto una dimensione di 10.

La dimensione del core è il numero di thread che rimangono sempre attivi nel pool di thread.

5.3. Programmazione difensiva con schema di cortocircuito

Tuttavia, c'è ancora un miglioramento che possiamo apportare alle chiamate di servizio remoto.

Consideriamo il caso in cui il servizio remoto abbia iniziato a fallire.

Non vogliamo continuare a sparare richieste e sprecare risorse. Idealmente vorremmo smettere di fare richieste per un certo periodo di tempo per dare al servizio il tempo di recuperare prima di riprendere le richieste. Questo è ciò che viene chiamato lo schema dell'interruttore di cortocircuito .

Vediamo come Hystrix implementa questo pattern:

@Test public void givenCircuitBreakerSetup_whenRemoteSvcCmdExecuted_thenReturnSuccess() throws InterruptedException { HystrixCommand.Setter config = HystrixCommand .Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupCircuitBreaker")); HystrixCommandProperties.Setter properties = HystrixCommandProperties.Setter(); properties.withExecutionTimeoutInMilliseconds(1000); properties.withCircuitBreakerSleepWindowInMilliseconds(4000); properties.withExecutionIsolationStrategy (HystrixCommandProperties.ExecutionIsolationStrategy.THREAD); properties.withCircuitBreakerEnabled(true); properties.withCircuitBreakerRequestVolumeThreshold(1); config.andCommandPropertiesDefaults(properties); config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter() .withMaxQueueSize(1) .withCoreSize(1) .withQueueSizeRejectionThreshold(1)); assertThat(this.invokeRemoteService(config, 10_000), equalTo(null)); assertThat(this.invokeRemoteService(config, 10_000), equalTo(null)); assertThat(this.invokeRemoteService(config, 10_000), equalTo(null)); Thread.sleep(5000); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(), equalTo("Success")); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(), equalTo("Success")); assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(), equalTo("Success")); }
public String invokeRemoteService(HystrixCommand.Setter config, int timeout) throws InterruptedException { String response = null; try { response = new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(timeout)).execute(); } catch (HystrixRuntimeException ex) { System.out.println("ex = " + ex); } return response; }

Nel test sopra abbiamo impostato diverse proprietà dell'interruttore. I più importanti sono:

  • Il CircuitBreakerSleepWindow che è impostato a 4.000 ms. Questo configura la finestra dell'interruttore e definisce l'intervallo di tempo dopo il quale verrà ripresa la richiesta al servizio remoto
  • Il CircuitBreakerRequestVolumeThreshold che è impostato a 1 e definisce il numero minimo di richieste necessari prima sarà considerato il tasso di fallimento

Con le impostazioni di cui sopra in atto, il nostro HystrixCommand ora si aprirà dopo due richieste non riuscite. La terza richiesta non raggiungerà nemmeno il servizio remoto anche se abbiamo impostato il ritardo del servizio su 500 ms, Hystrix andrà in cortocircuito e il nostro metodo restituirà null come risposta.

Successivamente aggiungeremo un Thread.sleep (5000) per oltrepassare il limite della finestra di sonno che abbiamo impostato. Ciò farà sì che Hystrix chiuda il circuito e le richieste successive scorreranno correttamente.

6. Conclusione

In sintesi, Hystrix è progettato per:

  1. Fornisce protezione e controllo su errori e latenza dai servizi a cui si accede normalmente sulla rete
  2. Interrompere la cascata di errori derivanti dall'inattività di alcuni servizi
  3. Fallire velocemente e recuperare rapidamente
  4. Degradare con grazia, ove possibile
  5. Monitoraggio in tempo reale e avviso del centro di comando in caso di guasti

Nel prossimo post vedremo come combinare i vantaggi di Hystrix con il framework Spring.

Il codice completo del progetto e tutti gli esempi possono essere trovati nel progetto GitHub.