Mit Docker und Spring Cloud Netflix eine elegante und effiziente Microservices-Architektur realisieren

Infrastruktur neu gedacht

Henning Ziburski

© Shutterstock / chungking

Wie so häufig liegen Licht und Schatten sehr nahe beieinander, dies gilt auch für Microservices-Architekturen in Kombination mit Docker. Anhand konkreter Praxisbeispiele wollen wir zeigen, wie die Technologien im Zusammenspiel funktionieren und wie damit schlanke, elegante und zukunftsweisende Architekturen umgesetzt werden können. Wichtig dabei: Wir haben es hier nicht nur mit einem reinen Technologiethema zu tun; ein Umdenken alter Prinzipien ist erforderlich. Doch wer diesen Weg geht, hat viel gewonnen.

Neben den vielen Vorteilen, die eine verteilte Microservices-Architektur mit sich bringen kann, gilt es auch, für die damit entstehenden Herausforderungen Lösungen zu finden. Im Vergleich zu klassischen monolithischen Anwendungen sind die einzelnen Microservices für sich gesehen jeweils nicht sonderlich komplex. Einen großen Overhead bringt jedoch die für Microservices benötigte Infrastruktur mit sich. Anstatt eines Application Servers für die gesamte Anwendung, muss für jeden Microservice ein eigener Application Server installiert, konfiguriert und betreut werden. Für die Kommunikation unter den einzelnen Microservices muss eine Lösung zur Service Discovery implementiert werden, und um angemessen auf Fehler und Latenzprobleme in einem verteilten System reagieren zu können, Techniken wie ein Circuit Breaker eingesetzt werden. Dieser Overhead an erforderlicher Infrastruktur kann vor allem kleinere Teams und Start-ups schnell vor ein großes Problem stellen.

DevOpsCon Istio Cheat Sheet

Free: BRAND NEW DevOps Istio Cheat Sheet

Ever felt like service mesh chaos is taking over? Then our brand new Istio cheat sheet is the right one for you! DevOpsCon speaker Michael Hofmann has summarized Istio’s most important commands and functions. Download FOR FREE now & sort out your microservices architecture!

Spring Cloud Netflix

Ebendiese Herausforderungen, die durch einen erhöhten Infrastrukturaufwand resultieren, versucht Spring Cloud Netflix zu lösen. Spring Cloud Netflix basiert auf Spring Boot und stellt die vielfach im produktiven Einsatz erprobte Netflix OSS für Spring-Boot-Anwendungen zur Verfügung. Durch die Integration der Netflix-Bibliotheken in Spring Cloud wird die ansonsten sehr komplexe Implementierung einer Microservices-Architektur deutlich vereinfacht.

Abb. 1: Übersicht der Spring-Cloud-Netflix-Komponenten

Abb. 1: Übersicht der Spring-Cloud-Netflix-Komponenten

Momentan beinhaltet das Spring-Cloud-Netflix-Projekt folgende Technologien der Netflix OSS (Abb. 1). Mit den zur Verfügung gestellten Technologien (in Abb. 1 blau hinterlegt), wird im Folgenden ein URL-Shortener-Service in einer Microservices-Architektur als Demoapplikation entwickelt und als Docker-Container bereitgestellt. Die Demoapplikation ist im GitHub Repository verfügbar.

Service Registry

In einer Microservices-Architektur sind die einzelnen Services meistens über verschiedene Hosts verteilt, sodass es gilt, sie miteinander bekannt zu machen. Im Grunde ist diese Aufgabe sehr simpel: Es wird lediglich eine Zuordnung der Services zum Host und Port der einzelnen Instanzen benötigt. In einer Microservices-Architektur sollen jedoch bei höherer Last dynamisch weitere Instanzen gestartet bzw. bei geringerer Last heruntergefahren werden können. Hinzu kommt, dass virtuelle Maschinen und Container in vielen Fällen dynamische IP-Adressen verwenden, wodurch eine statische Zuordnung zwischen Service und Host nicht praktikabel ist. Um dieser Anforderung gerecht zu werden, wird eine Service Registry benötigt, in der sich die einzelnen Clients beim Start registrieren können und beim Herunterfahren wieder entfernt werden.

