CircuitBreaker

È comune per i sistemi software effettuare chiamate remote a software in esecuzione in processi diversi, probabilmente su macchine diverse attraverso una rete. Una delle grandi differenze tra le chiamate in memoria e le chiamate remote è che le chiamate remote possono fallire o bloccarsi senza una risposta fino al raggiungimento di un limite di timeout. Quel che è peggio se hai molti chiamanti su un fornitore che non risponde, allora puoi esaurire le risorse critiche che portano a errori a cascata su più sistemi. Nel suo eccellente libro Release It, Michael Nygard ha reso popolare il modello di interruttore automatico per prevenire questo tipo di cascata catastrofica.

L’idea di base dietro l’interruttore è molto semplice. Si avvolge una chiamata di funzione protetta in un oggetto interruttore automatico, che monitora i guasti. Una volta che i guasti raggiungono una certa soglia, l’interruttore scatta e tutte le ulteriori chiamate all’interruttore ritornano con un errore, senza che la chiamata protetta venga effettuata affatto. Di solito si vorrà anche una sorta di avviso di monitoraggio se i viaggi interruttore.

Ecco un semplice esempio di questo comportamento in Ruby, proteggendo dai timeout.

Ho impostato l’interruttore con un blocco (Lambda) che è la chiamata protetta.

cb = CircuitBreaker.new {|arg| @supplier.func arg}

L’interruttore memorizza il blocco, inizializza vari parametri (per soglie, timeout e monitoraggio) e ripristina l’interruttore nel suo stato chiuso.

classe CircuitBreaker…

 attr_accessor :invocation_timeout, :failure_threshold, :monitor def initialize &block @circuit = block @invocation_timeout = 0.01 @failure_threshold = 5 @monitor = acquire_monitor reset end

Chiamando l’interruttore automatico chiamerà il blocco sottostante se il circuito è chiuso, ma restituirà un errore se è aperto

# client code aCircuitBreaker.call(5)

CircuitBreaker di classe…

 def call args case state when :closed begin do_call args rescue Timeout::Error record_failure raise $! end when :open then raise CircuitBreaker::Open else raise "Unreachable Code" end end def do_call args result = Timeout::timeout(@invocation_timeout) do @circuit.call args end reset return result end

Se otteniamo un timeout, incrementiamo il contatore degli errori, le chiamate riuscite lo ripristinano a zero.

classe CircuitBreaker…

 def record_failure @failure_count += 1 @monitor.alert(:open_circuit) if :open == state end def reset @failure_count = 0 @monitor.alert :reset_circuit end

Determino lo stato dell’interruttore confrontando il conteggio dei guasti con la soglia

CircuitBreaker di classe…

 def state (@failure_count >= @failure_threshold) ? :open : :closed end

Questo semplice interruttore evita di effettuare la chiamata protetta quando il circuito è aperto, ma avrebbe bisogno di un intervento esterno per ripristinarlo quando le cose sono di nuovo bene. Questo è un approccio ragionevole con interruttori elettrici negli edifici, ma per gli interruttori software possiamo avere l’interruttore stesso rilevare se le chiamate sottostanti stanno funzionando di nuovo. Possiamo implementare questo comportamento di auto-ripristino riprovando la chiamata protetta dopo un intervallo adeguato e reimpostando l’interruttore in caso di successo.

Creare questo tipo di interruttore significa aggiungere una soglia per provare il reset e impostare una variabile per mantenere il tempo dell’ultimo errore.

classe ResetCircuitBreaker…

 def initialize &block @circuit = block @invocation_timeout = 0.01 @failure_threshold = 5 @monitor = BreakerMonitor.new @reset_timeout = 0.1 reset end def reset @failure_count = 0 @last_failure_time = nil @monitor.alert :reset_circuit end

Ora è presente un terzo stato – semiaperto – il che significa che il circuito è pronto per effettuare una chiamata reale come prova per vedere se il problema è stato risolto.

classe ResetCircuitBreaker…

 def state case when (@failure_count >= @failure_threshold) && (Time.now - @last_failure_time) > @reset_timeout :half_open when (@failure_count >= @failure_threshold) :open else :closed end end

