Microservices mit Spring Cloud, Teil 1 Service Discovery mit Eureka

Autor / Redakteur: Dr. Dirk Koller / Stephan Augsten

In Spring Cloud sind Werkzeuge enthalten, welche die Arbeit mit Microservices erleichtern. Wir stellen beginnend mit dem Service-Discovery-Dienst Eureka einige dieser Tools vor.

Firmen zum Thema

Das Eureka-Dashboard listet die registrierten Dienste auf.
Das Eureka-Dashboard listet die registrierten Dienste auf.
(Bild: Koller / Spring.io)

Microservices sind einer der großen Trends in der Softwareentwicklung der letzten Jahre. Die Idee, den behäbigen und schwer wartbaren Monolithen im Application Server gegen schlanke Services in eigenen Prozessen auszutauschen, hat die Entwicklung in den Unternehmen verändert.

Die unbestrittenen Vorteile der flotten Dienste wie schnelle Release-Zyklen, freie Wahl der Programmiersprache, dynamische Skalierung und individuelles Deployment stehen dabei allerdings einer ebenso großen Menge Problemen gegenüber. Einige davon (Sind Microservices überhaupt die richtige Architektur? Welche Granularität sollten die Services haben?) sind konzeptioneller Natur und eine Aufgabe für den Architekten. Andere Probleme dagegen betreffen eher die technische Infrastruktur:

  • Wie bekommt man die Menge an Konfigurationsdaten unter Kontrolle?
  • Wie lassen sich Services auffinden, wenn sich Ports und IPs schnell ändern?
  • Wie kann man den Weg eines Aufrufs über mehrere Services hinweg nachverfolgen?
  • Wer verteilt wie die Last eingehender Anfragen auf mehrere Instanzen eines Services?
  • Wer leitet Anfragen an andere Services weiter?
  • Wie verhindert man, dass ein Ausfall eines Services nicht das „Gesamtkunstwerk“ blockiert?

Für diese Infrastrukturprobleme gibt es zahlreiche Lösungen, eine davon ist das Projekt Spring Cloud, das im Rahmen dieser Reihe vorgestellt werden soll. Unter anderem sind das die auch als Netflix-Stack bekannten Dienste Eureka (Service Discovery), Zuul (Intelligentes Routing), Ribbon (Client-seitiges Load Balancing) und Hystrix (Circuit Breaker).

Alle Services liegen als Spring Boot-Dependencies vor, die man leicht in eigene Projekte integrieren kann. Den Anfang macht eine Registry zum Auffinden von Services anhand eines logischen Namens: Eureka.

Aufsetzen eines Eureka-Dienstes

Einer Spring-Anwendung teilt man IP-Adresse und Port eines aufzurufenden (zweiten) Service üblicherweise im Properties-File hartkodiert mit. Für Microservice-Architekturen ist das kein gangbarer Weg. Durch die automatische Skalierung, also das Hinzufügen weiterer Instanzen zur Laufzeit, ist ein Service unter mehreren IPs/Ports erreichbar und es können jederzeit neue hinzukommen (oder wieder verschwinden).

Die Lösung für dieses Problem ist ein Naming- oder Registry-Service wie Eureka, bei dem sich alle Service-Instanzen registrieren. Jeder Service kennt dabei nur IP und Port dieses Discovery-Service und die logischen Namen aufzurufender Services. Die erforderlichen Verbindungsdaten erhalten die Services dann von Eureka.

Erstellen des Eureka-Server mit dem Initializr.
Erstellen des Eureka-Server mit dem Initializr.
(Bild: Koller / Spring.io)

Zum Erstellen eines Eureka-Servers wird ein neues Projekt mit dem Spring Initializr angelegt und dabei die Abhängigkeiten Eureka Server und Actuator zugefügt.

Ein Blick in die Datei pom.xml des erzeugten Projekts verrät, dass die Abhängigkeiten spring-cloud-starter-netflix-eureka-server und spring-boot-starter-actuator zugefügt wurden. Außerdem wird spring-cloud-dependencies in der derzeit aktuellen Version 2020.0.3 referenziert.

Zur Inbetriebnahme des Servers sind nur wenige Schritte notwendig. Die Application-Klasse erhält die Annotation @EnableEurekaServer:

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryServerApplication {
   public static void main(String[] args) {
      SpringApplication.run(DiscoveryServerApplication.class, args);
   }
}

Name und Port des Service werden im Properties-File application.yml hinterlegt:

server:
   port: 8761
spring:
   application:
      name: discovery-server

Startet man den Server, erhält man Fehlermeldungen. Der Server möchte sich seinerseits bei Eureka registrieren und Informationen über andere Endpunkte laden. Das Feature ist für komplexe Szenarien mit mehreren Eureka-Servern gedacht und wird deshalb in application.yml deaktiviert:

eureka:

   client:
   register-with-eureka: false
   fetch-registry: false

Das Eureka-Dashboard listet die registrierten Dienste auf.
Das Eureka-Dashboard listet die registrierten Dienste auf.
(Bild: Koller / Spring.io)

Danach startet der Discovery-Service ohne Fehler. Ruft man die URL http://localhost:8761 im Browser auf, landet man auf dem Eureka-Dashboard.

Registrierung eines Service

Wie man sieht, sind derzeit noch keine Service-Instanzen bei Eureka registriert. Das gilt es nun zu ändern. Zu diesem Zweck wird hier ein einfacher Service namens Provider angelegt, der von einem Client-Service aufgerufen werden soll. Welche Daten der Service zur Verfügung stellt, ist unerheblich. Damit der Code überschaubar bleibt, gibt er hier einfach die aktuelle Uhrzeit zurück. Auch auf eine saubere Aufteilung in Service-Klasse, Controller-Klasse usw. wird aus Platzgründen verzichtet:

@EnableEurekaClient
@RestController
@SpringBootApplication
public class ProviderApplication {
   public static void main(String[] args) {
      SpringApplication.run(ProviderApplication.class, args);
   }
   @GetMapping("/")
   public String time() {
      DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd.MM.yyyy");
   return dtf.format(LocalDateTime.now());
   }
}

Die verwendete Annotation _@EnableEurekaClient_ findet sich im Starter spring-cloud-starter-netflix-eureka-client, diese Abhängigkeit muss im Initializr als Dependency zugefügt werden. Wer das lieber von Hand im POM machen möchte, muss dran denken, auch wieder die Abhängigkeit zu spring-cloud-dependencies zuzufügen. In der Konfigurationsdatei bekommt der Provider-Service den (eigenen) Port und Namen und die URL des Eureka-Servers mitgeteilt:

server:
   port: 8888
spring:
   application:
      name: provider
eureka:
   client:
   serviceUrl:
      defaultZone: http://localhost:8761/eureka/

Nach dem Start des Provider-Service taucht dieser in Eureka unter den registrierten Services auf und lässt sich im Browser mit http://localhost:8888 aufrufen. Der registrierte Client sendet alle 30 Sekunden einen Heartbeat an den Server. Bleibt dieser aus, wird er aus der Liste der registrierten Instanzen entfernt.

Aufruf mithilfe der Registry

Der Client des Provider-Service ist hier ebenfalls ein Spring Boot-Web-Service. Auch bei diesem Service handelt es sich um eine Quick and Dirty-Implementierung, die nicht als Vorlage für gute Webservices dienen soll. Da auch er mit Eureka sprechen muss, benötigt er ebenso die Dependency spring-cloud-starter-netflix-eureka-client, die Annotation @EnableEurekaClient einen Namen (consumer) und Port (8889) sowie die URL des Eureka-Servers in application.yml.

Die Klasse bekommt ein EurekaClient-Objekt injiziert, mit dem der Eureka-Server nach einer Application befragt und anschließend von einer Instanz dieser Application IP und Port ermittelt werden können:

@EnableEurekaClient
@RestController
@SpringBootApplication
public class ConsumerApplication {
   @Autowired
   private EurekaClient eurekaClient;
   @Autowired
   private RestTemplate restTemplate;
   public static void main(String[] args) {
      SpringApplication.run(ConsumerApplication.class, args);
   }
   @GetMapping("/")
   public String callProvider() {
      Application application = eurekaClient.getApplication("provider");
      if(application != null) {
         InstanceInfo instanceInfo = application.getInstances().get(0);
         String url = "http://" + instanceInfo.getIPAddr() + ":" + instanceInfo.getPort() + "/";
         return "Die ermittelte Uhrzeit ist " + restTemplate.getForObject(url, String.class);
      } else {
         return "Provider nicht verfügbar";
      }
   }
   @Bean
   public RestTemplate restTemplate() {
      return new RestTemplate();
   }
}

Ruft man im Browser nun http://localhost:8889 auf, wird der zurückgegebene String inklusive der vom Provider ermittelten Uhrzeit angezeigt. Der Vorteil von Eureka wird deutlich, wenn man den Port des Providers ändert. Der Consumer bezieht die Daten vom Eureka-Server, der über die Änderung informiert wird. Im Consumer muss also nichts angepasst werden.

In diesem Beitrag wurde nur die Grundfunktionalität angerissen, es gibt eine Menge mehr zu entdecken. Die Eureka-Doku enthält ausführliche Informationen.

(ID:47645451)