Getting Started with SmallRye Stork
The essence of distributed systems resides in the interaction between services. In modern architecture, you often have multiple instances of your service to share the load or improve the resilience by redundancy. But how do you select the best instance of your service? That’s where SmallRye Stork helps. Stork is going to choose the most appropriate instance. It offers:
-
Extensible service discovery mechanisms
-
Built-in support for Consul and Kubernetes
-
Customizable client load-balancing strategies
这项技术被认为是preview。 在 preview(预览) 中,不保证向后兼容和在生态系统中的存在。具体的改进可能需要改变配置或API,并且正在计划变得 稳定 。欢迎在我们的 邮件列表 中提供反馈,或在我们的 GitHub问题列表 中提出问题。 For a full list of possible statuses, check our FAQ entry. |
先决条件
完成这个指南,你需要:
-
大概15分钟
-
编辑器
-
JDK 17+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.9
-
A working container runtime (Docker or Podman)
-
如果你愿意的话,还可以选择使用Quarkus CLI
-
如果你想构建原生可执行程序,可以选择安装Mandrel或者GraalVM,并正确配置(或者使用Docker在容器中进行构建)
应用结构
In this guide, we will build an application composed of:
-
A simple blue service exposed on port 9000
-
A simple red service exposed on port 9001
-
A REST Client calling the blue or red service (the selection is delegated to Stork)
-
A REST endpoint using the REST client and calling the services
-
The blue and red services are registered in Consul.
For the sake of simplicity, everything (except Consul) will be running in the same Quarkus application. Of course, each component will run in its own process in the real world.
解决方案
We recommend that you follow the instructions in the next sections and create the applications step by step. However, you can go right to the completed example.
克隆 Git 仓库: git clone https://github.com/quarkusio/quarkus-quickstarts.git
,或下载一个 存档 。
The solution is located in the stork-quickstart
directory.
Discovery and selection
Before going further, we need to discuss discovery vs. selection.
-
Service discovery is the process of locating service instances. It produces a list of service instances that is potentially empty (if no service matches the request) or contains multiple service instances.
-
Service selection, also called load-balancing, chooses the best instance from the list returned by the discovery process. The result is a single service instance or an exception when no suitable instance can be found.
Stork handles both discovery and selection. However, it does not handle the communication with the service but only provides a service instance. The various integrations in Quarkus extract the location of the service from that service instance.
创建项目
Create a Quarkus project importing the quarkus-rest-client and quarkus-rest extensions using your favorite approach:
For Windows users:
-
If using cmd, (don’t use backward slash
\
and put everything on the same line) -
If using Powershell, wrap
-D
parameters in double quotes e.g."-DprojectArtifactId=stork-quickstart"
In the generated project, also add the following dependencies:
<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>
implementation("io.smallrye.stork:stork-service-discovery-consul")
implementation("io.smallrye.reactive:smallrye-mutiny-vertx-consul-client")
stork-service-discovery-consul
provides an implementation of service discovery for Consul.
smallrye-mutiny-vertx-consul-client
is a Consul client which we will use to register our services in Consul.
The Blue and Red services
Let’s start with the very beginning: the service we will discover, select and call.
Create the src/main/java/org/acme/services/BlueService.java
with the following content:
package org.acme.services;
import io.quarkus.runtime.StartupEvent;
import io.vertx.mutiny.core.Vertx;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
@ApplicationScoped
public class BlueService {
@ConfigProperty(name = "blue-service-port", defaultValue = "9000") int port;
/**
* Start an HTTP server for the blue service.
*
* Note: this method is called on a worker thread, and so it is allowed to block.
*/
public void init(@Observes StartupEvent ev, Vertx vertx) {
vertx.createHttpServer()
.requestHandler(req -> req.response().endAndForget("Hello from Blue!"))
.listenAndAwait(port);
}
}
It creates a new HTTP server (using Vert.x) and implements our simple service when the application starts. For each HTTP request, it sends a response with "Hello from Blue!" as the body.
Following the same logic, create the src/main/java/org/acme/services/RedService.java
with the following content:
package org.acme.services;
import io.quarkus.runtime.StartupEvent;
import io.vertx.mutiny.core.Vertx;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
@ApplicationScoped
public class RedService {
@ConfigProperty(name = "red-service-port", defaultValue = "9001") int port;
/**
* Start an HTTP server for the red service.
*
* Note: this method is called on a worker thread, and so it is allowed to block.
*/
public void init(@Observes StartupEvent ev, Vertx vertx) {
vertx.createHttpServer()
.requestHandler(req -> req.response().endAndForget("Hello from Red!"))
.listenAndAwait(port);
}
}
This time, it writes "Hello from Red!".
Service registration in Consul
Now that we have implemented our services, we need to register them into Consul.
Stork is not limited to Consul and integrates with other service discovery mechanisms. |
Create the src/main/java/org/acme/services/Registration.java
file with the following content:
package org.acme.services;
import io.quarkus.runtime.StartupEvent;
import io.vertx.ext.consul.ServiceOptions;
import io.vertx.mutiny.ext.consul.ConsulClient;
import io.vertx.ext.consul.ConsulClientOptions;
import io.vertx.mutiny.core.Vertx;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
@ApplicationScoped
public class Registration {
@ConfigProperty(name = "consul.host") String host;
@ConfigProperty(name = "consul.port") int port;
@ConfigProperty(name = "red-service-port", defaultValue = "9000") int red;
@ConfigProperty(name = "blue-service-port", defaultValue = "9001") int blue;
/**
* Register our two services in Consul.
*
* Note: this method is called on a worker thread, and so it is allowed to block.
*/
public void init(@Observes StartupEvent ev, Vertx vertx) {
ConsulClient client = ConsulClient.create(vertx, new ConsulClientOptions().setHost(host).setPort(port));
client.registerServiceAndAwait(
new ServiceOptions().setPort(red).setAddress("localhost").setName("my-service").setId("red"));
client.registerServiceAndAwait(
new ServiceOptions().setPort(blue).setAddress("localhost").setName("my-service").setId("blue"));
}
}
When the application starts, it connects to Consul using the Vert.x Consul Client and registers our two instances.
Both registration uses the same name (my-service
), but different ids to indicate that it’s two instances of the same service.
The REST Client interface and the front end API
So far, we didn’t use Stork; we just scaffolded the services we will be discovering, selecting, and calling.
We will call the services using the REST Client.
Create the src/main/java/org/acme/MyService.java
file with the following content:
package org.acme;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
/**
* The REST Client interface.
*
* Notice the `baseUri`. It uses `stork://` as URL scheme indicating that the called service uses Stork to locate and
* select the service instance. The `my-service` part is the service name. This is used to configure Stork discovery
* and selection in the `application.properties` file.
*/
@RegisterRestClient(baseUri = "stork://my-service")
public interface MyService {
@GET
@Produces(MediaType.TEXT_PLAIN)
String get();
}
It’s a straightforward REST client interface containing a single method. However, note the baseUri
attribute.
It starts with stork://
.
It instructs the REST client to delegate the discovery and selection of the service instances to Stork.
Notice the my-service
part in the URL.
It is the service name we will be using in the application configuration.
It does not change how the REST client is used.
Create the src/main/java/org/acme/FrontendApi.java
file with the following content:
package org.acme;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
/**
* A frontend API using our REST Client (which uses Stork to locate and select the service instance on each call).
*/
@Path("/api")
public class FrontendApi {
@RestClient MyService service;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String invoke() {
return service.get();
}
}
It injects and uses the REST client as usual.
Stork Filter
The baseUri
configured in the REST client will be processed by StorkClientRequestFilter
class, this is a Jakarta REST filter.
If you need to process the metadata associated with a message: HTTP headers, query parameters, media type, and other metadata, you can implement another filter to configure what you need.
Let’s implement a custom filter to add logging capability to our service. We create CustomLoggingFilter
and annotating it with the @Provider annotation:
package org.acme;
import io.vertx.core.http.HttpServerRequest;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestContext;
import org.jboss.resteasy.reactive.client.spi.ResteasyReactiveClientRequestFilter;
import jakarta.ws.rs.ext.Provider;
@Provider
public class CustomLoggingFilter implements ResteasyReactiveClientRequestFilter {
private static final Logger LOG = Logger.getLogger(CustomLoggingFilter.class);
@Override
public void filter(ResteasyReactiveClientRequestContext requestContext) {
LOG.infof("Resolved address by Stork: %s",requestContext.getUri().toString());
}
}
The order in which filters are executed is defined by Priorities.
Note that CustomLoggingFilter
is using a default value, so the user-level priority and the StorkClientRequestFilter
uses the security authentication filter priority. This means that StorkClientRequestFilter
will be executed before our CustomLoggingFilter
.
Use @Priority
annotation to change this behaviour.
Stork configuration
The system is almost complete. We only need to configure Stork and the Registration
bean.
In the src/main/resources/application.properties
, add:
consul.host=localhost
consul.port=8500
quarkus.stork.my-service.service-discovery.type=consul
quarkus.stork.my-service.service-discovery.consul-host=localhost
quarkus.stork.my-service.service-discovery.consul-port=8500
quarkus.stork.my-service.load-balancer.type=round-robin
The first two lines provide the Consul location used by the Registration
bean.
The other properties are related to Stork.
stork.my-service.service-discovery
indicates which type of service discovery we will be using to locate the my-service
service.
In our case, it’s consul
.
quarkus.stork.my-service.service-discovery.consul-host
and quarkus.stork.my-service.service-discovery.consul-port
configures the access to Consul.
Finally, quarkus.stork.my-service.load-balancer.type
configures the service selection.
In our case, we use a round-robin
.
运行应用程序
We’re done! So, let’s see if it works.
First, start Consul:
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
If you start Consul differently, do not forget to edit the application configuration.
Then, package the application:
quarkus build
./mvnw install
./gradlew build
And run it:
> java -jar target/quarkus-app/quarkus-run.jar
在另外一个终端中,运行:
> curl http://localhost:8080/api
...
> curl http://localhost:8080/api
...
> curl http://localhost:8080/api
...
The responses alternate between Hello from Red!
and Hello from Blue!
.
You can compile this application into a native executable:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
And start it with:
> ./target/stork-getting-started-1.0.0-SNAPSHOT-runner