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 中注册。

stork getting started architecture

为了简洁起见,一切都(除了 Consul)将在同一个 Quarkus 应用程序中运行。当然,每个组件将在现实世界中的自己的进程中运行。

Solution

我们建议您按照下一部分中的说明进行操作,并逐步创建应用程序。但是,您可以直接转到已完成的示例。

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

解决方案位于 stork-quickstart directory 中。

Discovery and selection

在继续之前,我们需要讨论发现与选择。

  • 服务发现是指查找服务实例的过程。它会生成一个服务实例列表,该列表可能为空(如果没有服务与请求匹配)或包含多个服务实例。

  • 服务选择(也称为负载均衡)从发现过程返回的列表中选择最佳实例。结果是一个服务实例或当找不到合适的实例时的异常。

Stork 同时处理发现和选择。但是,它不处理与服务之间的通信,而仅提供一个服务实例。Quarkus 中的各种集成从该服务实例中提取服务的位置。

stork process

Bootstrapping the project

使用偏好的方法创建一个导入 quarkus-rest-client 和 quarkus-rest 扩展的 Quarkus 项目:

CLI
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 指南。

Maven
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}"

在生成的项目中,还添加以下依赖项:

pom.xml
<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>
build.gradle
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`服务的哪种类型服务查找。在本示例中,是 `consulquarkus.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,不要忘记编辑应用程序配置。

然后,打包应用程序:

CLI
quarkus build
Maven
./mvnw install
Gradle
./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!`之间交替。

您可以将此应用程序编译为本机可执行文件:

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.native.enabled=true

并使用以下命令启动它:

> ./target/stork-getting-started-1.0.0-SNAPSHOT-runner

Going further

本指南演示了如何使用 SmallRye Stork 来查找和选择您的服务。您可以在此处找到有关 Stork 的更多信息: