CircuitBreaker

é comum que os sistemas de software façam chamadas remotas para software em execução em diferentes processos, provavelmente em máquinas diferentes através de uma rede. Uma das grandes diferenças entre chamadas em memória e chamadas remotas é que chamadas remotas podem falhar, ou pendurar sem uma resposta até algum limite de tempo é alcançado. O que é pior se você tem muitos chamadores em um fornecedor sem resposta, então você pode ficar sem recursos críticos levando a falhas em cascata em vários sistemas. Em seu excelente livro Release, Michael Nygard popularizou o padrão de disjuntores para evitar este tipo de cascata catastrófica.

a ideia básica por trás do disjuntor é muito simples. Envolves uma chamada de função protegida num objecto de disjuntor, que monitoriza as falhas. Uma vez que as falhas atingem um certo limiar, o disjuntor tropeça, e todas as chamadas adicionais para o disjuntor retornam com um erro, sem que a chamada protegida seja feita de todo. Normalmente você também vai querer algum tipo de alerta de monitor se o disjuntor tropeça.

aqui está um exemplo simples deste comportamento em Ruby, protegendo contra os tempos-limite.

configurei o disjuntor com um bloco (Lambda) que é a chamada protegida.

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

o disjuntor armazena o bloco, inicializa vários parâmetros (para limiares, timeouts e monitoramento), e repõe o disjuntor em seu estado fechado.

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

chamando o disjuntor irá chamar o bloco subjacente se o circuito estiver fechado, mas retornar um erro se estiver aberto

# client code aCircuitBreaker.call(5)

Classe 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

se conseguirmos um tempo-limite, aumentamos o contador de falhas, as chamadas de sucesso reiniciam-no para 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

I determine the state of the breaker comparing the failure count to the threshold

class CircuitBreaker…

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

este disjuntor simples evita fazer a chamada protegida quando o circuito está aberto, mas precisaria de uma intervenção externa para reiniciá-lo quando as coisas estão bem novamente. Esta é uma abordagem razoável com disjuntores elétricos em edifícios, mas para disjuntores de software nós podemos ter o disjuntor próprio detectar se as chamadas subjacentes estão funcionando novamente. Podemos implementar este comportamento auto-reinicialização, tentando a chamada protegida novamente após um intervalo adequado, e reiniciando o disjuntor caso ele tenha sucesso.

criar este tipo de disjuntor significa adicionar um limiar para tentar o reset e configurar uma variável para manter o tempo do último erro.

class 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

existe agora um terceiro estado presente-meio aberto-o que significa que o circuito está pronto para fazer uma chamada real como teste para ver se o problema é corrigido.

class 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

pediu para chamar no estado meio aberto resulta em uma chamada de teste, que irá reiniciar o disjuntor se bem sucedido ou reiniciar o tempo-limite se não.

class 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

este exemplo é um simples exemplo explicativo, na prática disjuntores fornecem um pouco mais de características e parametrização. Muitas vezes eles vão proteger contra uma série de erros que a chamada protegida poderia levantar, como falhas de conexão de rede. Nem todos os erros devem tropeçar no circuito, alguns devem refletir falhas normais e ser tratados como parte da lógica regular.

com muito tráfego, você pode ter problemas com muitas chamadas apenas esperando o tempo limite inicial. Uma vez que as chamadas remotas são muitas vezes lentas, muitas vezes é uma boa idéia colocar cada chamada em um tópico diferente usando um futuro ou promessa de lidar com os resultados quando eles voltam. Ao desenhar estes tópicos a partir de uma piscina de fios, você pode organizar para que o circuito para quebrar quando a piscina de fios está exausto.

o exemplo mostra uma maneira simples de tropeçar o disjuntor-uma contagem que reinicia uma chamada bem sucedida. Uma abordagem mais sofisticada pode olhar para a frequência de erros, tropeçando assim que você começa, digamos, uma taxa de falha de 50%. Você também pode ter limiares diferentes para erros diferentes, como um limiar de 10 para timeouts, mas 3 para falhas de conexão.

o exemplo que mostrei é um disjuntor para chamadas síncronas, mas disjuntores também são úteis para comunicações assíncronas. Uma técnica comum aqui é colocar todas as solicitações em uma fila, que o fornecedor consome à sua velocidade – uma técnica útil para evitar sobrecarga de servidores. Neste caso, o circuito quebra quando a fila se enche.

por si só, disjuntores ajudam a reduzir os recursos amarrados em operações que são susceptíveis de falhar. Evita-se esperar pelos tempos-limite para o cliente, e um circuito avariado evita colocar carga num servidor em dificuldades. Eu falo aqui sobre chamadas remotas, que são um caso comum para disjuntores, mas eles podem ser usados em qualquer situação onde você quer proteger as partes de um sistema de falhas em outras partes.

disjuntores são um local valioso para monitorização. Qualquer mudança no estado dos disjuntores deve ser registrada e os disjuntores devem revelar detalhes de seu estado para um monitoramento mais profundo. O comportamento Breaker é muitas vezes uma boa fonte de avisos sobre problemas mais profundos no ambiente. O pessoal das operações deve poder tropeçar ou reiniciar os disjuntores.

Breakers on their own are valuable, but clients using them need to react to breaker failures. Como em qualquer invocação remota, você precisa considerar o que fazer em caso de falha. Falha na operação que está a realizar, ou há trabalhos que pode fazer? Uma autorização de cartão de crédito poderia ser colocado em uma fila para lidar com mais tarde, a falha de obter alguns dados pode ser mitigada, mostrando alguns dados obsoletos que são bons o suficiente para exibir.

Leitura Adicional

o blog netflix tech contém uma série de informações úteis sobre a melhoria da fiabilidade dos sistemas com muitos serviços. Seu comando de Dependência fala sobre o uso de disjuntores e um limite de thread pool.

Netflix tem Hystrix open-source, uma ferramenta sofisticada para lidar com a latência e tolerância a falhas em sistemas distribuídos. Ele inclui uma implementação do disjuntor padrão com o limite do pool de threads

Há outros de código-fonte aberto implementações do disjuntor padrão em Ruby, Java, Plugin para Grails, C#, AspectJ, e Scala

Agradecimentos

Pavel Shpak visto e relatado um erro no exemplo de código

Deixe uma resposta

O seu endereço de email não será publicado.