Getting Started with SmallRye Stork

分布式系统的本质在于服务之间的交互。在现代架构中,您通常有多个服务实例来共享负载或通过冗余提高弹性。但是,您如何选择最佳的服务实例?这就是 SmallRye Stork 派上用场之处。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

Unresolved directive in stork.adoc - include::{includes}/extension-status.adoc[]

Prerequisites

Unresolved directive in stork.adoc - include::{includes}/prerequisites.adoc[]

Architecture

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

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.

stork getting started architecture

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

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.

Solution

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

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 {quickstarts-clone-url},或下载 {quickstarts-archive-url}[存档]。

Clone the Git repository: git clone {quickstarts-clone-url}, or download an {quickstarts-archive-url}[archive].

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

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

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.

stork process

Bootstrapping the project

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

Create a Quarkus project importing the quarkus-rest-client and quarkus-rest extensions using your favorite approach:

Unresolved directive in stork.adoc - include::{includes}/devtools/create-app.adoc[]

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

In the generated project, also add the following dependencies:

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

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.

使用以下内容创建 src/main/java/org/acme/services/BlueService.java

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);
    }
}

它创建一个新的 HTTP 服务器(使用 Vert.x)并在应用程序启动时实现我们简单的服务。对于每个 HTTP 请求,它发送一个以“Hello from Blue!”为正文的响应。

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.

遵循相同的逻辑,使用以下内容创建 src/main/java/org/acme/services/RedService.java

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);
    }

}

这一次,它写“Hello from Red!”。

This time, it writes "Hello from Red!".

Service registration in Consul

现在我们已经实现了我们的服务,我们需要将它们注册到 Consul 中。

Now that we have implemented our services, we need to register them into Consul.

Stork 不仅限于 Consul,还可以与其他服务发现机制集成。

Stork is not limited to Consul and integrates with other service discovery mechanisms.

使用以下内容创建 src/main/java/org/acme/services/Registration.java 文件:

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"));
    }
}

当应用程序启动时,它使用 Vert.x Consul Client 连接到 Consul 并注册我们的两个实例。两个注册使用相同的名称 (my-service),但不同的 ID 来表示它是同一 service 的两个实例。

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

到目前为止,我们还没有使用 Stork;我们只是搭建了我们将要发现、选择和调用的服务。

So far, we didn’t use Stork; we just scaffolded the services we will be discovering, selecting, and calling.

我们将使用 REST Client 调用服务。使用以下内容创建 src/main/java/org/acme/MyService.java 文件:

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();
}

它是一个包含单个方法的简单 REST 客户端接口。但是,请注意 baseUri 属性。它以 stork:// 开头。它指示 REST 客户端将服务实例的发现和选择委托给 Stork。注意 URL 中的 my-service 部分。这是我们将在应用程序配置中使用的服务名称。

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.

它不会改变 REST 客户端的使用方式。使用以下内容创建 src/main/java/org/acme/FrontendApi.java 文件:

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();
    }

}

它照常注入并使用 REST 客户端。

It injects and uses the REST client as usual.

Stork Filter

REST 客户端中配置的 baseUri 将由 StorkClientRequestFilter 类处理,这是一个 Jakarta REST filter 。如果您需要处理与消息关联的元数据:HTTP 标头、查询参数、媒体类型和其他元数据,您可以实现另一个过滤器来配置您需要的内容。让我们实现一个自定义过滤器来为我们的服务添加日志记录功能。我们创建 CustomLoggingFilter 并用 @Provider 注解对其进行注释:

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());
    }
}

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

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

系统已接近完成。我们只需要配置 Stork 和 Registration bean。

The system is almost complete. We only need to configure Stork and the Registration bean.

src/main/resources/application.properties 中,添加:

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

前两行提供 Registration bean 使用的 Consul 位置。

The first two lines provide the Consul location used by the Registration bean.

其他属性与 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

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.

Running the application

大功告成!让我们看看是否有效。

We’re done! So, let’s see if it works.

首先,启动 Consul:

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

如果您以其他方式启动 Consul,不要忘记编辑应用程序配置。

If you start Consul differently, do not forget to edit the application configuration.

然后,打包应用程序:

Then, package the application:

Unresolved directive in stork.adoc - include::{includes}/devtools/build.adoc[]

运行应用程序:

And run it:

> java -jar target/quarkus-app/quarkus-run.jar

在另一个终端中,运行:

In another terminal, run:

> curl http://localhost:8080/api
...
> curl http://localhost:8080/api
...
> curl http://localhost:8080/api
...

响应在 `Hello from Red!`和 `Hello from Blue!`之间交替。

The responses alternate between Hello from Red! and Hello from Blue!.

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

You can compile this application into a native executable:

Unresolved directive in stork.adoc - include::{includes}/devtools/build-native.adoc[]

并使用以下命令启动它:

And start it with:

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

Going further

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

This guide has shown how to use SmallRye Stork to discover and select your services. You can find more about Stork in: