Circuit Breaker em Ruby com Circuitbox

 

 

Remote calls

Remote calls ou chamadas remotas são uma parte  crucial das nossas aplicações, especialmente quando precisamos consultar serviços externos.

 

Serviços externos ajudam nossa aplicação a realizar operações necessárias, mas também acabam se tornando pontos de falha dos nossos apps.

Remote calls

Quando mais crítica é o serviço que a gente depende, maior o impacto na nossa aplicação quando ele retorna erros.

 

Equanto que um erro aqui e ali é torelado/esperado, o que acontece com a nossa aplicação quando um serviço que a gente depende está  degradado?

 
 

Remote calls

Imagine que algo muito ruim aconteceu em algum desses serviços e todas as requisições estão resultando em timeout.

 

A nossa aplicação então também vai ficar com a performance degradada.

 

Vamos sempre esperar até o limite do timeout para propagar o error pros usuários.

Catástrofe em cascata

Circuit Breaker

Circuit Breaker é um padrão de design de software queue propõe o monitoramento das falhas em remote calls.

 

Se uma certa taxa de erro for atingida, o circuito abrirá e pulará todas as requisições em seguidas durante uma janela de tempo.

 

Quando a janela de tempo expirar o circuit tentará executar a próxima requisição normalmente, mas se a reposta for um erro novamente então o circuito voltará a ficar aberto.

 

Quando o serviço que consultados voltar a responder com successo dentro dos mesmos limiares então o circuito fechará.

Circuit Breaker

Alguns casos

  • Falhar rápido
    • Não esperamos timeout se sabemos que não resultará em sucesso.
  • Falhar parcialmente
    • Quanto temos a opção de continuar com uma resposta diferente da ideal.
  • Toleramos um resultado alternativo
    • Temos a opção de retornar um valor default ou cached
  • Melhor esforço
    • Tentamos chamar o outro serviço, mas não nos importamos com o resultado, e.g. Analytics não crítico.

Falha parcial

Um aplicativo depende de várias consultas para responder com uma resposta ideal e há uma restrição de tempo que não pode esperar muito por qualquer uma das APIs.


Se qualquer uma das APIs estiver inativa, o circuito será aberto, mas o sistema continuará funcionando em um estado parcial.

places

gov

shows

Circuit Breaker

Circuitbox

circuit = Circuitbox.circuit(:your_service)
circuit.run do 
  Net::HTTP.get URI('http://example.com/api/messages')
end

Circuitbox.circuit(:your_service, exceptions: [Net::ReadTimeout]) do
  Net::HTTP.get URI('http://example.com/api/messages')
end

sleep_window: segundos em que o circuito permanece aberto depois de ultrapassar o limite de erro. O padrão é 90 seg.


time_window: duração do intervalo (em segundos) durante o qual calcula a taxa de erro. O padrão é 60 seg.

volume_threshold: número de requisições dentro do time_window antes de calcular as taxas de erro. O padrão é 5 requisições.

error_threshold: exceder esta taxa abrirá o circuito (verificado em falhas). O padrão é 50%.

circuit name

exceptions to monitor

app.get("/", function (req, res) {
  const delay = req.query.speed === "fast"  ? 50 : 200;

  setTimeout(function () {
    res.sendStatus(200);
  }, delay);
});
def index
  speed = params[:speed]

  answers = Array(0..9).map do |i|
    sleep(1)
    puts "Request number #{i+1}"
    HTTP.get("http://localhost:4000/?speed=#{speed}")
        .status
  end

  render json: answers
end
  def without_timeout
    speed = params[:speed]

    answers = Array(0..9).map do |i|
      sleep(1)
      puts "Request number #{i+1}"
      HTTP.get("http://localhost:4000/?speed=#{speed}")
          .status
    end

    render json: answers
  end
  def with_timeout
    speed = params[:speed]

    answers = Array(0..9).map do |i|
      sleep(1)
      puts "Request number #{i+1}"
      HTTP.timeout(0.1)
          .get("http://localhost:4000/?speed=#{speed}")
          .status
    end

    render json: answers
  end
class DefaultConfigClient
  def initialize(speed:)
    @speed = speed
    @circuit = ::Circuitbox.circuit(:node_client, exceptions: [HTTP::TimeoutError])
  end

  def call
    circuit.run do
      HTTP.timeout(0.1).get("http://localhost:4000/?speed=#{speed}").status
    end
  end

  private

  attr_reader :speed, :circuit
end

sleep_window: segundos em que o circuito permanece aberto depois de ultrapassar o limite de erro. O padrão é 90 seg.
time_window: duração do intervalo (em segundos) durante o qual calcula a taxa de erro. O padrão é 60 seg.

volume_threshold: número de requisições dentro do time_window antes de calcular as taxas de erro. O padrão é 5 requisições.

error_threshold: exceder esta taxa abrirá o circuito (verificado em falhas). O padrão é 50%.

circuit name

exceptions to monitor

default value

class CustomConfigClient
  def initialize(speed:)
    @speed = speed
    @circuit = ::Circuitbox.circuit(
      :node_client_custom_config,
      exceptions: [HTTP::TimeoutError],
      time_window: 5,
      volume_threshold: 2,
      sleep_window: 2
    )
  end

  def call
    circuit.run do
      HTTP.timeout(0.1).get("http://localhost:4000/?speed=#{speed}").status
    end
  end

  #...

Estamos definindo time_window (o intervalo observado) como 5 segundos.

Estamos definindo volume_threshold para 2, portanto, precisamos obter pelo menos 2 solicitações para calcular a taxa de erro.

Finalmente, substituímos sleep_window para 2 segundos.

# ...

  def recover_cb
    speed = params[:speed]

    answers = Array(0..9).map do |i|
      sleep(1)
      puts "Request number #{i + 1}"
      speed = 'fast' if i > 3
      CustomConfigClient.new(speed: speed).call
    end

    render json: answers
  end

# ...

Após 4 requisições, mude para o modo rápido.

 

speed is slow, circuit closed

speed is fast, circuit open

speed is fast, circuit closed

Conclusões

Circuit breaker é um padrão que nos ajuda a manter nossos sistemas operando ou parcialmente operando. A ideia é parar de requisitar serviços que não respondem e voltar a requisitá-los quando eles estiverem saudáveis.


Conseguimos tornar nossos aplicativos mais resilientes quando falhamos graciosamente. É necessário entender e afinar as configurações para melhor atender às necessidades de cada aplicação.


Use com moderação!

 

Obrigado!

Circuit Breaker em Ruby com Circuitbox

By Rafael Nunes

Circuit Breaker em Ruby com Circuitbox

Slides da apresentação no Aba.rb

  • 1,202