Es común que los sistemas de software realicen llamadas remotas a software que se ejecuta en diferentes procesos, probablemente en diferentes máquinas a través de una red. Una de las grandes diferencias entre las llamadas en memoria y las llamadas remotas es que las llamadas remotas pueden fallar o colgarse sin respuesta hasta que se alcance un límite de tiempo de espera. Lo que es peor si tiene muchas personas que llaman en un proveedor que no responde, entonces puede quedarse sin recursos críticos que conducen a fallas en cascada en varios sistemas. En su excelente libro It, Michael Nygard popularizó el patrón de Disyuntores para evitar este tipo de cascada catastrófica.
La idea básica detrás del disyuntor es muy simple. Envuelva una llamada a función protegida en un objeto de disyuntor, que monitoriza los fallos. Una vez que las fallas alcanzan un cierto umbral, el disyuntor se dispara y todas las llamadas adicionales al disyuntor regresan con un error, sin que se realice la llamada protegida. Por lo general, también querrá algún tipo de alerta de monitor si el disyuntor se dispara.
Este es un ejemplo sencillo de este comportamiento en Ruby, que protege contra los tiempos de espera.
Configuré el interruptor con un bloque (Lambda) que es la llamada protegida.
cb = CircuitBreaker.new {|arg| @supplier.func arg}
El interruptor almacena el bloque, inicializa varios parámetros (para umbrales, tiempos de espera y monitoreo) y restablece el interruptor a su estado cerrado.interruptor de circuito de clase
…
attr_accessor :invocation_timeout, :failure_threshold, :monitor def initialize &block @circuit = block @invocation_timeout = 0.01 @failure_threshold = 5 @monitor = acquire_monitor reset end
Llamar al disyuntor llamará al bloque subyacente si el circuito está cerrado, pero devolverá un error si está abierto
# client code aCircuitBreaker.call(5)
CortaciRcuitos de clase…
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
Si obtenemos un tiempo de espera, incrementamos el contador de fallas, las llamadas exitosas lo restablecen a cero. interruptor de circuito de clase
…
def record_failure @failure_count += 1 @monitor.alert(:open_circuit) if :open == state end def reset @failure_count = 0 @monitor.alert :reset_circuit end
Determino el estado del interruptor comparando el recuento de fallas con el umbral
CortaciRcuitos de clase…
def state (@failure_count >= @failure_threshold) ? :open : :closed end
Este simple disyuntor evita hacer la llamada protegida cuando el circuito está abierto, pero necesitaría una intervención externa para restablecerlo cuando las cosas estén bien de nuevo. Este es un enfoque razonable con disyuntores eléctricos en edificios, pero para disyuntores de software podemos hacer que el propio disyuntor detecte si las llamadas subyacentes están funcionando nuevamente. Podemos implementar este comportamiento de auto-restablecimiento probando de nuevo la llamada protegida después de un intervalo adecuado, y restableciendo el interruptor en caso de que tenga éxito.
Crear este tipo de interruptor significa agregar un umbral para probar el reinicio y configurar una variable para retener el tiempo del último error.
interruptor de circuito de reinicio de clase…
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
Ahora hay un tercer estado presente, medio abierto, lo que significa que el circuito está listo para hacer una llamada real como prueba para ver si se soluciona el problema.
interruptor de circuito de reinicio de clase…
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
Pedido llamar en el medio abierto estado de resultados en una llamada de juicio, que reinicie el disyuntor en caso de éxito o reiniciar el tiempo de espera si no.
interruptor de circuito de reinicio de clase…
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 ejemplo es explicativo, en la práctica los disyuntores proporcionan un poco más de funciones y parametrización. A menudo, protegerán contra una serie de errores que la llamada protegida podría generar, como fallas en la conexión de red. No todos los errores deben hacer tropezar el circuito, algunos deben reflejar fallas normales y tratarse como parte de la lógica regular.
Con mucho tráfico, puede tener problemas con muchas llamadas esperando el tiempo de espera inicial. Dado que las llamadas remotas a menudo son lentas, a menudo es una buena idea poner cada llamada en un hilo diferente utilizando un futuro o promesa para manejar los resultados cuando regresen. Al extraer estas roscas de un grupo de roscas, puede organizar que el circuito se rompa cuando se agote el grupo de roscas.
El ejemplo muestra una forma sencilla de activar el interruptor, un recuento que se restablece en una llamada exitosa. Un enfoque más sofisticado podría ver la frecuencia de errores, tropezar una vez que obtienes, por ejemplo, una tasa de fallas del 50%. También puede tener umbrales diferentes para errores diferentes, como un umbral de 10 para los tiempos de espera, pero de 3 para los fallos de conexión.
El ejemplo que he mostrado es un disyuntor para llamadas síncronas, pero los disyuntores también son útiles para comunicaciones asíncronas. Una técnica común aquí es poner todas las solicitudes en una cola, que el proveedor consume a su velocidad, una técnica útil para evitar sobrecargar los servidores. En este caso el circuito se rompe cuando la cola se llena.
Por sí solos, los disyuntores ayudan a reducir los recursos atados a operaciones que probablemente fallen. Evita esperar los tiempos de espera para el cliente, y un circuito roto evita poner carga en un servidor con problemas. Hablo aquí de las llamadas remotas, que son un caso común para los disyuntores, pero se pueden usar en cualquier situación en la que desee proteger partes de un sistema de fallas en otras partes.
Los disyuntores son un lugar valioso para el monitoreo. Cualquier cambio en el estado del interruptor debe registrarse y los interruptores deben revelar detalles de su estado para un monitoreo más profundo. El comportamiento de los interruptores es a menudo una buena fuente de advertencias sobre problemas más profundos en el medio ambiente. El personal de operaciones debe poder activar o reiniciar los interruptores.Los interruptores
por sí solos son valiosos, pero los clientes que los usan deben reaccionar a las fallas de los interruptores. Al igual que con cualquier invocación remota, debe considerar qué hacer en caso de falla. ¿Falla la operación que está llevando a cabo, o hay soluciones alternativas que puede hacer? Una autorización de tarjeta de crédito se podría poner en una cola para lidiar con ella más tarde, la falla en obtener algunos datos se puede mitigar mostrando algunos datos obsoletos que son lo suficientemente buenos para mostrar.
Más información
El blog de tecnología de netflix contiene mucha información útil sobre cómo mejorar la confiabilidad de los sistemas con muchos servicios. Su Comando de Dependencia habla sobre el uso de disyuntores y un límite de grupo de subprocesos.
Netflix tiene Hystrix de código abierto, una herramienta sofisticada para lidiar con la latencia y la tolerancia a fallos en sistemas distribuidos. Incluye una implementación del patrón de disyuntor con el límite del grupo de subprocesos
Hay otras implementaciones de código abierto del patrón de disyuntor en Ruby, Java, Grails Plugin, C#, AspectJ y Scala
Reconocimientos
Pavel Shpak detectó e informó de un error en el código de ejemplo