Eine mögliche Antwort der Implementierung des Service-Registry-Pattern ist der Netflix-OSS-Eureka-Server. Eureka-Server ermöglicht einerseits die dynamische Registrierung von Clients und bietet außerdem die Funktionen eines einfachen Round-Robin Middle-Tier Load Balancings und Failovers. So wird die Last bei mehreren registrierten Instanzen pro Service verteilt, und es kann auf ausgefallene Instanzen reagiert werden. Spring Cloud macht den Einsatz des Eureka-Servers als Service Registry denkbar einfach. Hierzu muss eine Spring-Boot-Applikation mit der Dependency spring-cloud-starter-eureka-server erstellt und die Main-Application-Klasse mit @EnableEurekaServer annotiert werden (Listing 1).

@SpringBootApplication
@EnableEurekaServer
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(true).run(args);
    }
}

Der Eureka-Server kann wahlweise im Standalone- oder Peer-Modus betrieben werden. Während der Standalone-Modus schon ein gewisses Maß an Resilience mit sich bringt, indem die in der Service Registry registrierten Instanzen auf den einzelnen Clients gecacht werden, ist es im Peer-Modus möglich, mehrere Eureka-Server-Instanzen zu betreiben, die ihre Daten unter allen registrierten Peer Nodes replizieren. Wie der Eureka-Server betrieben werden soll, kann in der application.yml konfiguriert werden. Im Beispiel wird der Standalone-Modus verwendet (Listing 2).

server:
  port: 8761

eureka:
  instance:
    hostname: localhost
    preferIpAddress: true
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

Da als Default der Peer-Modus gewählt ist, muss für den Standalone-Modus das Clientverhalten deaktiviert werden. Ansonsten versucht Eureka fortlaufend, seine nicht vorhandenen Peer-Nodes zu erreichen, und würde dabei Fehler produzieren. serviceUrl verweist dabei auf die lokale Instanz. Der Eureka-Server kann nun mit dem Spring Boot Maven Plugin als executable JAR gebaut werden.

Service Discovery

Wenn der Eureka-Server gestartet ist, können sich die einzelnen Instanzen dort als Clients registrieren. Im Fall der Demoapplikation werden also die Services – ein Shortener- und ein Unshortener-Service – als Spring-Boot-Applikation erstellt und anschließend als Eureka-Clients registriert. Auch diese Aufgabe macht Spring wieder denkbar einfach, es muss lediglich die Dependency spring-cloud-starter-eureka eingebunden werden und die Main-Applikationsklasse mit @EnableEurekaClient annotiert werden. Anschließend wird der URL zum Eureka-Server in der application.yml konfiguriert (Listing 3).

eureka:
  instance:
    preferIpAddress: true
  client:
    serviceUrl:
      defaultZone: http://eureka:8761/eureka/

Wenn der Service in einem Docker-Container läuft, muss zusätzlich die Property preferIpAddress auf true gesetzt werden, da sich die Clients ansonsten mit der Docker-Container-ID als Hostname beim Eureka-Server registrieren würden.

Die Annotation @EnableEurekaClient sorgt dafür, dass sich die Instanz bei dem Eureka-Server registriert. Außerdem stellt sie den Netflix-DiscoveryClient zur Verfügung, um registrierte Instanzen eines Service vom Eureka-Server zu bekommen (Listing 4).

@Autowired
private DiscoveryClient discoveryClient;

public String serviceUrl() {
    InstanceInfo instance = discoveryClient.getNextServerFromEureka("SHORTENER", false);
    return instance.getHomePageUrl();
}

Mit dem URL des Discovery-Clients ist es jetzt beispielweise möglich, die REST-Schnittstelle des entsprechenden Service aufzurufen. Wesentlich bequemer wird der Aufruf von REST-Schnittstellen jedoch mit dem Einsatz von Netflix Feign. Feign ist ein deklarativer Web-Services-Client, dessen Aufgabe es ist, den Overhead an Code, der bei der Kommunikation zu Web-APIs entsteht, auf ein Minimum zu reduzieren. So ist es ausreichend, die Spring Feign Dependency spring-cloud-starter-feign einzubinden und ein Interface zu erstellen, das mit Feign und JAX-RS Annotations versehen ist. Feign kümmert sich dann um die Implementierung des Web-Services-Clients. Für die Demoapplikation wird dementsprechend folgendes Feign-Interface erstellt (Listing 5).

@FeignClient("SHORTENER")
interface ShortenerClient {
    @RequestMapping(value = "/url", method = RequestMethod.POST, consumes = "application/json")
    String shortenURL(String url);
}

Die Zuordnung zum Web Service erfolgt über die in der Annotation @FeignClient angegebenen Service-ID. Der Wert SHORTENER entspricht dabei der beim Eureka-Server registrierten ID des Service. Die Annotation @RequestMapping wird von Feign genutzt und bindet die Methode a den Web-Service-URL. In den Consumer-Klassen kann das Interface dann über Spring DI mittels @Autowired implementiert werden.

Resilience

In einer verteilten Microservices-Architektur wird es unweigerlich früher oder später zu Ausfällen einzelner Services kommen. Die Ausfälle können wie in klassischen monolithischen Applikationen aus fehlerhaften Komponenten der Anwendung resultieren oder durch „neue“ Fehlerquellen wie etwa Netzwerkfehler und Fehler auf den Hostsystemen der einzelnen Microservices entstehen. Ein einfaches Beispiel für eine Anwendung mit 30 Microservices und einer hervorragenden Verfügbarkeit von 99,99 Prozent jedes einzelnen Service sieht wie folgt aus:

99,9930 = 99,7 % Verfügbarkeit
0,3 % von 720 Stunden = mehr als 2 Stunden Downtime pro Monat

Ab einem gewissen Punkt ist es also nahezu unmöglich, die Verfügbarkeit weiter zu erhöhen, daher ist es erfolgsversprechender, einen Weg zu finden, um mit den auftretenden Fehlern angemessen umzugehen (Resilience). Neben den in verteilten Systemen hinzugekommenen Fehlerquellen gibt es allerdings auch einige Aspekte, die das Entwickeln von resilienter Software erleichtern. So ist es durch die lose Kopplung der einzelnen Services wesentlich einfacher, kaskadierende Fehler zu vermeiden, da jeder Service einen so genannten Bulkhead bildet, in dem sich auftretende Fehler einfacher isolieren lassen.

An dieser Stelle kommt Hystrix aus der Netflix OSS ins Spiel. Hystrix bietet u. a. eine Implementierung des Circuit-Breaker-Patterns, die Möglichkeit auf Time-outs zu reagieren und Fallbacks. Wie bereits von Spring Cloud Netflix gewohnt, ist es auch für Hystrix ausreichend, die Dependency spring-cloud-starter-hystrix einzubinden und anschließend die jeweilige Klasse mit @EnableHystrix zu annotieren. In der Demoapplikation wird Hystrix als Circuit Breaker eingesetzt, um bei fehlerhaften Anfragen den Threadpool zu schützen und um bei lang laufenden Netzwerkanfragen die Anfrage frühzeitig abbrechen zu können.

Durch die Annotation von Methoden mit @HystrixCommand werden die Methodenaufrufe in einem Hystrix-Objekt gekapselt und in einem separaten Thread ausgeführt. Die Default-Einstellungen für Methoden, die mit @HystrixCommand annotiert werden, aktivieren den Circuit Breaker und berechnen einen Time-out, nachdem der Aufruf abgebrochen wird. In der Demoapplikation wird der Aufruf des Feign-Clients wie folgt in einem Hystrix-Command-Objekt gekapselt (Listing 6).

@EnableHystrix
public class ApiGateway {

    @Autowired
    ShortenerClient shortenerClient;

    @RequestMapping("/url", method = RequestMethod.POST)
    @HystrixCommand(fallbackMethod = "defaultShortURL")
    public String shortenURL(@RequestBody String url) {
        return shortenerClient. shortenURL ();
    }

    private String defaultShortenURL(String url) {
        return "Der Service ist nicht verfügbar.";
    }
}

Über den Parameter fallbackMethod der @HystrixCommand Annotation wird der Name einer Methode angegeben, die aufgerufen werden soll, wenn der Aufruf des Hystrix Commands zu einem Fehler führt. Die Fallback-Methode muss dabei natürlich den gleichen Rückgabewert und die gleichen Parameter wie die ursprüngliche Methode haben.

Persistenz

In klassischen monolithischen Anwendungen gibt es in der Regel eine zentrale Datenbank, auf die alle Services der Anwendung zugreifen. Mit einer konsequent entkoppelten Microservices-Architektur lässt sich dieses Konzept jedoch nicht vereinen. Eine lose Kopplung bedeutet, dass einzelne Services unabhängig voneinander geändert werden können. Das schließt auch die Persistenzschicht ein. In der Demoapplikation wird Cassandra mit spring-data-cassandra als Datenbank verwendet.

Docker

Microservices-Architekturen erzeugen im Vergleich zu monolithischen Anwendungen auch bei der Bereitstellung einen sehr hohen Konfigurationsaufwand. So muss für jeden Microservice eine eigene Umgebung abhängig vom gewählten OS, der gewählten Programmiersprache, der Datenbank usw. erstellt und konfiguriert werden. Dieser Aufwand wiederholt sich, sobald ein Service aufgrund höherer Last skaliert werden soll. Eine mögliche Lösung für dieses Problem kann der Einsatz von virtuellen Maschinen für die einzelnen Services sein. Virtuelle Maschinen benötigen jedoch vergleichsweise viele Ressourcen, da sie ein eigenes Guest OS auf dem Host betreiben und im Vergleich zu Containern aufwendig zu erstellen sind.

An dieser Stelle kommt Docker ins Spiel. Mit Docker können die einzelnen Microservices inklusive der benötigten Abhängigkeiten in leichtgewichtige Container gepackt werden. Um die einzelnen Spring-Boot-Services der Demoapplikation in Docker-Container zu packen, sind im Wesentlichen die folgenden Schritte notwendig:

  • Laden eines Base Images vom Docker Hub
  • Java installieren
  • Kopieren der executable JAR des Services
  • Das Image bei Start des Containers ausführen

Diese Schritte, die zum Starten eines Service in einem Docker-Container ausgeführt werden, können auch in einem Dockerfile angegeben werden. So ist es mit dem Befehl docker build möglich, die Erstellung des Images zu automatisieren. Das Dockerfile des Shortener Service der Demoapplikation zeigt Listing 7.

# Base Image
FROM ubuntu:latest

# Freigabe des Port 8080
EXPOSE 8080

# Kopieren und entpacken der JRE
ADD jre-8u45-linux-x64.tar.gz /root/

# Kopieren des executable JAR
COPY shortener-1.0-SNAPSHOT.jar /root/shortener-1.0-SNAPSHOT.jar

# Konfigurieren der JRE
RUN mkdir -p /opt/oracle/java && cp -r /root/jre1.8.0_45 /opt/oracle/java/ && \
  update-alternatives --install "/usr/bin/java" "java" "/opt/oracle/java/jre1.8.0_45/bin/java" 1 && \
  update-alternatives --install "/usr/bin/javaws" "javaws" "/opt/oracle/java/jre1.8.0_45/bin/javaws" 1 && \
  update-alternatives --set "java" "/opt/oracle/java/jre1.8.0_45/bin/java" && \
  update-alternatives --set "javaws" "/opt/oracle/java/jre1.8.0_45/bin/javaws"

# Starten des Services
CMD java -jar /root/shortener-1.0-SNAPSHOT.jar

Die Erstellung von Docker-Containern kann zusätzlich in CI- und Build-Server-Tools wie Jenkins oder Atlassian Bamboo integriert und somit weiter automatisiert werden.

Nachdem das Docker Image mit dem Service gebaut wurde, muss der Container gestartet werden. Das Erstellen und Starten eines Containers aus einem Image erfolgt in Docker mit dem Befehl docker run. Im Fall der Demoapplikation muss dem Microservice der Link zum Eureka-Server mitgegeben werden, damit er sich dort registrieren kann und Zugriff auf die Service Discovery erhält. Damit ergibt sich für den Shortener-Service der Befehl docker run -d –link eurekaserver:eureka repos/image.

Fazit

Das Spring-Cloud-Netflix-Projekt hat es geschafft, die weltweit millionenfach in der Praxis erprobte Infrastruktur von Netflix in das Spring-Boot-Projekt zu integrieren und bietet so eine Lösung, die durch verteilte Microservices-Architekturen gestiegenen Anforderungen an die Infrastruktur zu kompensieren. Einziger Wehrmutstropfen an dieser Stelle ist die hohe Abhängigkeit von Spring und der Netflix OSS, in die man sich mit dieser Lösung begibt. Die Kombination von Docker und Microservices ist sehr vielversprechend. Docker bietet leichtgewichtige, portable Container, die auf die Anforderungen der einzelnen Microservices angepasst werden können.

Aufmacherbild: city interchange via Shutterstock / Urheberrecht: chungking

Verwandte Themen:

Geschrieben von
Henning Ziburski

Als Software Engineer bei der best-blu consulting with energy GmbH ist Henning Ziburski in diversen Projekten unterwegs und kennt daher aus eigenen Erfahrungen die Möglichkeiten und auch die Hürden beim Einsatz von Technologien. Er ist aktiv in der Java User Group Bremen und berät und unterstützt mit seinem Wissen zu Trends die Exzellenzinitiative bestXperts.

Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: