Microservices mit Spring Cloud, Teil 2 Load Balancing in Microservice-Umgebungen

Von Dr. Dirk Koller

Einer der Hauptvorteile von Microservice-Architekturen ist die Skalierbarkeit. Doch wie funktioniert in diesem Umfeld eigentlich die Lastverteilung?

Um eine vernünftige Lastverteilung zu gewährleisten, erhält jeder aufrufende Microservice eigenen integrierten Load Balancer.
Um eine vernünftige Lastverteilung zu gewährleisten, erhält jeder aufrufende Microservice eigenen integrierten Load Balancer.
(Bild: geralt / Pixabay )

Bei hoher Last können in modernen Cloud-Umgebungen auf Knopfdruck weitere Service-Instanzen zugeschaltet und später wieder entfernt werden. Damit das funktioniert, muss die Last eingehender Anfragen mittels Load Balancer auf die einzelnen Instanzen verteilt werden.

Im Microservices-Umfeld werden dabei gerne Client-seitige Load Balancer verwendet. Jeder Service, der einen anderen Service aufruft, erhält also einen eigenen integrierten Load Balancer. Dadurch fällt der Single-Point-Of-Failure weg, den ein serverseitiger Load Balancer mit nachgeschalteten Services mit sich bringt.

Mehrere Instanzen starten

Im ersten Teil dieser Reihe ruft ein Microservice (Consumer) mithilfe des Registry-Dienstes Eureka einen anderen Service (Provider) auf. Hier soll nun eine zweite Instanz des Provider-Dienstes hochgefahren und die Anfragen zwischen beiden Instanzen aufgeteilt werden.

Erstellen der Run-Konfiguration in der Spring Tool Suite.
Erstellen der Run-Konfiguration in der Spring Tool Suite.
(Bild: Koller / Spring Boot)

Um mehrere Instanzen eines Service gleichzeitig zu betreiben, ist es erforderlich, den hart codierten Server-Port aus der Konfigurationsdatei application.properties oder application.yml herauszulösen. Die Instanzen sollen schließlich unter verschiedenen Ports erreichbar sein. In der Entwicklungsumgebung geschieht das mithilfe zweier verschiedener Run-Konfigurationen die den Server-Port 8890 bzw. 8891 als VM-Argument Port übergeben bekommen:

Services und Instanzen im Boot Dashboard.
Services und Instanzen im Boot Dashboard.
(Bild: Koller / Spring Boot)

In der Spring Tool Suite werden die beiden hochgefahrenen Instanzen inklusive der verwendeten Ports im Boot Dashboard aufgelistet. Mit dem übergeordneten Eintrag provider lassen sich bequem beide Instanzen gleichzeitig starten.

Um beim Aufruf erkennen zu können, welche Instanz antwortet, wird der Provider-Code aus dem letzten Teil noch um die Ausgabe des Ports erweitert. Man erhält die Information aus der Environment-Property local.server.port:

@EnableEurekaClient
@RestController
@SpringBootApplication
public class ProviderApplication {
  @Autowired
  Environment environment;
  public static void main(String[] args) {
    SpringApplication.run(ProviderApplication.class, args);
  }
  @GetMapping("/")
  public String time() {
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm:ss");
    return dtf.format(LocalDateTime.now()) + " from Port " + environment.getProperty("local.server.port");
  }
}

Die Codebeispiele im Beitrag bestehen aus Gründen der Übersichtlichkeit aus möglichst wenigen Klassen. In der Praxis würde man selbstverständlich eigene Klassen für Controller, Services usw. anlegen. Der Provider-Service ist damit einsatzbereit, er liefert die aktuelle Uhrzeit und den Port:

08:24:47 from Port 8890

Client-seitiges Load Balancing

Das Load Balancing wird im Client, also den Consumer-Service eingerichtet. Zu diesem Zweck erhält das POM die Abhängigkeit Spring Cloud Loadbalancer in Form des Artefakts spring-cloud-starter-loadbalancer:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

Spring Cloud Loadbalancer ist ein Wrapper um den eigentlich intern verwendeten Load Balancer. Im Moment ist das noch Ribbon, ein ehemaliges Netflix-Projekt, dank der generischen Abstraktionsschicht spielt das aber keine große Rolle.

Im Client-Code (Projekt Consumer) sind zwei kleine Anpassungen erforderlich. Zum einen wird die RestTemplate-Bean mit _@LoadBalanced_ gekennzeichnet. Die Marker-Annotation weist RestTemplate an, einen LoadBalancerClient für die Interaktion mit dem aufzurufenden Service zu verwenden. Außerdem wird anstelle der URL nun der Servicename (hier PROVIDER) als logischer Name für den Aufruf angegeben, der Load Balancer holt sich Host und Port von Eureka:

@EnableEurekaClient
@RestController
@SpringBootApplication
public class ConsumerApplication {
  @Autowired
  private RestTemplate restTemplate;
  public static void main(String[] args) {
    SpringApplication.run(ConsumerApplication.class, args);
  }
  @GetMapping("/")
  public String callProvider() {
    return restTemplate.getForObject("http://PROVIDER", String.class);
  }
  @LoadBalanced
  @Bean
  public RestTemplate restTemplate(){
    return new RestTemplate();
  }
}

Startet man nun Eureka, Provider- und Consumer-Service und ruft die URL des Consumer-Service (http://localhost:8889) mehrfach auf, so zeigt sich, dass die Zeitausgabe abwechselnd von den beiden hochgefahrenen Provider-Instanzen geliefert wird.

Load Balancing mit Feign

Das Client-seitige Load Balancing funktioniert auch mit dem Feign Client, ebenfalls Bestandteil von Spring Cloud. Dabei handelt es sich um eine Alternative zu Rest Template, bei der die Serveraufrufe nur in Form eines Interfaces beschrieben werden. Feign analysiert den dabei vergebenen Methodennamen und generiert die Implementierung selbst. Das Prinzip ist Spring-Entwicklern von JpaRepositories bekannt.

Feign Client wird durch folgenden Eintrag im POM.xml des Client eingebunden:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Eine einfache Implementierung eines Feign Clients, der einen GET-Request an / des Service namens PROVIDER absetzt, sieht folgendermaßen aus:

@FeignClient(path="/", name="PROVIDER")
public interface TimeClient {
  @GetMapping("/")
  public String callTimeService();
}

Der Service enthält keine RequestParameter oder Pfadvariablen, die beschreibende Methode im Interface ist deshalb sehr einfach gestrickt. _@EnableFeignClients_ in einer Konfigurationsklasse aktiviert die Feign-Funktionalität. Das Interface TimeClient wird hier mit Autowired injiziert und die darin die enthaltene, von Feign automatisch implementierte Methode, aufgerufen.

@EnableEurekaClient
@RestController
@SpringBootApplication
@EnableFeignClients
public class ConsumerApplication {
  @Autowired
  private TimeClient timeClient;
  public static void main(String[] args) {
    SpringApplication.run(ConsumerApplication.class, args);
  }
  @GetMapping("/")
  public String callProvider() {
    return timeClient.callTimeService();
  }
}

Ein Test zeigt, dass auch hier die Rückgabe der Daten abwechselnd von beiden Instanzen erfolgt, das Load Balancing arbeitet.

(ID:47767258)