编辑此页面

SmallRye Stork 入门

分布式系统的本质在于服务之间的交互。在现代架构中,您通常会有多个服务实例来分摊负载或通过冗余来提高弹性。但是,您如何选择服务的最佳实例?这就是 SmallRye Stork 的用武之地。Stork 将选择最合适的实例。它提供

  • 可扩展的服务发现机制

  • 内置 Consul 和 Kubernetes 支持

  • 可定制的客户端负载均衡策略

此技术被认为是预览版。

预览版中,不保证向后兼容性和生态系统中的存在。具体改进可能需要更改配置或 API,并且正在制定成为稳定版的计划。欢迎通过我们的邮件列表或我们的GitHub 问题跟踪器中的问题提供反馈。

有关可能的完整状态列表,请查看我们的常见问题解答条目

先决条件

要完成本指南,您需要

  • 大约 15 分钟

  • 一个 IDE

  • 已安装 JDK 17+ 并正确配置了 JAVA_HOME

  • Apache Maven 3.9.9

  • 一个正常工作的容器运行时(Docker 或 Podman

  • 如果您想使用它,可以选择 Quarkus CLI

  • 如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置

架构

在本指南中,我们将构建一个由以下部分组成的应用:

  • 一个在端口 9000 上公开的简单蓝色服务

  • 一个在端口 9001 上公开的简单红色服务

  • 一个调用蓝色或红色服务的 REST 客户端(选择委托给 Stork)

  • 一个使用 REST 客户端并调用服务的 REST 端点

  • 蓝色和红色服务已在 Consul 中注册。

Architecture of the application

为了简单起见,所有内容(Consul 除外)都将在同一个 Quarkus 应用程序中运行。当然,在现实世界中,每个组件都会在其自己的进程中运行。

解决方案

我们建议您按照下一节中的说明逐步创建应用程序。但是,您可以直接转到完整的示例。

克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或下载 archive

该解决方案位于 stork-quickstart 目录中。

发现和选择

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

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

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

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

Discovery and Selection of services

引导项目

创建一个 Quarkus 项目,使用您喜欢的方法导入 quarkus-rest-client、quarkus-rest 和 quarkus-smallrye-stork 扩展

CLI
quarkus create app org.acme:stork-quickstart \
    --extension='quarkus-rest-client,quarkus-rest,quarkus-smallrye-stork' \
    --no-code
cd stork-quickstart

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=stork-quickstart \
    -Dextensions='quarkus-rest-client,quarkus-rest,quarkus-smallrye-stork' \
    -DnoCode
cd stork-quickstart

要创建 Gradle 项目,请添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

对于 Windows 用户

  • 如果使用 cmd,(不要使用反斜杠 \ 并将所有内容放在同一行上)

  • 如果使用 Powershell,请将 -D 参数包装在双引号中,例如 "-DprojectArtifactId=stork-quickstart"

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

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 中注册我们的服务。

蓝色和红色服务

让我们从最开始的部分开始:我们将发现、选择和调用的服务。

创建 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!"。

在 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 来指示它是同一个服务的两个实例。

REST 客户端接口和前端 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 过滤器

REST 客户端中配置的 baseUri 将由 StorkClientRequestFilter 类处理,这是一个 Jakarta REST 过滤器。如果您需要处理与消息关联的元数据: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());
    }
}

过滤器的执行顺序由 优先级 定义。请注意,CustomLoggingFilter 使用默认值,因此用户级别的优先级和 StorkClientRequestFilter 使用安全身份验证过滤器优先级。这意味着 StorkClientRequestFilter 将在我们的 CustomLoggingFilter 之前执行。使用 @Priority 注释来更改此行为。

Stork 配置

系统几乎完成了。我们只需要配置 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-hostquarkus.stork.my-service.service-discovery.consul-port 配置对 Consul 的访问。最后,quarkus.stork.my-service.load-balancer.type 配置服务选择。在我们的例子中,我们使用 round-robin

运行应用程序

我们完成了!那么,让我们看看它是否有效。

首先,启动 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 https://:8080/api
...
> curl https://:8080/api
...
> curl https://: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

更进一步

本指南介绍了如何使用 SmallRye Stork 发现和选择您的服务。您可以在以下位置找到有关 Stork 的更多信息:

相关内容