Suche
Load Balancing, Fault Tolerance und Konfiguration - Spring Cloud – Teil 2

Mit Spring Cloud gegen die Komplexität

Eberhard Wolff

© Shutterstock.com / phloxii

Microservices sind verteilte Systeme – und stellen Entwickler vor komplexe Probleme. In diesem Teil der Spring-Cloud-Artikelserie geht es um Lastverteilung, die Vermeidung von Ausfällen bei REST-Kommunikation und die Konfiguration verteilter Services.

Im ersten Teil der Artikel-Serie (Java Magazin 3.2015) wurde das Projekt Spring Cloud vorgestellt. Spring Cloud basiert auf Spring Boot, das die Entwicklung von Spring-Anwendungen wesentlich vereinfacht und es auch erlaubt, Spring-Anwendungen ohne Application Server zu betreiben. Spring Cloud bietet zusätzlich zahlreiche Technologien und integriert einige Basistechnologien. Dadurch wird die Implementierung von Anwendungen für populäre Cloud-Anbieter einfacher. Zudem sind einige Features vor allem hinsichtlich der Herausforderungen bei Microservices-Systemen hilfreich.

Eine solche Herausforderung: Wenn in einem Netzwerk Microservices installiert sind, müssen die Dienste zueinander finden. Die Aufgabe ist eigentlich sehr einfach: Gegeben ein Name eines Services – auf welchem Rechner kann der Service unter welchem Port erreicht werden? Eine mögliche Lösung für dieses Problem ist die Service Registry Eureka aus dem Netflix-Stack. Diese Lösung ist speziell auf die Anforderungen von Microservices ausgerichtet. In einer Microservices-Architektur können Dienste kommen und gehen – schließlich kann eine neue Version eines Microservice installiert werden oder es werden mehr Instanzen gestartet, um mit höherer Last zurechtzukommen. Außerdem bietet eine Service Registry auch eine gute Möglichkeit, um Load Balancing und Failover umzusetzen. Unter dem Namen des Diensts können mehrere Instanzen registriert werden, die sich die Last teilen. Beim Ausfall einer Instanz kann eine andere die Arbeit übernehmen. Genau solche Features bietet Eureka an.

Der Fokus ist allerdings anders als bei klassischen Load Balancern: Eureka wird intern in einem Microservices-System genutzt – nicht für die HTTP-Requests von außen, wo klassische Load Balancer wirken. Eureka arbeitet daher auch anders als typische Load Balancer, die sich in den HTTP-Verkehr als Proxy einschalten: Eureka ist ein Server mit einer REST-Schnittstelle. Auf Anfragen gibt er für einen Namen jeweils eine Serviceinstanz zurück. Eureka kann zusammen mit einer Clientbibliothek verwendet werden, die Aufrufe der REST-Schnittstelle mit einem API für eine bestimmte Programmiersprache versieht. Eine solche Bibliothek gibt es für Java. Für andere Programmiersprachen muss ebenfalls eine entsprechende Library verwendet werden – oder man betreibt einen kleinen Java-Prozess, der die Kommunikation mit dem Eureka-Server regelt. Die Clientbibliothek hat neben der leichteren Nutzbarkeit noch einen weiteren Vorteil: Die Daten über die Microservices können auf dem Client in einem Cache gehalten werden. So kann die Kommunikation zwischen den Services selbst dann noch gewährleistet werden, wenn der Eureka-Server ausfällt – schließlich sind die Daten noch auf dem Client vorhanden. Natürlich kann Eureka auch in einem Cluster genutzt werden, dann replizieren die Server die Informationen. Die Server halten die Informationen über die Services im Speicher, was einen sehr schnellen Zugriff auf die Daten und eine einfache Implementierung erlaubt. Da die Daten mehrfach redundant im Netz liegen, ist dieser sehr einfache Ansatz völlig ausreichend.

Eureka mit Spring Cloud

Also benötigt ein Microservices-System, das Eureka verwendet, zunächst einen Eureka-Server. Eureka ist in Spring Cloud und Spring Boot integriert. Daher kann ein Server sehr einfach gestartet werden: Es muss lediglich die Hauptklasse der Spring-Boot-Anwendung mit einem @EnableEurekaServer annotiert werden. Der Eureka-Server kann als eigener Prozess laufen – eine entsprechende Implementierung findet sich auf GitHub. Oder der Eureka-Server wird in eine Spring-Boot-Anwendung integriert.

Nun müssen sich Anwendungen bei Eureka anmelden. Das ist mit einer Spring-Cloud-Anwendung ebenfalls sehr einfach: Die Anwendung muss als zusätzliche Build-Abhängigkeit das Modul spring-cloud-starter-eureka haben, und eine der Konfigurationsklassen der Anwendung muss mit @EnableDiscoveryClient annotiert sein. Mehr ist nicht notwendig. Nun wird die Anwendung automatisch registriert. Der Name kann über die Property spring.application.name gesetzt werden, der über die üblichen Spring-Boot-Mechanismen festgelegt werden kann, also beispielsweise in der Datei application.properties. Ebenso muss die Datei den zu benutzenden Eureka-Server festlegen – unter dem Schlüssel eureka.client.serviceUrl.defaultZone. Dieser Eintrag muss auch den Nutzer und das Passwort enthalten, beispielsweise http://user:password@localhost:8761/eureka/. Da die Konfiguration SpEL (Spring Expression Language) unterstützt, können Benutzername und Passwort natürlich auch aus Umgebungsvariablen ausgelesen werden.

Um einen Dienst tatsächlich zu finden, kann nun die Klasse DiscoveryClient genutzt werden, die Spring über Dependency Injection in Spring Beans injizieren kann. Mit getInstance() kann eine Datenstruktur ausgelesen werden, die Hostnamen und Port enthält, und zwar von allen bekannten Instanzen dieses Service.

Feign: REST ganz einfach

Manuell aus diesen Informationen einen URL zusammenzustellen ist natürlich mühselig, das geht sicher auch einfacher. Typischerweise kommunizieren Microservices beispielsweise über REST miteinander. Kommunikation mit REST ist allerdings ebenfalls kompliziert: Jeder einzelne Aufruf muss beispielsweise mit Springs RestTemplate einzeln formuliert werden. Vielleicht geht das auch einfacher? Die Implementierung eines REST-Servers besteht eigentlich auch nur aus Klassen, die einige Annotationen enthalten. Bei anderen Verteilungsansätzen ist es durchaus üblich, dass der Client aus einer ähnlichen Schnittstellendefinition generiert wird.

Feign nutzt genau diesen Ansatz für HTTP-basierte Frameworks. Es kann genutzt werden, um HTTP-Aufrufe aus einem annotierten Interface zu generieren und die Parameter zum Beispiel als JSON zu serialisieren. Ebenso gibt es eine Unterstützung für JAX-RS-Annotationen. So kann ein Interface mit den JAX-RS-Annotationen versehen werden, die eigentlich für Server gedacht sind. Die Implementierung des Interface durch Feign leitet dann die Aufrufe an einen REST-Server weiter.

Durch die Integration mit Spring Cloud hat Feign nun auch eine Unterstützung für REST-Web-Services, die mit den Spring-REST-Annotationen arbeitet.

Listing 1
@FeignClient("customer")
public interface CustomerClient {
  @RequestMapping(method=RequestMethod.GET, value="/customer/{id}")
  Customer findOne(@PathVariable("id") long id);
}

Ein Beispiel zeigt Listing 1. Dieses Interface trägt die Annotation @FeignClient, um zu signalisieren, dass für dieses Interface eine Implementierung mit Feign generiert werden soll. Diese Implementierung überträgt dann die Java-Aufrufe an das Interface in REST-Calls an den Server. customer ist der Name des Eureka-Service, an den Feign sich binden soll. Die Methode findOne() wird durch die @RequestMapping-Annotation an einen URL und eine HTTP-Methode gebunden. @RequestMapping wird sonst bei einer Spring-REST-Server-Implementierung genutzt. Schließlich definiert noch die Annotation @PathVariable, dass der letzte Teil des URL aus dem Parameter id übernommen werden soll. Auch diese Annotation wird normalerweise zur Implementierung von REST-Servern genutzt. Hinter den Kulissen wird natürlich auch das Customer-Objekt geeignet deserialisiert.

In der Anwendung kann nun mit Spring Dependency Injection eine Implementierung des Interfaces injiziert und genutzt werden. Hinter den Kulissen wird dann der REST-Service über Eureka ausgelesen und per REST angesprochen. Um die Implementierung des Interface tatsächlich anzustoßen, muss spring-cloud-starter-feign als Abhängigkeit zum Spring-Boot-Projekt hinzugefügt und in einer Spring-Konfiguration eine Annotation wie @FeignClientScan(„com.ewolff.microservice.order.feignclient“) eingetragen werden, damit Spring die annotierten Interfaces in dem angegebenen Package und allen Unterpackages sucht. Ein einfaches Beispiel findet sich auf GitHub.

Ribbon: Clientseitiges Load Balancing

Genau genommen ist bei Feign hinter den Kulissen noch eine weitere Bibliothek tätig, nämlich Ribbon. Ribbon ermöglicht auf dem Client ein Load Balancing. Der Client hat über Eureka eine Liste der möglichen Server bekommen und kann nun mit Ribbon aus dieser Liste einen Server auswählen. Die konkrete Wahl kann durch eine Konfiguration – entweder mit Properties oder in einer Konfigurationsklasse – gesteuert werden. So können Fault Tolerance und Load Balancing feingetunt werden. Erst mit Ribbon kann also aus der Eureka Service Registry der maximale Nutzen gezogen werden.

Eine Spring-Cloud-Anwendung kann Ribbon auch direkt durch das Interface LoadBalancerClient nutzen. Dann müssten die ausgelesenen Werte genutzt werden, um den Server anzusprechen. Durch die Integration in Springs RestTemplate ist es außerdem möglich, im URL den Namen eines Eureka-Diensts anzugeben. Dann wird hinter den Kulissen ebenfalls Ribbon für das Load Balancing genutzt.

Konfiguration

Wenn im Netzwerk viele Microservices laufen, sollte auch die Konfiguration zentralisiert werden. Schließlich kann es kaum sinnvoll sein, wenn für jede Konfigurationsänderung eine Vielzahl von Servern aktualisiert wird. Spring Cloud hat dafür einen eigenen Konfigurationsserver. Wie schon beim Eureka-Server kann auch dieser Server einfach durch eine Annotation in einer Spring-Konfigurationsklasse aktiviert werden, nämlich @EnableConfigServer. Die Anwendung benötigt dann noch eine Abhängigkeit zu spring-cloud-config-server. Der Server kann als eigener Prozess laufen oder auch in eine Spring-Boot-Anwendung integriert werden. Wenn Spring Security im Klassenpfad vorhanden ist, wird der Zugriff automatisch durch eine HTTP Basic abgesichert, sodass Unberechtigte die Werte nicht einfach auslesen können. Zudem können die Konfigurationswerte verschlüsselt werden, sodass Datenbankpasswörter nicht im Klartext übertragen werden.

Der Server kann seine Werte aus einem Git-Repository auslesen – oder auch aus einer Menge von Dateien. Ein Git-Repository hat den Vorteil, dass die Werte versioniert sind und auch ein gemeinsames Ändern der Konfiguration möglich ist. Eine Alternative sind einfach Dateien, wie sie auch sonst bei Spring üblich sind.

Die Werte werden dann über eine einfache REST-Schnittstelle angeboten. Dabei sind im Pfad des URL der Name der Anwendung, des aktiven Spring-Konfigurationsprofils und gegebenenfalls ein Git-Label enthalten. Das zurückgegebene JSON-Dokument enthält die verschiedenen Dateien, die für diese Konfiguration der Anwendung relevant sind, und die Werte aus diesen Dateien. Es ist also nicht unbedingt notwendig, dass die Anwendungen, die durch den Server konfiguriert werden sollen, alle mit Spring implementiert sind. Sie müssen lediglich die JSON-Daten herunterladen und interpretieren.

Mit Spring Boot ist die Nutzung des Servers besonders einfach: Damit eine Spring-Boot-Anwendung durch den Konfigurationsmechanismus erfasst wird, muss sie lediglich das spring-cloud-starter-parent-POM nutzen und spring-cloud-starter als Abhängigkeit haben. Ohne weitere Anpassungen nutzt die Anwendung den Server, der auf localhost unter dem Port 8888 erreichbar ist, um die Konfiguration auszulesen. Es kann aber in der Datei bootstrap.properties unter dem Schlüssel spring.cloud.config.uri ein anderer Server angegeben werden.

Die Anwendung meldet sich dann beim Konfigurationsserver automatisch an – mit ihrem Namen, der durch spring.application.name gesetzt ist. Vom Server wird dann eine Datei verwendet, die dem Namen der Spring-Anwendung entspricht, gegebenenfalls um das Profil ergänzt. Ebenso ist es möglich, Werte zu setzen, die für alle Anwendungen identisch sein sollen. Dabei können natürlich in den Anwendungen alle Spring-Boot-Konfigurationswerte gesetzt werden – oder eigene Werte, die mit Annotationen wie @Value oder @ConfigurationProperties in der Anwendung verwendet werden, oder in SpEL-Ausdrücken.

Ein einfaches Beispiel für einen Server ist auf GitHub zu sehen. Ebenso wird in der Dokumentation zum Spring-Cloud-Config-Projekt der Einsatz des Servers erläutert.

Rekonfiguration

