In traditional monolithic architecture, applications already knew where the backend services existed through static hostnames, IP addresses, and ports. The IT operation team maintained the static configurations for service reliability and system stability. This Day 2 operation has significantly changed since microservices began running in distributed networking systems. The change happened because microservices need to communicate with multiple backend services to improve the load balancing and service resiliency.
The microservices topology became much more complex as the service applications were containerized and placed on Kubernetes. Because the application containers can be terminated and recreated anytime by Kubernetes, the applications can't know the static information in advance. The microservices don't need to be configured with the static information of the backend applications because Kubernetes handles service discovery, load balancing, and self-healing dynamically and automatically.
However, Kubernetes doesn't support programmatic service discovery and client-based load balancing through integrated application configurations. Smallrye Stork is an open source project to solve this problem, providing the following benefits and features:
- Augment service discovery capabilities
- Support for Consul and Kubernetes
- Custom client load-balancing features
- Manageable and programmatic APIs
Nevertheless, Java developers need some time to adapt to the Stork project and integrate it with an existing Java framework. Luckily, Quarkus enables developers to plug Stork's features into Java applications. This article demonstrates how Quarkus allows developers to add Stork's features to Java applications.
Create a new Quarkus project using Quarkus CLI
Using the Quarkus command-line tool (CLI), create a new Maven project. The following command will scaffold a new reactive RESTful API application:
$ quarkus create app quarkus-stork-example -x rest-client-reactive,resteasy-reactive
The output should look like this:
...
[SUCCESS] ✅ quarkus project has been successfully generated in:
--> /Users/danieloh/Downloads/demo/quarkus-stork-example
...
Open a pom.xml
file and add the following Stork dependencies: stork-service-discovery-consul and smallrye-mutiny-vertx-consul-client. Find the solution to this example here.
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-service-discovery-consul</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye.reactive</groupId>
<artifactId>smallrye-mutiny-vertx-consul-client</artifactId>
</dependency>
Create new services for the discovery
Create two services (hero
and villain
) that the Stork load balancer will discover. Create a new services directory in src/main/java/org/acme
. Then create a new HeroService.java
file in src/main/java/org/acme/services
.
Add the following code to the HeroService.java
file that creates a new HTTP server based on the Vert.x reactive engine:
@ApplicationScoped
public class HeroService {
@ConfigProperty(name = "hero-service-port", defaultValue = "9000") int port;
public void init(@Observes StartupEvent ev, Vertx vertx) {
vertx.createHttpServer()
.requestHandler(req -> req.response().endAndForget("Super Hero!"))
.listenAndAwait(port);
}
}
Next, create another service by creating a VillainService.java
file. The only difference is that you need to set a different name, port, and return message in the init() method as below:
@ConfigProperty(name = "villain-service-port", defaultValue = "9001") int port;
public void init(@Observes StartupEvent ev, Vertx vertx) {
vertx.createHttpServer()
.requestHandler(req -> req.response().endAndForget("Super Villain!"))
.listenAndAwait(port);
}
Register the services to Consul
As I mentioned earlier, Stork allows you to use Consul based on Vert.x Consul Client for the service registration. Create a new ConsulRegistration.java
file to register two services with the same name (my-rest-service) in src/main/java/org/acme/services
. Finally, add the following ConfigProperty and init() method:
@ApplicationScoped
public class ConsulRegistration {
@ConfigProperty(name = "consul.host") String host;
@ConfigProperty(name = "consul.port") int port;
@ConfigProperty(name = "hero-service-port", defaultValue = "9000") int hero;
@ConfigProperty(name = "villain-service-port", defaultValue = "9001") int villain;
public void init(@Observes StartupEvent ev, Vertx vertx) {
ConsulClient client = ConsulClient.create(vertx, new ConsulClientOptions().setHost(host).setPort(port));
client.registerServiceAndAwait(
new ServiceOptions().setPort(hero).setAddress("localhost").setName("my-rest-service").setId("hero"));
client.registerServiceAndAwait(
new ServiceOptions().setPort(villain).setAddress("localhost").setName("my-rest-service").setId("villain"));
}
}
Delegate the reactive REST client to Stork
The hero
and villain
services are normal reactive RESTful services that can be accessed directly by exposable APIs. You need to delegate those services to Stork for service discovery, selection, and calling.
Create a new interface MyRestClient.java
file in the src/main/java
directory. Then add the following code:
@RegisterRestClient(baseUri = "stork://my-rest-service")
public interface MyRestClient {
@GET
@Produces(MediaType.TEXT_PLAIN)
String get();
}
The baseUri that starts with stork://
enables Stork to discover the services and select one based on load balancing type. Next, modify the existing resource file or create a new resource file (MyRestClientResource
) to inject the RestClient
(MyRestClient) along with the endpoint (/api) as seen below:
@Path("/api")
public class MyRestClientResource {
@RestClient MyRestClient myRestClient;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String invoke() {
return myRestClient.get();
}
}
Before you run the application, configure Stork to use the Consul server in the application.properties as shown below:
consul.host=localhost
consul.port=8500
stork.my-rest-service.service-discovery=consul
stork.my-rest-service.service-discovery.consul-host=localhost
stork.my-rest-service.service-discovery.consul-port=8500
stork.my-rest-service.load-balancer=round-robin
Test your application
You have several ways to run a local Consul server. For this example, run the server using a container. This approach is probably simpler than installating or referring to an external server. Find more information here.
$ docker run --rm --name consul -p 8500:8500 -p 8501:8501 consul:1.7 agent -dev -ui -client=0.0.0.0 -bind=0.0.0.0 --https-port=8501
Run your Quarkus application using Dev mode:
$ cd quarkus-stork-example
$ quarkus dev
The output looks like this:
...
INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, jaxrs-client-reactive, rest-client-reactive, resteasy-reactive, smallrye-context-propagation, vertx]
--
Tests paused
Press [r] to resume testing, [o] Toggle test output, [:] for the terminal, [h] for more options>
Access the RESTful API (/api) to retrieve available services based on the round-robin load balancing mechanism. Execute the following curl command-line in your local terminal:
& while true; do curl localhost:8080/api ; echo ''; sleep 1; done
The output should look like this:
Super Villain!
Super Hero!
Super Villain!
Super Hero!
Super Villain!
...
Wrap up
You learned how Quarkus enables developers to integrate client-based load balancing programming using Stork and Consul for reactive Java applications. Developers can also have better developer experiences using live coding while they keep developing the reactive programming in Quarkus. For more information about Quarkus, visit the Quarkus guides and practices.
Comments are closed.