Einführung in Hystrix

Einführung in Hystrix

1. Überblick

Ein typisches verteiltes System besteht aus vielen Diensten, die zusammenarbeiten.

Diese Dienste sind anfällig für Fehler oder verzögerte Antworten. Wenn ein Dienst ausfällt, kann dies Auswirkungen auf andere Dienste haben, die sich auf die Leistung auswirken und möglicherweise dazu führen, dass auf andere Anwendungsteile nicht zugegriffen werden kann oder im schlimmsten Fall die gesamte Anwendung heruntergefahren wird.

Natürlich gibt es Lösungen, die dazu beitragen, Anwendungen widerstandsfähig und fehlertolerant zu machen - ein solches Framework ist Hystrix.

Die Hystrix-Framework-Bibliothek hilft bei der Steuerung der Interaktion zwischen Diensten, indem Fehlertoleranz und Latenztoleranz bereitgestellt werden. Es verbessert die allgemeine Ausfallsicherheit des Systems, indem die ausfallenden Dienste isoliert und der Kaskadeneffekt von Fehlern gestoppt werden.

In dieser Reihe von Beiträgen werden wir zunächst untersuchen, wie Hystrix bei einem Ausfall eines Dienstes oder Systems Abhilfe schafft und was Hystrix unter diesen Umständen leisten kann.

2. Einfaches Beispiel

Die Art und Weise, wie Hystrix Fehler- und Latenztoleranz bietet, besteht darin, Anrufe an Remotedienste zu isolieren und zu bündeln.

In diesem einfachen Beispiel schließen wir einen Aufruf in dierun()-Methode derHystrixCommand: ein

class CommandHelloWorld extends HystrixCommand {

    private String name;

    CommandHelloWorld(String name) {
        super(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"));
        this.name = name;
    }

    @Override
    protected String run() {
        return "Hello " + name + "!";
    }
}

und wir führen den Aufruf wie folgt aus:

@Test
public void givenInputBobAndDefaultSettings_whenCommandExecuted_thenReturnHelloBob(){
    assertThat(new CommandHelloWorld("Bob").execute(), equalTo("Hello Bob!"));
}

3. Maven Setup

Um Hystrix in einem Maven-Projekt verwenden zu können, müssenhystrix-core undrxjava-core von Netflix im Projektpom.xml abhängig sein:


    com.netflix.hystrix
    hystrix-core
    1.5.4

Die neueste Version ist immerhere zu finden.


    com.netflix.rxjava
    rxjava-core
    0.20.7

Die neueste Version dieser Bibliothek befindet sich immer inhere.

4. Remote-Service einrichten

Beginnen wir mit der Simulation eines Beispiels aus der realen Welt.

In the example below, die KlasseRemoteServiceTestSimulator repräsentiert einen Dienst auf einem Remote-Server. Es gibt eine Methode, die nach Ablauf des angegebenen Zeitraums mit einer Nachricht antwortet. Wir können uns vorstellen, dass dieses Warten eine Simulation eines zeitaufwendigen Prozesses auf dem fernen System ist, der zu einer verzögerten Antwort auf den aufrufenden Dienst führt:

class RemoteServiceTestSimulator {

    private long wait;

    RemoteServiceTestSimulator(long wait) throws InterruptedException {
        this.wait = wait;
    }

    String execute() throws InterruptedException {
        Thread.sleep(wait);
        return "Success";
    }
}

And here is our sample client, dasRemoteServiceTestSimulator aufruft.

Der Aufruf des Dienstes wird isoliert und in dierun()-Methode vonHystrixCommand. eingeschlossen. Diese Umhüllung bietet die oben erwähnte Ausfallsicherheit:

class RemoteServiceTestCommand extends HystrixCommand {

    private RemoteServiceTestSimulator remoteService;

    RemoteServiceTestCommand(Setter config, RemoteServiceTestSimulator remoteService) {
        super(config);
        this.remoteService = remoteService;
    }

    @Override
    protected String run() throws Exception {
        return remoteService.execute();
    }
}

Der Aufruf wird ausgeführt, indem die Methodeexecute()für eine Instanz des ObjektsRemoteServiceTestCommandaufgerufen wird.

Der folgende Test zeigt, wie das gemacht wird:

@Test
public void givenSvcTimeoutOf100AndDefaultSettings_whenRemoteSvcExecuted_thenReturnSuccess()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroup2"));

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(100)).execute(),
      equalTo("Success"));
}

Bisher haben wir gesehen, wie Remotedienstaufrufe in das ObjektHystrixCommandeingeschlossen werden. Im folgenden Abschnitt sehen wir uns an, wie Sie mit einer Situation umgehen, in der sich der Remote-Dienst zu verschlechtern beginnt.

5. Arbeiten mit Remote Service und defensiver Programmierung

5.1. Defensive Programmierung mit Timeout

Es ist allgemeine Programmierpraxis, Zeitüberschreitungen für Anrufe an entfernte Dienste festzulegen.

Lassen Sie uns zunächst untersuchen, wie das Zeitlimit fürHystrixCommand festgelegt wird und wie es durch Kurzschließen hilft:

@Test
public void givenSvcTimeoutOf5000AndExecTimeoutOf10000_whenRemoteSvcExecuted_thenReturnSuccess()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest4"));

    HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
    commandProperties.withExecutionTimeoutInMilliseconds(10_000);
    config.andCommandPropertiesDefaults(commandProperties);

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));
}

Im obigen Test verzögern wir die Antwort des Dienstes, indem wir das Zeitlimit auf 500 ms einstellen. Außerdem setzen wir das Ausführungszeitlimit fürHystrixCommand auf 10.000 ms, damit der Remote-Dienst ausreichend Zeit hat, um zu antworten.

Nun wollen wir sehen, was passiert, wenn das Ausführungszeitlimit geringer ist als der Service-Timeout-Aufruf:

@Test(expected = HystrixRuntimeException.class)
public void givenSvcTimeoutOf15000AndExecTimeoutOf5000_whenRemoteSvcExecuted_thenExpectHre()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupTest5"));

    HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
    commandProperties.withExecutionTimeoutInMilliseconds(5_000);
    config.andCommandPropertiesDefaults(commandProperties);

    new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(15_000)).execute();
}

Beachten Sie, wie wir den Balken gesenkt und das Ausführungszeitlimit auf 5.000 ms festgelegt haben.

Wir erwarten, dass der Dienst innerhalb von 5.000 ms antwortet, während wir den Dienst so eingestellt haben, dass er nach 15.000 ms antwortet. Wenn Sie beim Ausführen des Tests feststellen, dass der Test nach 5.000 ms beendet wird, anstatt auf 15.000 ms zu warten, wird einHystrixRuntimeException. ausgegeben

Dies zeigt, wie Hystrix nicht länger als das konfigurierte Zeitlimit auf eine Antwort wartet. Dies trägt dazu bei, dass das von Hystrix geschützte System reaktionsfähiger wird.

In den folgenden Abschnitten werden wir uns mit dem Festlegen der Thread-Pool-Größe befassen, die verhindert, dass Threads erschöpft werden, und ihre Vorteile erörtern.

5.2. Defensive Programmierung mit begrenztem Thread-Pool

Durch das Festlegen von Zeitlimits für den Serviceabruf werden nicht alle Probleme im Zusammenhang mit Remotediensten behoben.

Wenn ein Remotedienst langsam reagiert, ruft eine typische Anwendung diesen Remotedienst weiterhin auf.

Die Anwendung weiß nicht, ob der Remotedienst fehlerfrei ist oder nicht, und jedes Mal, wenn eine Anforderung eingeht, werden neue Threads erstellt. Dies führt dazu, dass Threads auf einem Server verwendet werden, der bereits Probleme hat.

Wir möchten nicht, dass dies geschieht, da wir diese Threads für andere Remote-Aufrufe oder Prozesse benötigen, die auf unserem Server ausgeführt werden, und wir möchten auch vermeiden, dass die CPU-Auslastung ansteigt.

Mal sehen, wie man die Größe des Thread-Pools inHystrixCommand festlegt:

@Test
public void givenSvcTimeoutOf500AndExecTimeoutOf10000AndThreadPool_whenRemoteSvcExecuted
  _thenReturnSuccess() throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupThreadPool"));

    HystrixCommandProperties.Setter commandProperties = HystrixCommandProperties.Setter();
    commandProperties.withExecutionTimeoutInMilliseconds(10_000);
    config.andCommandPropertiesDefaults(commandProperties);
    config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
      .withMaxQueueSize(10)
      .withCoreSize(3)
      .withQueueSizeRejectionThreshold(10));

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));
}

Im obigen Test legen wir die maximale Warteschlangengröße, die Kernwarteschlangengröße und die Größe der Zurückweisung der Warteschlange fest. Hystrix lehnt die Anforderungen ab, wenn die maximale Anzahl von Threads 10 erreicht hat und die Taskwarteschlange eine Größe von 10 erreicht hat.

Die Kerngröße ist die Anzahl der Threads, die im Thread-Pool immer am Leben bleiben.

5.3. Defensive Programmierung mit Kurzschlussschutzmuster

Es gibt jedoch noch eine Verbesserung, die wir bei Remote-Serviceanrufen vornehmen können.

Betrachten wir den Fall, dass der Remote-Dienst fehlgeschlagen ist.

Wir möchten nicht immer wieder Anfragen abfeuern und Ressourcen verschwenden. Wir möchten im Idealfall die Anforderung für einen bestimmten Zeitraum einstellen, um dem Service Zeit für die Wiederherstellung zu geben, bevor die Anforderungen fortgesetzt werden. Dies wird alsShort Circuit Breaker-Muster bezeichnet.

Mal sehen, wie Hystrix dieses Muster implementiert:

@Test
public void givenCircuitBreakerSetup_whenRemoteSvcCmdExecuted_thenReturnSuccess()
  throws InterruptedException {

    HystrixCommand.Setter config = HystrixCommand
      .Setter
      .withGroupKey(HystrixCommandGroupKey.Factory.asKey("RemoteServiceGroupCircuitBreaker"));

    HystrixCommandProperties.Setter properties = HystrixCommandProperties.Setter();
    properties.withExecutionTimeoutInMilliseconds(1000);
    properties.withCircuitBreakerSleepWindowInMilliseconds(4000);
    properties.withExecutionIsolationStrategy
     (HystrixCommandProperties.ExecutionIsolationStrategy.THREAD);
    properties.withCircuitBreakerEnabled(true);
    properties.withCircuitBreakerRequestVolumeThreshold(1);

    config.andCommandPropertiesDefaults(properties);
    config.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter()
      .withMaxQueueSize(1)
      .withCoreSize(1)
      .withQueueSizeRejectionThreshold(1));

    assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));
    assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));
    assertThat(this.invokeRemoteService(config, 10_000), equalTo(null));

    Thread.sleep(5000);

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));

    assertThat(new RemoteServiceTestCommand(config, new RemoteServiceTestSimulator(500)).execute(),
      equalTo("Success"));
}
public String invokeRemoteService(HystrixCommand.Setter config, int timeout)
  throws InterruptedException {

    String response = null;

    try {
        response = new RemoteServiceTestCommand(config,
          new RemoteServiceTestSimulator(timeout)).execute();
    } catch (HystrixRuntimeException ex) {
        System.out.println("ex = " + ex);
    }

    return response;
}

Im obigen Test haben wir verschiedene Leistungsschaltereigenschaften eingestellt. Die wichtigsten sind:

  • DieCircuitBreakerSleepWindow, die auf 4.000 ms eingestellt sind. Dadurch wird das Fenster des Leistungsschalters konfiguriert und das Zeitintervall definiert, nach dem die Anforderung an den Remote-Service fortgesetzt wird

  • DasCircuitBreakerRequestVolumeThreshold, das auf 1 gesetzt ist und die Mindestanzahl von Anforderungen definiert, die erforderlich sind, bevor die Fehlerrate erreicht wird, wird berücksichtigt

Mit den oben genannten Einstellungen werden unsereHystrixCommand jetzt nach zwei fehlgeschlagenen Anforderungen geöffnet. Die dritte Anforderung trifft nicht einmal den Remote-Dienst, obwohl wir die Dienstverzögerung auf 500 ms eingestellt haben,Hystrix kurzschließen und unsere Methodenull als Antwort zurückgibt.

Anschließend fügen wir einThread.sleep(5000) hinzu, um die Grenze des von uns festgelegten Schlaffensters zu überschreiten. Dies führt dazu, dassHystrix den Stromkreis schließt und die nachfolgenden Anforderungen erfolgreich durchlaufen werden.

6. Fazit

Zusammenfassend soll Hystrix:

  1. Bieten Sie Schutz und Kontrolle über Ausfälle und Latenzen von Diensten, auf die normalerweise über das Netzwerk zugegriffen wird

  2. Beenden Sie die Kaskadierung von Fehlern, die auf den Ausfall einiger Dienste zurückzuführen sind

  3. Scheitern Sie schnell und erholen Sie sich schnell

  4. Zerfallen, wo es möglich ist

  5. Echtzeitüberwachung und Alarmierung der Kommandozentrale bei Störungen

Im nächsten Beitrag werden wir sehen, wie wir die Vorteile von Hystrix mit dem Spring-Framework kombinieren können.

Der vollständige Projektcode und alle Beispiele finden Sie aufgithub project.