CircuitBreaker

det er almindeligt, at systemer foretager fjernopkald til programmer, der kører i forskellige processer, sandsynligvis på forskellige maskiner på tværs af et netværk. En af de store forskelle mellem opkald i hukommelsen og fjernopkald er, at fjernopkald kan mislykkes eller hænge uden svar, indtil en vis timeoutgrænse er nået. Hvad er værre, hvis du har mange opkald på en leverandør, der ikke reagerer, kan du løbe tør for kritiske ressourcer, der fører til kaskadefejl på tværs af flere systemer. I sin fremragende bog Release it, Michael Nygard populariserede Afbrydermønsteret for at forhindre denne form for katastrofal kaskade.

grundideen bag afbryderen er meget enkel. Du pakker et beskyttet funktionsopkald ind i et afbryderobjekt, der overvåger for fejl. Når fejlene når en bestemt tærskel, Afbryder afbryderen, og alle yderligere opkald til afbryderen vender tilbage med en fejl, uden at det beskyttede opkald overhovedet foretages. Normalt vil du også have en slags skærmalarm, hvis afbryderen kører.

her er et simpelt eksempel på denne adfærd i Ruby, der beskytter mod timeouts.

jeg satte afbryderen op med en blok (Lambda), som er det beskyttede opkald.

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

afbryderen gemmer blokken, initialiserer forskellige parametre (for tærskler, timeouts og overvågning) og nulstiller afbryderen til dens lukkede tilstand.

klasse 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

opkald til afbryderen vil kalde den underliggende blok, hvis kredsløbet er lukket, men returnere en fejl, hvis den er åben

# client code aCircuitBreaker.call(5)

klasse Kredsløbbryder…

 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

skal vi få en timeout, øger vi fejltælleren, vellykkede opkald nulstiller den tilbage til nul.

klasse 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

jeg bestemmer tilstanden for afbryderen, der sammenligner fejltællingen med tærsklen

klasse Kredsløbbryder…

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

denne enkle Afbryder undgår at foretage det beskyttede opkald, når kredsløbet er åbent, men har brug for en ekstern indgriben for at nulstille det, når tingene er godt igen. Dette er en rimelig tilgang med elektriske afbrydere i bygninger, men for afbrydere kan vi få selve afbryderen til at registrere, om de underliggende opkald fungerer igen. Vi kan implementere denne selvindstillende adfærd ved at prøve det beskyttede opkald igen efter et passende interval, og nulstilling af afbryderen, hvis det lykkes.

oprettelse af denne form for breaker betyder at tilføje en tærskel for at prøve nulstillingen og oprette en variabel for at holde tiden for den sidste fejl.

klasse 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

der er nu en tredje tilstand til stede – halv åben – hvilket betyder, at kredsløbet er klar til at foretage et rigtigt opkald som prøve for at se, om problemet er løst.

klasse 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

bedt om at ringe i halvåben tilstand resulterer i et prøveopkald, som enten nulstiller afbryderen, hvis det lykkes, eller genstarter timeout, hvis ikke.

klasse 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

dette eksempel er en simpel forklarende, i praksis giver afbrydere en god smule flere funktioner og parameterisering. Ofte vil de beskytte mod en række fejl, som beskyttet opkald kunne hæve, såsom netværksforbindelsesfejl. Ikke alle fejl skal rejse kredsløbet, nogle skal afspejle normale fejl og behandles som en del af almindelig logik.

med masser af trafik kan du have problemer med mange opkald, der bare venter på den første timeout. Da fjernopkald ofte er langsomme, er det ofte en god ide at sætte hvert opkald på en anden tråd ved hjælp af en fremtid eller lover at håndtere resultaterne, når de kommer tilbage. Ved at tegne disse tråde fra en trådpulje kan du sørge for, at kredsløbet går i stykker, når trådpuljen er opbrugt.

eksemplet viser en enkel måde at slå breakeren på — en optælling, der nulstilles ved et vellykket opkald. En mere sofistikeret tilgang kan se på hyppigheden af fejl, snuble, når du får en 50% fejlrate. Du kan også have forskellige tærskler for forskellige fejl, såsom en tærskel på 10 for timeouts, men 3 for forbindelsesfejl.

det eksempel, Jeg har vist, er en afbryder til synkrone opkald, men afbrydere er også nyttige til asynkron kommunikation. En almindelig teknik her er at sætte alle anmodninger i kø, som leverandøren bruger med sin hastighed – en nyttig teknik til at undgå overbelastning af servere. I dette tilfælde går kredsløbet i stykker, når køen fyldes op.

på egen hånd hjælper afbrydere med at reducere ressourcer bundet i operationer, der sandsynligvis vil mislykkes. Du undgår at vente på timeouts for klienten, og et brudt kredsløb undgår at lægge belastning på en kæmpende server. Jeg taler her om fjernopkald, som er et almindeligt tilfælde for afbrydere, men de kan bruges i enhver situation, hvor du vil beskytte dele af et system mod fejl i andre dele.

afbrydere er et værdifuldt sted til overvågning. Enhver ændring i afbrydertilstand skal logges, og afbrydere skal afsløre detaljer om deres tilstand for dybere overvågning. Breaker adfærd er ofte en god kilde til advarsler om dybere problemer i miljøet. Driftspersonale skal være i stand til at rejse eller nulstille afbrydere.

Breakers alene er værdifulde, men klienter, der bruger dem, skal reagere på breaker-fejl. Som med enhver ekstern påkaldelse skal du overveje, hvad du skal gøre i tilfælde af fiasko. Fejler det den operation, du udfører, eller er der løsninger, du kan gøre? En kreditkortgodkendelse kan sættes i kø for at håndtere senere, hvis du ikke får nogle data, kan det afhjælpes ved at vise nogle uaktuelle data, der er gode nok til at vise.

yderligere læsning

bloggen indeholder en masse nyttige oplysninger om forbedring af pålideligheden af systemer med masser af tjenester. Deres Afhængighedskommando taler om brug af afbrydere og en trådpuljegrænse.

har et avanceret værktøj til håndtering af latenstid og fejltolerance for distribuerede systemer. Det inkluderer en implementering af afbrydermønsteret med trådpuljegrænsen

der er andre open source-implementeringer af afbrydermønsteret i Ruby, Java, Grails Plugin, C#, AspectJ og Scala

anerkendelser

Pavel Shpak opdagede og rapporterede en fejl i eksempelkoden

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.