Chiesto di chiamare nello stato semiaperto si traduce in una chiamata di prova, che ripristinerà l’interruttore in caso di successo o riavvierà il timeout in caso contrario.

classe ResetCircuitBreaker…

 def call args case state when :closed, :half_open begin do_call args rescue Timeout::Error record_failure raise $! end when :open raise CircuitBreaker::Open else raise "Unreachable" end end def record_failure @failure_count += 1 @last_failure_time = Time.now @monitor.alert(:open_circuit) if :open == state end

Questo esempio è un semplice esplicativo, in pratica gli interruttori forniscono un bel po ‘ più di funzionalità e parametrizzazione. Spesso proteggeranno da una serie di errori che la chiamata protetta potrebbe generare, come errori di connessione di rete. Non tutti gli errori dovrebbero inciampare nel circuito, alcuni dovrebbero riflettere i guasti normali e essere trattati come parte della logica regolare.

Con molto traffico, puoi avere problemi con molte chiamate che aspettano solo il timeout iniziale. Poiché le chiamate remote sono spesso lente, è spesso una buona idea mettere ogni chiamata su un thread diverso usando un futuro o promettere di gestire i risultati quando tornano. Disegnando questi thread da un pool di thread, è possibile organizzare l’interruzione del circuito quando il pool di thread è esaurito.

L’esempio mostra un modo semplice per far scattare l’interruttore: un conteggio che si ripristina in caso di chiamata riuscita. Un approccio più sofisticato potrebbe esaminare la frequenza degli errori, inciampando una volta ottenuto, ad esempio, un tasso di errore del 50%. Potresti anche avere soglie diverse per errori diversi, ad esempio una soglia di 10 per i timeout ma 3 per gli errori di connessione.

L’esempio che ho mostrato è un interruttore automatico per le chiamate sincrone, ma gli interruttori automatici sono utili anche per le comunicazioni asincrone. Una tecnica comune qui è mettere tutte le richieste su una coda, che il fornitore consuma alla sua velocità-una tecnica utile per evitare di sovraccaricare i server. In questo caso il circuito si rompe quando la coda si riempie.

Da soli, gli interruttori aiutano a ridurre le risorse legate alle operazioni che rischiano di fallire. Si evita di attendere i timeout per il client e un circuito rotto evita di caricare un server in difficoltà. Parlo qui di chiamate remote, che sono un caso comune per gli interruttori automatici, ma possono essere utilizzati in qualsiasi situazione in cui si desidera proteggere parti di un sistema da guasti in altre parti.

Gli interruttori sono un luogo prezioso per il monitoraggio. Qualsiasi cambiamento nello stato dell’interruttore dovrebbe essere registrato e gli interruttori dovrebbero rivelare i dettagli del loro stato per un monitoraggio più approfondito. Il comportamento dell’interruttore è spesso una buona fonte di avvertimenti su problemi più profondi nell’ambiente. Il personale operativo dovrebbe essere in grado di far scattare o ripristinare gli interruttori.

Gli interruttori da soli sono preziosi, ma i clienti che li utilizzano devono reagire ai guasti degli interruttori. Come con qualsiasi invocazione remota è necessario considerare cosa fare in caso di guasto. Fallisce l’operazione che stai eseguendo o ci sono soluzioni alternative che puoi fare? Un’autorizzazione della carta di credito potrebbe essere messa in coda per affrontare in seguito, la mancata acquisizione di alcuni dati potrebbe essere mitigata mostrando alcuni dati obsoleti che sono abbastanza buoni da visualizzare.

Ulteriori informazioni

Il blog tecnico netflix contiene molte informazioni utili sul miglioramento dell’affidabilità dei sistemi con molti servizi. Il loro comando di dipendenza parla dell’utilizzo di interruttori automatici e di un limite di pool di thread.

Netflix ha Hystrix open source, uno strumento sofisticato per gestire la latenza e la tolleranza ai guasti per i sistemi distribuiti. Esso include un’implementazione dell’interruttore modello con il pool di thread limite

Ci sono altre versioni open-source di interruttore automatico modello in Ruby, Java, Grails Plugin, C#, AspectJ, e Scala

Ringraziamenti

Pavel Shpak avvistato e segnalato un bug nel codice di esempio

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.