Es ist üblich, dass Softwaresysteme Remote-Anrufe an Software tätigen, die in verschiedenen Prozessen ausgeführt wird, wahrscheinlich auf verschiedenen Computern in einem Netzwerk. Einer der großen Unterschiede zwischen In-Memory-Anrufen und Remote-Anrufen besteht darin, dass Remote-Anrufe fehlschlagen oder ohne Antwort hängen bleiben können, bis ein Zeitlimit erreicht ist. Was noch schlimmer ist, wenn Sie viele Anrufer bei einem nicht reagierenden Lieferanten haben, können Ihnen die kritischen Ressourcen ausgehen, was zu kaskadierenden Fehlern auf mehreren Systemen führt. In seiner ausgezeichneten Buchveröffentlichung It popularisierte Michael Nygard das Circuit Breaker Pattern, um diese Art von katastrophaler Kaskade zu verhindern.
Die Grundidee hinter dem Leistungsschalter ist sehr einfach. Sie wickeln einen geschützten Funktionsaufruf in ein Leistungsschalterobjekt ein, das auf Fehler überwacht. Sobald die Ausfälle einen bestimmten Schwellenwert erreichen, löst der Leistungsschalter aus, und alle weiteren Anrufe an den Leistungsschalter kehren mit einem Fehler zurück, ohne dass der geschützte Anruf überhaupt getätigt wird. Normalerweise möchten Sie auch eine Art Monitoralarm, wenn der Leistungsschalter auslöst.
Hier ist ein einfaches Beispiel für dieses Verhalten in Ruby, das vor Zeitüberschreitungen schützt.
Ich habe den Unterbrecher mit einem Block (Lambda) eingerichtet, der der geschützte Aufruf ist.
cb = CircuitBreaker.new {|arg| @supplier.func arg}
Der Leistungsschalter speichert den Block, initialisiert verschiedene Parameter (für Schwellenwerte, Zeitüberschreitungen und Überwachung) und setzt den Leistungsschalter in seinen geschlossenen Zustand zurück.
klasse Schutzschalter…
attr_accessor :invocation_timeout, :failure_threshold, :monitor def initialize &block @circuit = block @invocation_timeout = 0.01 @failure_threshold = 5 @monitor = acquire_monitor reset end
Das Aufrufen des Leistungsschalters ruft den zugrunde liegenden Block auf, wenn der Stromkreis geschlossen ist, gibt jedoch einen Fehler zurück, wenn er geöffnet ist
# client code aCircuitBreaker.call(5)
class 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
Sollten wir ein Timeout bekommen, erhöhen wir den Fehlerzähler, erfolgreiche Aufrufe setzen ihn auf Null zurück.
klasse Schutzschalter…
def record_failure @failure_count += 1 @monitor.alert(:open_circuit) if :open == state end def reset @failure_count = 0 @monitor.alert :reset_circuit end
Ich bestimme den Zustand des Leistungsschalters, indem ich die Fehleranzahl mit dem Schwellenwert
class CircuitBreaker vergleiche…
def state (@failure_count >= @failure_threshold) ? :open : :closed end
Dieser einfache Leistungsschalter vermeidet den geschützten Anruf, wenn der Stromkreis geöffnet ist, benötigt jedoch einen externen Eingriff, um ihn zurückzusetzen, wenn die Dinge wieder in Ordnung sind. Dies ist ein vernünftiger Ansatz bei elektrischen Leistungsschaltern in Gebäuden, aber bei Software-Leistungsschaltern kann der Leistungsschalter selbst erkennen, ob die zugrunde liegenden Aufrufe wieder funktionieren. Wir können dieses selbstrücksetzende Verhalten implementieren, indem wir den geschützten Aufruf nach einem geeigneten Intervall erneut versuchen und den Breaker zurücksetzen, falls er erfolgreich ist.
Das Erstellen dieser Art von Breaker bedeutet das Hinzufügen eines Schwellenwerts für den Versuch des Zurücksetzens und das Einrichten einer Variablen zum Speichern des Zeitpunkts des letzten Fehlers.
Klasse 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
Es ist jetzt ein dritter Zustand vorhanden – halb offen – was bedeutet, dass die Schaltung bereit ist, einen echten Anruf als Versuch zu tätigen, um zu sehen, ob das Problem behoben ist.
Klasse 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
Die Aufforderung, im halboffenen Zustand anzurufen, führt zu einem Testanruf, der entweder den Leistungsschalter zurücksetzt, wenn er erfolgreich ist, oder den Timeout neu startet, wenn dies nicht der Fall ist.
Klasse 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
Dieses Beispiel ist einfach erklärend, in der Praxis bieten Leistungsschalter ein gutes Stück mehr Funktionen und Parametrierung. Häufig schützen sie vor einer Reihe von Fehlern, die ein Anruf auslösen kann, z. B. Netzwerkverbindungsfehlern. Nicht alle Fehler sollten die Schaltung auslösen, einige sollten normale Fehler widerspiegeln und als Teil der regulären Logik behandelt werden.
Bei viel Datenverkehr können Probleme mit vielen Anrufen auftreten, die nur auf das anfängliche Timeout warten. Da Remote-Aufrufe oft langsam sind, ist es oft eine gute Idee, jeden Aufruf mit einem future oder promise auf einen anderen Thread zu setzen, um die Ergebnisse zu verarbeiten, wenn sie zurückkommen. Indem Sie diese Threads aus einem Thread-Pool ziehen, können Sie dafür sorgen, dass der Stromkreis unterbrochen wird, wenn der Thread-Pool erschöpft ist.
Das Beispiel zeigt eine einfache Möglichkeit, den Unterbrecher auszulösen – eine Zählung, die bei einem erfolgreichen Anruf zurückgesetzt wird. Ein ausgefeilterer Ansatz könnte die Häufigkeit von Fehlern betrachten und auslösen, sobald Sie beispielsweise eine Ausfallrate von 50% erhalten. Möglicherweise haben Sie auch unterschiedliche Schwellenwerte für verschiedene Fehler, z. B. einen Schwellenwert von 10 für Zeitüberschreitungen, aber 3 für Verbindungsfehler.
Das Beispiel, das ich gezeigt habe, ist ein Leistungsschalter für synchrone Anrufe, aber Leistungsschalter sind auch für asynchrone Kommunikation nützlich. Eine gängige Technik besteht darin, alle Anforderungen in eine Warteschlange zu stellen, die der Lieferant mit seiner Geschwindigkeit verbraucht – eine nützliche Technik, um eine Überlastung der Server zu vermeiden. In diesem Fall unterbricht der Stromkreis, wenn sich die Warteschlange füllt.
Leistungsschalter allein tragen dazu bei, Ressourcen zu reduzieren, die bei Operationen gebunden sind, die wahrscheinlich ausfallen. Sie vermeiden es, auf Zeitüberschreitungen für den Client zu warten, und eine unterbrochene Schaltung vermeidet die Belastung eines problematischen Servers. Ich spreche hier über Remote-Anrufe, die ein häufiger Fall für Leistungsschalter sind, aber sie können in jeder Situation verwendet werden, in der Sie Teile eines Systems vor Ausfällen in anderen Teilen schützen möchten.
Leistungsschalter sind ein wertvoller Ort für die Überwachung. Jede Änderung des Unterbrecherstatus sollte protokolliert werden, und Unterbrecher sollten Details ihres Zustands für eine tiefere Überwachung preisgeben. Dieses Verhalten ist oft eine gute Quelle für Warnungen vor tieferen Problemen in der Umwelt. Das Betriebspersonal sollte in der Lage sein, Leistungsschalter auszulösen oder zurückzusetzen.
Unterbrecher allein sind wertvoll, aber Kunden, die sie verwenden, müssen auf Unterbrecherausfälle reagieren. Wie bei jedem Remote-Aufruf müssen Sie überlegen, was im Fehlerfall zu tun ist. Schlägt die Operation fehl, die Sie ausführen, oder gibt es Problemumgehungen, die Sie ausführen können? Eine Kreditkartenautorisierung kann in eine Warteschlange gestellt werden, um später behandelt zu werden, das Versäumnis, einige Daten zu erhalten, kann gemildert werden, indem einige veraltete Daten angezeigt werden, die gut genug sind, um angezeigt zu werden.
Weiterführende Literatur
Der Netflix Tech Blog enthält viele nützliche Informationen zur Verbesserung der Zuverlässigkeit von Systemen mit vielen Diensten. Ihr Abhängigkeitsbefehl spricht über die Verwendung von Leistungsschaltern und ein Thread-Pool-Limit.
Netflix hat Open-Source-Hystrix, ein ausgeklügeltes Werkzeug für den Umgang mit Latenz und Fehlertoleranz für verteilte Systeme. Es enthält eine Implementierung des Circuit Breaker-Musters mit dem Thread-Pool-Limit
Es gibt andere Open-Source-Implementierungen des Circuit Breaker-Musters in Ruby, Java, Grails Plugin, C #, AspectJ und Scala
Acknowledgements
Pavel Shpak hat einen Fehler im Beispielcode
entdeckt und gemeldet