Spring Boot und Hystrix (Timeout)

02.07.2016

Spring Boot - Timeouts von Remote-Calls mit Hystrix absichern

In diesem Artikel wird gezeigt, wie man einen Mikroservice A, gegen einen Timeout absichert, der einen anderen Mikroservice B aufruft, der verzögert oder überhaupt nicht antwortet. Um das zu Ereichen wird Hystrix verwendet. Im Fall einer Zeitüberschreitung von Mikroservice B soll in dem Beispiel der Mikroservice A, nicht weiter auf eine Antwort warten, sondern einen definierten Wert liefern.

In dem Beispiel wird von einer Spring Boot Applikation aus ein anderer Service aufgerufen, der ggf. länger für eine Antwort benötigt oder überhaupt nicht antwortet. Das ist für den aufrufenden Service nicht gut, da er nicht weiß, ob er noch auf eine Antwort warten oder weiterarbeiten soll.

Manche APIs stellen für Remote-Calls schon die Möglichkeit bereit ein Timeout zu setzen. Wie z.B. die URLConnection. Wenn die API so etwas nicht zur Verfügung stellt, kann man das mit Hystrix "Nachrüsten".

In dem Beispiel wird von einem Browser aus die REST-Schnittstelle des HystrixEchoSleepService aufgerufen, der den Wert, der über den Parameter echo übergeben wird, einfach als Antwort wieder zurück an den Browser liefert.

Allerdings ruft der HystrixEchoSleepService vorher noch den RandomSleepService auf und gibt den echo-String weiter. Der RandomSleepService wartet bevor er mit dem gleichen echo-String antwortet bis zu 5000ms.

In einer Umgebung, die produktiv genutzt wird, kann das zu lange sein, deshalb gibt es im HystrixEchoSleepService einen Fallback, der nach 2000ms eine Antwort an den Browser liefert. In dem Beispiel ist es beim Fallback auch der echo-String.

In einem realen Szenario könnte man sich Vorstellen, dass der HystrixEchoSleepService (KontostandService) versucht den aktuellen Kontostand durch den Aufruf des RandomSleepService (KontoPersistenceService) aufzurufen. Falls der KontoPersistenceService wegen einer langsamen Datenbank nicht innerhalbvon 2 Sekunden antwortet, könnte der KontostandService, einen Kontostand aus dem Cache liefern. Das ist besser als überhaupt kein Ergebnis zu liefern.

RandomSleepService erstellen

Los geht es auf der Seite start.spring.io. Wo Web ausgewählt wird, um anschliessend das Projekt-Template zu erstellen.

Das Template wird anschliessend in einer IDE geöffnet und die Klasse RandomSleepApplication um eine REST-Schnittstelle erweitert.

package org.hameister;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Random;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
public class RandomSleepApplication {

	public static void main(String[] args) {
		SpringApplication.run(RandomSleepApplication.class, args);
	}
}


@RestController
class SleepServiceController {

	@RequestMapping(value = "/sleep", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
	ResponseEntity<String> sleepForMilliseconds(@RequestParam("echo") String echo) throws InterruptedException {
		Long maxSleep =5000l;

		Random random = new Random();
		Long sleep = Math.abs(random.nextLong()%maxSleep);
		TimeUnit.MILLISECONDS.sleep(sleep);

		return new ResponseEntity<String>(echo, HttpStatus.OK);
	}
}

Dazu wird mit der Annotation RestController ein rest-Controller angelegt, der über /sleep zu erreichen ist und als Parameter einer String-Wert mit dem Namen echo übernehmen kann.

In der Methode sleepForMiliseconds() wird beim Aufruf bis zu 5 Sekunden gewartet bis als Return-Wert der Echo-String wieder zurückgegeben wird.

Dieser Service wird unter dem Port 2001 gestartet.

HystrixEchoSleepService erstellen

Los geht es auf der Seite start.spring.io. Wo Web und Hystrix ausgewählt wird, um anschliessend das Projekt-Template zu erstellen und es in einer IDE zu öffnen.

Die Klasse HystrixEchoSleepApplication wird um einen Rest-Controller erweitert, so dass der RandomSleepService aufgerufen werden kann.

package org.hameister;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableHystrix
public class HystrixEchoSleepApplication {

    public static void main(String[] args) {
        SpringApplication.run(HystrixEchoSleepApplication.class, args);
    }
}

@RestController
class EchoSleep {

    RestTemplate restTemplate = new RestTemplate();

    private static final  String timeoutInMilliSeconds ="2000";

        @HystrixCommand(
           commandKey = "circuitBreaker", fallbackMethod = "echoFallback",
    
           commandProperties = {
              @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = timeoutInMilliSeconds)
             }
        )
        
    @RequestMapping(value = "/sleep", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    ResponseEntity<String> echo(@RequestParam("echo") String echo) {

        try {
            restTemplate.getForEntity("http://localhost:2001/sleep?echo="+echo, String.class);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("InternalServerError - Try Fallback");
        }

        return new ResponseEntity<String>(echo, HttpStatus.OK);
    }


    @HystrixCommand
    public ResponseEntity<String> echoFallback(String echo) {
        return new ResponseEntity<String>("(Fallback) : slept too long: " + echo, HttpStatus.OK);
    }

}

In Zeile 42 wird dazu die echo-Methode ergänzt, die über /sleep?echo=XXXX erreichbar ist.

Außerdem wird in Zeile 18 wird mit @EnableHystrix Hystrix aktiviert.

In der Methode selbst wird über ein RestTemplate der RandomSleepService aufgerufen, der auf Port 2001 läuft.

Vor der echo-Methode wird durch die Annotation @HystrixCommand festgelegt, dass im eine Fallback-Methode (echoFallback) aufgerufen werden soll, falls die Ausführung der Methode länger als timeoutInMilliSeconds dauert.