Getting Started with SmallRye Stork
分布式系统的本质在于服务之间的交互。在现代架构中,您通常有多个服务实例来共享负载或通过冗余提高弹性。但是,您如何选择最佳的服务实例?这就是 SmallRye Stork 派上用场之处。Stork 将选择最合适的服务实例。它提供:
-
Extensible service discovery mechanisms
-
内置对 Consul 和 Kubernetes 的支持
-
Customizable client load-balancing strategies
该技术被认为是 {extension-status}。 有关可能状态的完整列表,请查看我们的 FAQ entry. |
Prerequisites
如要完成本指南,您需要:
-
Roughly 15 minutes
-
An IDE
-
安装了 JDK 17+,已正确配置
JAVA_HOME
-
Apache Maven ${proposed-maven-version}
-
如果你想使用 Quarkus CLI, 则可以选择使用
-
如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately
Architecture
在本指南中,我们将构建一个由以下部分组成的应用程序:
-
端口 9000 上公开的一个简单的蓝色服务
-
端口 9001 上公开的一个简单的红色服务
-
一个调用蓝色服务或红色服务的 REST 客户端(选择委托给 Stork)
-
一个使用 REST 客户端并调用服务的 REST 端点
-
蓝色服务和红色服务已在 Consul 中注册。
为了简洁起见,一切都(除了 Consul)将在同一个 Quarkus 应用程序中运行。当然,每个组件将在现实世界中的自己的进程中运行。
Solution
我们建议您按照下一部分中的说明进行操作,并逐步创建应用程序。但是,您可以直接转到已完成的示例。
克隆 Git 存储库: git clone $${quickstarts-base-url}.git
,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。
解决方案位于 stork-quickstart
directory 中。
Discovery and selection
在继续之前,我们需要讨论发现与选择。
-
服务发现是指查找服务实例的过程。它会生成一个服务实例列表,该列表可能为空(如果没有服务与请求匹配)或包含多个服务实例。
-
服务选择(也称为负载均衡)从发现过程返回的列表中选择最佳实例。结果是一个服务实例或当找不到合适的实例时的异常。
Stork 同时处理发现和选择。但是,它不处理与服务之间的通信,而仅提供一个服务实例。Quarkus 中的各种集成从该服务实例中提取服务的位置。
Bootstrapping the project
使用偏好的方法创建一个导入 quarkus-rest-client 和 quarkus-rest 扩展的 Quarkus 项目:
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
--no-code
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 --gradle
或 --gradle-kotlin-dsl
选项。
有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
-DprojectGroupId={create-app-group-id} \
-DprojectArtifactId={create-app-artifact-id} \
-DnoCode
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 -DbuildTool=gradle
或 -DbuildTool=gradle-kotlin-dsl
选项。
适用于 Windows 用户:
-
如果使用 cmd,(不要使用反斜杠
\
,并将所有内容放在同一行上) -
如果使用 Powershell,将
-D
参数用双引号引起来,例如"-DprojectArtifactId={create-app-artifact-id}"
在生成的项目中,还添加以下依赖项:
<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
为 Consul 提供了服务发现的实现。smallrye-mutiny-vertx-consul-client
是一个 Consul 客户端,我们将使用它在 Consul 中注册我们的服务。
The Blue and Red services
我们从头开始:我们将要发现、选择和调用的服务。
使用以下内容创建 src/main/java/org/acme/services/BlueService.java
:
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);
}
}
它创建一个新的 HTTP 服务器(使用 Vert.x)并在应用程序启动时实现我们简单的服务。对于每个 HTTP 请求,它发送一个以“Hello from Blue!”为正文的响应。
遵循相同的逻辑,使用以下内容创建 src/main/java/org/acme/services/RedService.java
:
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);
}
}
这一次,它写“Hello from Red!”。
Service registration in Consul
现在我们已经实现了我们的服务,我们需要将它们注册到 Consul 中。
Stork 不仅限于 Consul,还可以与其他服务发现机制集成。 |
使用以下内容创建 src/main/java/org/acme/services/Registration.java
文件:
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"));
}
}
当应用程序启动时,它使用 Vert.x Consul Client 连接到 Consul 并注册我们的两个实例。两个注册使用相同的名称 (my-service
),但不同的 ID 来表示它是同一 service 的两个实例。
The REST Client interface and the front end API
到目前为止,我们还没有使用 Stork;我们只是搭建了我们将要发现、选择和调用的服务。
我们将使用 REST Client 调用服务。使用以下内容创建 src/main/java/org/acme/MyService.java
文件:
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();
}
它是一个包含单个方法的简单 REST 客户端接口。但是,请注意 baseUri
属性。它以 stork://
开头。它指示 REST 客户端将服务实例的发现和选择委托给 Stork。注意 URL 中的 my-service
部分。这是我们将在应用程序配置中使用的服务名称。
它不会改变 REST 客户端的使用方式。使用以下内容创建 src/main/java/org/acme/FrontendApi.java
文件:
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();
}
}
它照常注入并使用 REST 客户端。
Stork Filter
REST 客户端中配置的 baseUri
将由 StorkClientRequestFilter
类处理,这是一个 Jakarta REST filter 。如果您需要处理与消息关联的元数据:HTTP 标头、查询参数、媒体类型和其他元数据,您可以实现另一个过滤器来配置您需要的内容。让我们实现一个自定义过滤器来为我们的服务添加日志记录功能。我们创建 CustomLoggingFilter
并用 @Provider 注解对其进行注释:
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());
}
}
执行过滤器的顺序由 Priorities 定义。请注意,CustomLoggingFilter
正在使用默认值,因此用户级优先级和 StorkClientRequestFilter
使用安全身份验证过滤器优先级。这意味着我们的 CustomLoggingFilter
将在 StorkClientRequestFilter
之前执行。使用 @Priority
注解来更改此行为。
Stork configuration
系统已接近完成。我们只需要配置 Stork 和 Registration
bean。
在 src/main/resources/application.properties
中,添加:
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
前两行提供 Registration
bean 使用的 Consul 位置。
其他属性与 Stork 相关。stork.my-service.service-discovery`表示用于查找 `my-service`服务的哪种类型服务查找。在本示例中,是 `consul
。quarkus.stork.my-service.service-discovery.consul-host`和 `quarkus.stork.my-service.service-discovery.consul-port`配置对 Consul 的访问权限。最后,`quarkus.stork.my-service.load-balancer.type`配置服务选择。在本示例中,使用的是 `round-robin
。
Running the application
大功告成!让我们看看是否有效。
首先,启动 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
如果您以其他方式启动 Consul,不要忘记编辑应用程序配置。
然后,打包应用程序:
quarkus build
./mvnw install
./gradlew build
运行应用程序:
> java -jar target/quarkus-app/quarkus-run.jar
在另一个终端中,运行:
> curl http://localhost:8080/api
...
> curl http://localhost:8080/api
...
> curl http://localhost:8080/api
...
响应在 `Hello from Red!`和 `Hello from Blue!`之间交替。
您可以将此应用程序编译为本机可执行文件:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
并使用以下命令启动它:
> ./target/stork-getting-started-1.0.0-SNAPSHOT-runner