Wenn die Konfiguration geändert wird, müssen die betroffenen Microservices das natürlich entsprechend verarbeiten. Ohne weiteres Zutun unterstützt der Konfigurationsmechanismus die Änderung der Werte allerdings nicht – es werden also nicht regelmäßig die Werte vom Server abgeholt, was sicher auch nicht besonders effizient wäre. Um die Werte wirklich zu aktualisieren, müsste die Anwendung ein EnvironmentChangedEvent über den Spring-Event-Mechanismus erhalten. Dieser Mechanismus ist eigentlich nur für Events innerhalb einer Spring-Anwendung gedacht, aber mit dem Sprint Cloud Bus [8] können auch solche Events an mehrere Anwendungen geschickt werden. Eine andere Möglichkeit ist es, den ApplicationContext neu zu starten. Das ist bei einer Spring-Boot-Anwendung nicht sonderlich schwierig: Man muss nur Spring Boot Actuator einbinden, das die Administration von Spring-Boot-Anwendungen vereinfacht. Dann muss der Administrator an den URL /admin/restart ein HTTP POST schicken. Das funktioniert aber nur, wenn die Eigenschaft enpoint.restart.enabled auf true gesetzt ist. Dann werden alle Spring Beans neu erzeugt, was fast dem Neustart der Anwendung gleichkommt.

Eine Alternative ist es, den Endpunkt /admin/refresh anzusprechen. Dann werden aber nur die Werte neu injiziert, die mit @ConfigurationProperties annotiert sind. Diese Annotation stammt aus dem Spring-Boot-Projekt. Das ist natürlich nicht immer ausreichend. Schließlich lesen viele Spring Beans die Konfiguration nur beim Erzeugen aus. Wenn also eine Spring Bean bei einer Konfigurationsänderung neu erzeugt werden muss, so ist auch das möglich: Die Bean muss nur mit @RefreshScope annotiert sein. Hinter den Kulissen wird statt der eigentlichen Bean jeweils ein Proxy injiziert. Wenn die Konfiguration geändert wird, schwenkt der Proxy auf eine Bean-Instanz um, die neu erzeugt worden ist. Die anderen Beans bekommen davon nichts mit.

DevOpsCon Whitepaper 2018

Free: BRAND NEW DevOps Whitepaper 2018

Learn about Containers,Continuous Delivery, DevOps Culture, Cloud Platforms & Security with articles by experts like Michiel Rook, Christoph Engelbert, Scott Sanders and many more.

Fazit

Spring Cloud bietet einen relativ einfach Mechanismus, um Microservices zu implementieren. Die Technologie löst Probleme wie die Kommunikation zwischen den Microservices – und dabei beachtet sie spezifischen Herausforderungen der Microservices-Architekturen. Dazu zählen Load Balancing und Fault Tolerance genauso wie eine zentralisierte Konfiguration. Spring Cloud erfindet aber das Rad nicht neu, sondern nutzt die Netflix-Bibliotheken, die sich in der Praxis schon bewährt haben. Die Integration in Spring Cloud macht die Nutzung dieser Bibliotheken sehr einfach. Das ist ein wesentlicher Vorteil, denn die Implementierung von verteilten Microservices-Architekturen ist kompliziert genug. Jede Vereinfachung ist dann hochwillkommen. Natürlich können die Mechanismen auch mit beliebigen anderen Programmiersprachen oder Systemen genutzt werden, denn sie werden auch mit REST-Schnittstellen angeboten. So können Microservices in einer Umgebung, die auf Spring Cloud basiert, auch mit anderen Technologien implementiert werden, aber die Implementierung mit Spring Cloud ist besonders einfach. Also wird die Technologiefreiheit nicht eingeschränkt, die ein Vorteil von Microservices-Architekturen darstellt.

Mit Spring Cloud und Spring Boot zeigt das Spring-Ökosystem außerdem, dass es auch bei modernen Ansätzen wie Microservices relevant bleibt. Das zeigt wieder einmal, wie flexibel und zukunftsfähig diese Technologien sind.

Aufmacherbild: spring cloud word via Shutterstock.com / Urheberrecht: phloxii

Verwandte Themen:

Geschrieben von
Eberhard Wolff
Eberhard Wolff
Eberhard Wolff ist Fellow bei innoQ und arbeitet seit mehr als fünfzehn Jahren als Architekt und Berater, oft an der Schnittstelle zwischen Business und Technologie. Er ist Autor zahlreicher Artikel und Bücher, u.a. zu Continuous Delivery und Microservices und trägt regelmäßig als Sprecher auf internationalen Konferenz vor. Sein technologischer Schwerpunkt sind moderne Architektur- und Entwicklungsansätze wie Cloud, Continuous Delivery, DevOps, Microservices und NoSQL.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: