Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Disyuntores: o cómo no terminar viviendo abajo de un puente

Disyuntores: o cómo no terminar viviendo abajo de un puente

En esta charla para ruby.conf.online repasamos rápidamente el patrón Circuit Breaker, sus usos, ventajas y desventajas, y cómo aprovecharlo utilizando la gema disyuntor.

Leandro López

December 04, 2015
Tweet

More Decks by Leandro López

Other Decks in Programming

Transcript

  1. o/

  2. Disyuntores O cómo no terminar viviendo abajo de un puente

  3. @inkel • Web Developer • DevOps Wannabe • Citrusbyte •

    RubyConfAR • No sé diseñar slides • Fundamentalista de la barba • Lector
  4. None
  5. None
  6. ¿Dónde se usa? • Aplicaciones distribuidas • SOA (Service Oriented

    Architecture) • Microservicios
  7. None
  8. @adrianco ex-CTO de Netflix

  9. None
  10. on get, "users" do render("users/index", users: User.all) end

  11. None
  12. None
  13. • Más usuarios • Más consultas • Más lenta cada

    consulta • Mayor cantidad de recursos • Timeouts
  14. None
  15. None
  16. None
  17. None
  18. on get, "users" do users = Cache.fetch("user.all") do User.all end

    ! render("users/index", users: users) end
  19. on get, "users" do users = Cache.fetch("user.all") do User.all end

    ! render("users/index", users: users) end
  20. Falla en un servicio Lo que creemos que sucede

  21. Falla en un servicio Lo que realmente sucede

  22. • Cache no anda • Sobrecarga base de datos •

    Saturación de red • Timeouts
  23. None
  24. None
  25. Disyuntor • Controla el consumo de energía eléctrica • Sobrepasado

    cierto nivel, abre el circuito • No se te prende fuego la casa
  26. Patrón Disyuntor • Controla el acceso y ejecución de un

    bloque de código • Sobrepasado el límite de errores consecutivos, abre el circuito • Todo intento a ejecutar el código falla con un error conocido • Pasado cierto timeout, intenta ejecutar el código • Si funciona correctamente, cierra el circuito • Si falla, comienza el ciclo nuevamente
  27. None
  28. Ejemplo

  29. # A partir del décimo error consecutivo, abrir el circuito


    # y esperar 5 segundos para volver a intentarlo. disyuntor_cache = Disyuntor.new(threshold: 10, timeout: 5) ! # A partir del quinto error consecutivo, abrir el circuito
 # y esperar 1 minuto para volver a intentarlo. disyuntor_db = Disyuntor.new(threshold: 5, timeout: 60) ! # Ejecutar una acción personalizada cuando el circuito esté
 # abierto. disyuntor_db.on_circuit_open do fail DBNotFound end
  30. on get, "users" do begin users = get_users_from_cache render("users/index", users:

    users) rescue DBNotFound render("db_not_found") end end
  31. on get, "users" do begin users = get_users_from_cache render("users/index", users:

    users) rescue DBNotFound render("db_not_found") end end
  32. def get_users_from_cache disyuntor_cache.try do Cache.fetch("user.all") do get_users end end rescue

    Disyuntor::CircuitOpenError get_users end
  33. def get_users_from_cache disyuntor_cache.try do Cache.fetch("user.all") do get_users end end rescue

    Disyuntor::CircuitOpenError get_users end
  34. def get_users_from_cache disyuntor_cache.try do Cache.fetch("user.all") do get_users end end rescue

    Disyuntor::CircuitOpenError get_users end
  35. def get_users disyuntor_db.try do User.all end end

  36. None
  37. None
  38. • Si uno de los servicios no funciona falla inmediatamente.

    • Menor consumo de recursos en los servidores, debido a no ejecutar código que sabemos va a fallar. • Menos intervención manual dado que el sistema se “arregla” automáticamente. • Disminución en el número de errores en cascada.
  39. –Martin Fowler “On their own, circuit breakers help reduce resources

    tied up in operations which are likely to fail. You avoid waiting on timeouts for the client, and a broken circuit avoids putting load on a struggling server.”
  40. github.com/inkel/ disyuntor Shame on me!

  41. inkel@miralejos ~/dev/disyuntor (master=) $ cloc lib/disyuntor.rb 1 text file. 1

    unique file. 0 files ignored. ! http://cloc.sourceforge.net v 1.64 T=0.01 s (67.1 files/s, 6243.7 lines/s) ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- Ruby 1 19 0 74 -------------------------------------------------------------------------------
  42. require "micromachine" ! class Disyuntor CircuitOpenError = Class.new(RuntimeError) ! attr_reader

    :failures, :opened_at, :threshold, :timeout ! def initialize(threshold: 5, timeout: 10) @threshold = threshold @timeout = timeout ! on_circuit_open { fail CircuitOpenError } ! close! end ! def states @states ||= MicroMachine.new(:closed).tap do |fsm| fsm.when(:trip, :half_open => :open, :closed => :open) fsm.when(:reset, :half_open => :closed, :closed => :closed) fsm.when(:try, :open => :half_open) ! fsm.on(:open) do @opened_at = Time.now.to_i end ! fsm.on(:closed) do @opened_at = nil @failures = 0 end end end ! def close! () states.trigger!(:reset) end def open! () states.trigger!(:trip) end def half_open! () states.trigger!(:try) end ! def closed? () states.state == :closed end def open? () states.state == :open end def half_open? () states.state == :half_open end ! def timed_out? open? && Time.now.to_i > next_timeout_at end ! def next_timeout_at closed? ? nil : (@opened_at + @timeout) end ! def increment_failures! @failures += 1 end ! def try(&block) half_open! if timed_out? ! case when closed? then circuit_closed(&block) when half_open? then circuit_half_open(&block) when open? then circuit_open end end ! def circuit_closed(&block) ret = block.call rescue open! if increment_failures! > threshold raise else close! ret end ! def circuit_half_open(&block) ret = block.call rescue increment_failures! open! raise else close! ret end ! def on_circuit_open(&block) raise ArgumentError, "Must pass a block" unless block_given? @on_circuit_open = block end ! def circuit_open @on_circuit_open.call(self) end end
  43. end ! def increment_failures! @failures += 1 end ! def

    try(&block) half_open! if timed_out? ! case when closed? then circuit_closed(&block) when half_open? then circuit_half_open(&block) when open? then circuit_open end end ! def circuit_closed(&block) ret = block.call rescue open! if increment_failures! > threshold raise else
  44. • Simple • Configurable • Disyuntor::CircuitOpenError
 #on_circuit_open • Más código

    • Latencia
  45. #! /usr/bin/env ruby ! require "redis" require "benchmark/ips" require_relative "lib/disyuntor"

    ! disyuntor = Disyuntor.new(threshold: 1, timeout: 60) ! disyuntor.on_circuit_open { "" } ! Benchmark.ips do |r| r.report("sin") do begin Redis.current.ping rescue Redis::BaseError "" end end ! r.report("con") do begin disyuntor.try { Redis.current.ping } rescue Redis::BaseError "" end end ! r.compare! end
  46. Benchmark - Up Calculating ------------------------------------- sin 1.284k i/100ms con 1.192k

    i/100ms ------------------------------------------------- sin 13.690k (± 3.9%) i/s - 69.336k con 12.533k (± 2.3%) i/s - 63.176k ! Comparison: sin: 13689.9 i/s con: 12533.5 i/s - 1.09x slower
  47. Benchmark - Down Calculating ------------------------------------- sin 5.000 i/100ms con 22.659k

    i/100ms ------------------------------------------------- sin 3.277k (±38.6%) i/s - 380.000 in 5.743486s con 349.269k (± 3.6%) i/s - 1.767M ! Comparison: con: 349268.6 i/s sin: 3277.1 i/s - 106.58x slower
  48. –Martin Fowler “I talk here about remote calls, which are

    a common case for circuit breakers, but they can be used in any situation where you want to protect parts of a system from failures in other parts.”
  49. None
  50. ¿Preguntas?

  51. • Release It!: Design and Deploy Production-Ready Software
 http://goo.gl/zMb9Uj •

    Martin Fowler: Circuit Breaker
 http://goo.gl/7Nmy6A • Making the Netflix API More Resilient
 http://goo.gl/VLO9Sq • Gemas
 github.com/inkel/disyuntor
 github.com/wsargent/circuit_breaker • Esta presentación
 https://goo.gl/B2YbBA