este obișnuit ca sistemele software să efectueze apeluri la distanță către software care rulează în diferite procese, probabil pe diferite mașini dintr-o rețea. Una dintre diferențele mari dintre apelurile în memorie și apelurile la distanță este că apelurile la distanță pot eșua sau se pot bloca fără răspuns până când se atinge o anumită limită de expirare. Ce este mai rău dacă aveți mulți apelanți pe un furnizor care nu răspunde, atunci puteți rămâne fără resurse critice care duc la eșecuri în cascadă pe mai multe sisteme. În cartea sa excelentă Release It, Michael Nygard a popularizat modelul întrerupătorului pentru a preveni acest tip de cascadă catastrofală.
ideea de bază din spatele întrerupătorului este foarte simplă. Înfășurați un apel de funcție protejat într-un obiect întrerupător de circuit, care monitorizează defecțiunile. Odată ce defecțiunile ating un anumit prag, întrerupătorul se declanșează și toate apelurile ulterioare către întrerupător revin cu o eroare, fără ca apelul protejat să fie efectuat deloc. De obicei, veți dori, de asemenea, un fel de alertă de monitor dacă întrerupătorul se declanșează.
Iată un exemplu simplu al acestui comportament în Ruby, protejând împotriva timeout-urilor.
am configurat întrerupătorul cu un bloc (Lambda) care este apelul protejat.
cb = CircuitBreaker.new {|arg| @supplier.func arg}
întrerupătorul stochează blocul, inițializează diverși parametri (pentru praguri, timeout-uri și monitorizare) și resetează întrerupătorul în starea sa închisă.
clasa 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
apelarea întrerupătorului va apela blocul de bază dacă circuitul este închis, dar returnați o eroare dacă este deschis
# client code aCircuitBreaker.call(5)
clasa CircuitBreaker…
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
ar trebui să ne un timeout, am incrementa contra eșec, apeluri de succes reseta înapoi la zero.
clasa 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
i determinați starea întrerupătorului comparând numărul de defecțiuni cu pragul
clasa CircuitBreaker…
def state (@failure_count >= @failure_threshold) ? :open : :closed end
acest întrerupător simplu evită efectuarea apelului protejat atunci când circuitul este deschis, dar ar avea nevoie de o intervenție externă pentru a-l reseta atunci când lucrurile sunt din nou bine. Aceasta este o abordare rezonabilă cu întrerupătoarele electrice din clădiri, dar pentru întrerupătoarele software putem avea întrerupătorul în sine să detecteze dacă apelurile subiacente funcționează din nou. Putem implementa acest comportament de auto-resetare încercând din nou apelul protejat după un interval adecvat și resetând întrerupătorul dacă va reuși.
crearea acestui tip de întrerupător înseamnă adăugarea unui prag pentru încercarea resetării și configurarea unei variabile pentru a menține timpul ultimei erori.
clasa 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
există acum o a treia stare prezentă – pe jumătate deschisă – ceea ce înseamnă că circuitul este gata să facă un apel real ca proces pentru a vedea dacă problema este rezolvată.
clasa 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
a cerut pentru a apela în rezultatele de stat semi-deschis într-un apel de încercare, care va reseta întrerupător dacă succes sau reporniți timeout dacă nu.
clasa 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
acest exemplu este unul explicativ simplu, în practică întreruptoarele oferă un pic mai multe caracteristici și parametrizare. Adesea, acestea vor proteja împotriva unei serii de erori pe care apelul protejat le-ar putea ridica, cum ar fi eșecurile conexiunii la rețea. Nu toate erorile ar trebui să declanșeze circuitul, unele ar trebui să reflecte eșecurile normale și să fie tratate ca parte a logicii obișnuite.
cu o mulțime de trafic, puteți avea probleme cu multe apeluri doar de așteptare pentru timeout inițială. Deoarece apelurile la distanță sunt adesea lente, este adesea o idee bună să puneți fiecare apel pe un fir diferit folosind un viitor sau o promisiune de a gestiona rezultatele atunci când revin. Desenând aceste fire dintr-un bazin de fire, puteți aranja ca circuitul să se rupă atunci când bazinul de fire este epuizat.
exemplul arată o modalitate simplă de a declanșa întrerupătorul — un număr care se resetează la un apel reușit. O abordare mai sofisticată ar putea privi frecvența erorilor, declanșând odată ce obțineți, să zicem, o rată de eșec de 50%. De asemenea, este posibil să aveți praguri diferite pentru erori diferite, cum ar fi un prag de 10 pentru timeout-uri, dar 3 pentru eșecuri de conexiune.
exemplul pe care l-am arătat este un întrerupător de circuit pentru apeluri sincrone, dar întrerupătoarele de circuit sunt utile și pentru comunicațiile asincrone. O tehnică comună aici este de a pune toate cererile pe o coadă, pe care Furnizorul o consumă la viteza sa – o tehnică utilă pentru a evita supraîncărcarea serverelor. În acest caz, circuitul se rupe atunci când coada se umple.
pe cont propriu, întrerupătoarele de circuit ajută la reducerea resurselor legate în operațiuni care sunt susceptibile de a eșua. Evitați să așteptați timeout-urile pentru client, iar un circuit rupt evită încărcarea pe un server care se luptă. Vorbesc aici despre apelurile la distanță, care sunt un caz obișnuit pentru întrerupătoarele de circuit, dar pot fi utilizate în orice situație în care doriți să protejați părți ale unui sistem de defecțiuni în alte părți.
întrerupătoarele de Circuit sunt un loc valoros pentru monitorizare. Orice modificare a stării întrerupătorului ar trebui să fie înregistrată, iar întrerupătoarele ar trebui să dezvăluie detalii despre starea lor pentru o monitorizare mai profundă. Comportamentul întrerupătorului este adesea o sursă bună de avertismente cu privire la problemele mai profunde din mediu. Personalul de operațiuni ar trebui să poată declanșa sau reseta întrerupătoarele.
întrerupătoarele pe cont propriu sunt valoroase, dar clienții care le folosesc trebuie să reacționeze la eșecurile întrerupătorului. Ca și în cazul oricărei invocări la distanță, trebuie să luați în considerare ce să faceți în caz de eșec. Nu reușește operațiunea pe care o efectuați sau există soluții pe care le puteți face? O autorizație de card de credit ar putea fi pus pe o coadă pentru a face față mai târziu, eșecul de a obține unele date pot fi atenuate prin afișarea unor date vechi, care este suficient de bun pentru a afișa.
lecturi suplimentare
blogul tehnic netflix conține o mulțime de informații utile despre îmbunătățirea fiabilității sistemelor cu o mulțime de servicii. Comanda lor de dependență vorbește despre utilizarea întreruptoarelor și a unei limite a bazinului de fire.
Netflix au Hystrix open-source, un instrument sofisticat pentru a face față latenței și toleranței la erori pentru sistemele distribuite. Acesta include o implementare a modelului întrerupător de circuit cu limita piscină fir
există alte implementări open-source ale modelului întrerupător de circuit în Ruby, Java, Grails Plugin, c#, AspectJ, și Scala
mulțumiri
Pavel Shpak reperat și a raportat o eroare în codul exemplu