Absicherung gegenüber externen Diensten mit Hystrix

Ein Programm ist schnell geschrieben. Durch Anbindung externer Dienste lässt sich dann der Nutzen für die Anwender gar noch erhöhen. Doch was passiert, wenn der Dienst nicht, viel zu langsam oder falsch antwortet? Mit Hystrix lassen sich Schnittstellen zu Diensten überwachen, im Fehlerfall automatisch kappen und mit Fallbacks ergänzen.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 13 Min.
Von
  • Alexander Schwartz
Inhaltsverzeichnis

Ein Programm ist schnell geschrieben. Durch Anbindung externer Dienste lässt sich dann der Nutzen für die Anwender gar noch erhöhen. Doch was passiert, wenn der Dienst nicht, viel zu langsam oder falsch antwortet? Mit Hystrix lassen sich Schnittstellen zu Diensten überwachen, im Fehlerfall automatisch kappen und mit Fallbacks ergänzen.

Ein typisches Beispiel: Um eine Kundendatenbank mit Kollegen zu teilen, wird eine Webanwendung entwickelt. Die Daten werden in einer Datenbank gespeichert, die Vermittlung zwischen den Webbrowsern der Anwender und der Datenbank übernimmt ein Application Server.

In der nächsten Programmversion sollen bei der Eingabe die Bankverbindungen der Kunden geprüft werden, zum Beispiel darauf, ob die angegebene Bank existiert und das Konto für SEPA-Lastschriften freigegeben ist. Da sich die zugrunde liegenden Daten für die Bankdatenvalidierung regelmäßig ändern, soll die Validierung als externer Dienst eingekauft werden. Doch jeder externe Dienst hat ein Risiko – ist er nicht erreichbar, kommt es zur Fehlermeldung:

try {
Result r = IbanBicRemoteValidator.validate(iban, bic);
} catch (IOException e) {
// ... show error message to the user
}

Frontends sind darauf ausgelegt, dass sie Eingaben entgegennehmen und dazugehörige Fehlermeldungen sofort anzeigen. Bei entfernten Diensten können Antworten oder Fehlermeldungen stark verzögert ankommen. Bei Problemen mit der DNS-Auflösung, der Netzanbindung oder durch eine Überlastung des Dienstes kann es Minuten dauern, bis die Antwort eintrifft. Das ist für den Nutzer nicht akzeptabel.

Ein einzelner lang laufender Aufruf des Dienstes hat keine Auswirkungen auf die Stabilität der eigenen Anwendung. Hunderte paralleler Aufrufe, die statt 100 Millisekunden wegen Netzproblemen zehn Sekunden dauern, aber schon. Schnell ist die maximale Anzahl von Verbindungen zum Application Server oder zur Datenbank erreicht, und die eigene Anwendung kommt zum Stillstand: Es werden keine Anfragen mehr beantwortet. Ähnlich einem Domino-Effekt zieht der Dienst die von ihm abhängige eigene Anwendung mit in den Abgrund. Sie kann sich nur erholen, wenn der Dienst wieder normal antwortet.

Eine Lösung ist, die verschiedenen Netz-Timeouts auf Java- und Betriebssystemebene zu prüfen und richtig einzustellen. Je nach Schnittstellentechnik und eingesetzter Bibliothek sind dafür unterschiedliche Parameter zu setzen. Manchmal sind diese jedoch nicht zugänglich, weil sie die verwendete Bibliothek nicht zur Konfiguration anbietet. Eine komplexere Strategie, die einzelne langsame Antworten erlaubt, jedoch Grenzen beispielsweise über maximale Warteschlangen oder durchschnittliche Antwortzeiten setzt, ist so nicht zu implementieren. Außerdem fehlen einheitliche Überwachungsmöglichkeiten.

Einen Ausweg bietet die Hystrix-Bibliothek, die Netflix 2012 unter eine Open-Source-Lizenz gestellt hat und die sich seitdem steigender Beliebtheit erfreut. Unabhängig von der eingesetzten Schnittstellentechnik kann der Nutzer damit Aufrufe kapseln. Dabei lassen sich viele Parameter zur Laufzeit konfigurieren, in Echtzeit überwachen und für die Betriebsüberwachung zum Beispiel via JMX (Java Management Extensions) freigeben.

Die Aufrufe des Dienstes werden als HystrixCommand gekapselt.

public class IbanBicCommand extends HystrixCommand<Result> {

private String iban, bic;

protected IbanBicCommand(String iban, String bic) {
super(HystrixCommandGroupKey.Factory.
asKey("IbanBic"));
this.iban = iban;
this.bic = bic;
}

protected Result run() throws IOException {
return IbanBicRemoteValidator.validate(iban, bic);
}

}

...


Result result = new IbanBicCommand(iban, bic).execute();

Mit der synchronen execute()-Methode lassen sich bestehende Code-Stellen einfach umstellen. Und durch die Kapselung stehen auch Möglichkeiten einer asynchronen Verarbeitung zur Verfügung. Das nächste Codebeispiel zeigt die Verwendung als Future, wenn die Verfügbarkeit der Antwort im weiteren Programmablauf abgefragt werden soll.

Future<Result> futureResult = new IbanBicCommand(iban, bic).queue();

/* ... do something in between ... */

// see if the call has completed in the meantime
if(futureResult.isDone()) {
/* ... */
}

// retrieve result - wait if necessary
Result result = futureResult.get();

Das folgende Beispiel zeigt die Verwendung als Observable, wenn direkt auf die Antwort reagiert werden soll, sobald diese zur Verfügung steht.

Observable<Result> observableResult = new IbanBicCommand(iban, 
bic).observe();

observableResult.subscribe(new Action1<Result>() {

public void call(Result s) {
// do something once the response is ready
}

});

Für den Fall, dass der Dienst nicht zur Verfügung steht, lässt sich das IbanBicCommand um eine Fallback-Strategie ergänzen, die zum Beispiel nur die IBAN-Prüfsumme berechnet und die Prüfung der BIC ignoriert:

public class IbanBicCommandWithFallback extends IbanBicCommand {

/** ... **/

protected Result getFallback() {
// just calculate check digit, ignore BIC
return IbanBicLocalValidator.validate(iban);
}

}