1. Überblick
In diesem Artikel schauen wir uns an, wie mehrere Microservices untereinander über REST auf OpenShift kommunizieren. Damit wir die Kommunikation und die Konzepte Load Balancing sowie Service Discovery auf OpenShift demonstrieren können, entwickeln wir im Rahmen dieses Artikels drei Microservices, die über deklarative REST Clients miteinander integriert werden.
2. Einleitung
Für unser Szenario entwickeln wir drei Microservices: customer-service, product-service und order-service. Wie die Namen bereits verraten hält der customer-service Kundendaten, der product-service Produktdaten und der order-service die eigentliche Bestellungen bereit. Um die Beispielanwendung schmal zu halten existiert keine Persistenzschicht, die Daten sind statisch im Code.
API Beschreibung:
- order-service/orders/
- order-service/orders/{id}
- customer-service/customers
- customer-service/customers/{id}
- product-service/products
- product-service/products/{id}
Um die Integration zwischen allen Services zu betrachten interessiert uns der Aufruf order-service/orders/{id} , hierbei erfolgen mehrere Serviceaufrufe, zum einen an den customer-service um den jeweiligen Kunden zu holen und zum anderen an product-service um die Produktinformationen zu der Bestellung zu erhalten.
3. Implementierung: product-service und customer-service
Die beiden Microservices product-service und customer-service sind komplett identisch aufgebaut, wir schauen uns daher nur einen Service genauer an.
Den gesamten Quellcode findet ihr auf GitHub.
https://github.com/softwarehandwerk/sample-openshift-microservices.git
Branch: sample-communication
Die Klassen sind auf die Ordner controller, service und model aufgeteilt. In dem Ordner openshift/ sind die YAML Konfigurationsdateien für die Erstellung der OpenShift Container zu finden.
product-service
|
├── openshift
│ ├── app-deployment.yaml
│ ├── app-route.yaml
│ └── app-service.yaml
├── src
│ ├── main
│ ├── java
│ └── com
│ └── example
│ ├── ProductServiceApplication.java
│ ├── controller
│ │ └── ProductController.java
│ ├── model
│ │ └── Product.java
│ └── service
│ └── ProductService.java
├── pom.xml
Im folgendem sehen wir die pragmatische Implementierung der Klassen Product
, ProductService
und ProductController
.
@Data
@Builder
public class Product {
private String id;
private String name;
private String description;
private String price;
private String podInfo; // 1)
}
Das Model enthält neben den Produktinformationen das Feld podInfo 1)
, dieses werden wir benötigen um später das Load Balancing zu demonstrieren.
@Service
public class ProductService {
private List<Product> productList = List.of(
Product.builder()
.id("1")
.name("iPhone 11 pro“)
.description("Pro Kameras. Pro Display. Pro Performance.“)
.price("889").build(),
Product.builder()
.id("2")
.name("Xiaomi mi 9")
.description("128GB Schwarz Dual SIM hat 6GB RAM und eine 48 MP")
.price("309").build(),
Product.builder()
.id("3")
.name("16 MacBook Pro")
.description("Native Auflösung von 3072 x 1920 Pixeln bei 226 ppi")
.price("2.699").build() ); // 2)
public List<Product> getAll(){
return productList;
}
public Product getById(String id){
return productList.stream()
.filter(product -> product.getId().equals(id))
.findFirst().orElse(Product.builder().build());
}
}
Der ProductService
greift auf keine Persistenzschicht sondern beinhaltet statische Werte 2)
mit denen der Service arbeitet.
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public List<Product> getAll(){
return productService.getAll();
}
@GetMapping("/{id}")
public Product findById(@PathVariable("id") String id){
Product product = productService.getById(id);
product.setPodInfo(System.getenv("HOSTNAME")); // 3)
return product;
}
}
Im RestController
wird beim Zurückgeben der Produktdaten /products/{id} die PodInfo 3)
mit System.getenv("HOSTNAME“)
gesetzt. Hierdurch identifizieren wir später welcher Container die Daten zurück liefert.
4. Implementierung: order-service mit OpenFeign
Der order-service greift auf die anderen Services zu und holt sich von ihnen die Daten. Von der Struktur ist er genauso aufgebaut wie customer-service und product-service.
Der OrderService
hält ebenfalls statische Testdaten bereit. Eine Order
hat Informationen über Kunden und Produkte. Im folgendem Listing sehen wir, dass über customerId
und productIdList
die Integration hergestellt wird. Anhand dieser Beziehung demonstrieren wir, wie die Auflösung der IDs funktioniert.
private List<Order> orderList = List.of(
Order.builder().id("1").status("STORNIERT").customerId("2").productIdList(List.of("1","2","3")).build(),
Order.builder().id("2").status("VERSENDET").customerId("3").productIdList(List.of("2")).build(),
Order.builder().id("3").status("GELIEFERT").customerId("1").productIdList(List.of("1","1","1","1")).build()
);
Deklarative REST Clients mit OpenFeign
Um Kunden- und Produktinformationen zu bekommen, erstellen wir mit Hilfe von OpenFeign einen Client für die REST Endpoints. Wir beschreiben hierfür ein Interface mit bekannten Spring MVC Annotation, die wir aus dem Bereitstellen von REST Endpoints bereits kennen. Den Rest übernehmen für uns Feign-spezifische Annotation.
OpenFeign ist mittlerweile Teil des Umbrella Projektes Spring Cloud für den es auch einen eigenen Spring Starter existiert.
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Wir aktivieren die Verwendung von OpenFeign mit der Annotation @EnableFeignClients
und die eigentlichen HTTP-Clients bekommen die Annotationen @FeignClient
wo wir auch die URL der Endpoints deklarieren.
CustomerClient:
@FeignClient(name = "customers", url = "${microservices.customer.url}")
public interface CustomerClient {
@GetMapping("/customers/")
List<Customer> getAllCustomers();
@GetMapping("/customers/{id}")
Customer getCustomerById(@PathVariable("id") String id);
}
ProductClient:
@FeignClient(name = "products", url = "${microservices.product.url}")
public interface ProductClient {
@GetMapping("/products/")
List<Product> getAllProducts();
@GetMapping("/products/{id}")
Product getProductById(@PathVariable("id") String id);
}
application.properties
microservices.product.url=http://product-service:8080
microservices.customer.url=http://customer-service:8080
5. Service Discovery und Load Balancing
Die URLs für den FeignClient verweisen auf die Namen der Container, dadurch wird die Service Discovery komplett an OpenShift und Kubernetes übergeben. Dies hat ebenfalls den Vorteil, dass auch das Load Balancing von der Orchestrierungsplattform übernommen wird. Es ist nicht notwendig eigene Dienste für Service Discovery und Load Balancing aufzusetzen.
Um das Load Balancing zu demonstrieren haben wir in die Model Entitäten die Podinformation in podInfo
hinzugefügt, die im Controller kurz vor dem Zurückliefen der Objekte gesetzt werden.
Anhand der nächsten Abbildung sehen wir, dass wir in OpenShift die Pod Anzahl vom product-service auf drei Pods erhöht haben. Dies könnt ihr in der Konsole machen oder bequem über die OpenShift Web Oberfläche. Es ist weiterhin keine weitere Konfiguration in der Anwendung notwendig, kein zusätzliches registrieren der zusätzlichen Instanzen, die Auflösung erfolgt über die Namen der Services.

Wenn wir anschließend den Aufruf vom http://order-service-myproject.192.168.64.6.nip.io/orders/2 mehrmals hintereinander ausführen, sehen wir, dass sich die PodInfo verändert und die Requests von verschiedenen Pods verarbeitet werden.
$ curl http://order-service-myproject.192.168.64.6.nip.io/orders/2
{"id":"2","status":"VERSENDET","customer":{"id":"3","firstName":"Andy","lastName":"Müller","podInfo":"customer-service-4-9knqd"},"productList":[{"name":"Xiaomi mi 9","description":"128GB Schwarz Dual SIM hat 6GB RAM und eine 48 MP","price":"309","podInfo":"product-service-8-7qbzc"}]}
Das Load Balancing erfolgt über einen HAProxy, die Default Strategie hierbei ist Round Robin und kann bei Bedarf angepasst werden.
6. Deployen der Microservices auf OpenShift
Die YAML-Konfigurationsdateien zum Deployen der Anwendung in OpenShift sind im openshift/ Ordner zu finden. Wir benutzen das Maven Plugin fabric8-maven-plugin, um ein Docker Image zu bauen und dieses anschließend in das interne OpenShift Repository zu pushen.
Wie baue ich ein Docker Image für meine Spring Boot Anwendung
Nach dem erfolgreichen Bauen eines Docker Images, wie wir es bereits in den oben erwähnten Artikeln kennengelernt haben, können wir über die YAML Konfigurationsdateien OpenShift Container erstellen:
$ oc apply -f app-deployment.yaml
$ oc apply -f app-service.yaml
$ oc apply -f app-route.yaml
7. Fazit
Es gibt unterschiedliche Ansätze um die Kommunikation und die Bereitstellung von Microservices zu realisieren. Die Anfänge kamen aus dem Open Source Netflix Stack mit Diensten wie Heureka, Hystrix, Zuul usw. und sind auch irgendwann zum Teil in das Umbrella Projekt Spring Cloud gewandert. Mit Kubernetes und OpenShift wurden die Themen wie Service Discovery, Load Balancing, API Gateways und viele weitere auf der Platformebene angegangen. In diesem Artikel haben wir uns angeschaut wie Microservices nativ auf OpenShift kommunizieren können.
https://github.com/softwarehandwerk/sample-openshift-microservices.git (branch: sample-communication)
Source Code auf GitHub