Rafael Nunes
Always learning software engineer | locale 🇧🇷🇦🇺
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.
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?
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.
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á.
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 = 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
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!
By Rafael Nunes
Slides da apresentação no Aba.rb