The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

Stork Reference Guide

This guide is the companion from the Stork Getting Started Guide. It explains the configuration and usage of SmallRye Stork integration in Quarkus.

这项技术被认为是preview。

preview(预览) 中,不保证向后兼容和在生态系统中的存在。具体的改进可能需要改变配置或API,并且正在计划变得 稳定 。欢迎在我们的 邮件列表 中提供反馈,或在我们的 GitHub问题列表 中提出问题。

For a full list of possible statuses, check our FAQ entry.

Supported clients

The current integration of Stork supports:

  • the Reactive REST Client

  • the gRPC clients

Warning: The gRPC client integration does not support statistic-based load balancers.

Available service discovery and selection

Check the SmallRye Stork website to find more about the provided service discovery and selection.

Using Stork in Kubernetes

Stork provides a service discovery support for Kubernetes, which goes beyond what Kubernetes provides by default. It looks for all the pods backing up a Kubernetes service, but instead of applying a round-robin (as Kubernetes would do), it gives you the option to select the pod using a Stork load-balancer.

To use this feature, add the following dependency to your project:

pom.xml
<dependency>
    <groupId>io.smallrye.stork</groupId>
    <artifactId>stork-service-discovery-kubernetes</artifactId>
</dependency>
build.gradle
implementation("io.smallrye.stork:stork-service-discovery-kubernetes")

For each service expected to be exposed as a Kubernetes Service, configure the lookup:

quarkus.stork.my-service.service-discovery.type=kubernetes
quarkus.stork.my-service.service-discovery.k8s-namespace=my-namespace

Stork looks for the Kubernetes Service with the given name (my-service in the previous example) in the specified namespace. Instead of using the Kubernetes Service IP directly and let Kubernetes handle the selection and balancing, Stork inspects the service and retrieves the list of pods providing the service. Then, it can select the instance.

For a full example of using Stork with Kubernetes, please read the Using Stork with Kubernetes guide.

Implementing a custom service discovery

Stork is extensible, and you can implement your own service discovery mechanism.

Dependency

To implement your Service Discovery Provider, make sure your project depends on Core and Configuration Generator. The former brings classes necessary to implement custom discovery, the latter contains an annotation processor that generates classes needed by Stork.

pom.xml
<dependency>
    <groupId>io.smallrye.stork</groupId>
    <artifactId>stork-core</artifactId>
</dependency>
<dependency>
    <groupId>io.smallrye.stork</groupId>
    <artifactId>stork-configuration-generator</artifactId>
    <!-- provided scope is sufficient for the annotation processor -->
    <scope>provided</scope>
</dependency>
build.gradle
implementation("io.smallrye.stork:stork-core")
compileOnly("io.smallrye.stork:stork-configuration-generator")

If the provider is located in an extension, the configuration generator should be declared in the annotationProcessorPaths section of the runtime module using the default scope:

<annotationProcessorPaths>
  ...
  <path>
    <groupId>io.smallrye.stork</groupId>
    <artifactId>stork-configuration-generator</artifactId>
  </path>
</annotationProcessorPaths>

Implementing a service discovery provider

The custom provider is a factory that creates an io.smallrye.stork.ServiceDiscovery instance for each configured service using this service discovery provider. A type, for example, acme identifies each provider. This type is used in the configuration to reference the provider:

quarkus.stork.my-service.service-discovery.type=acme

The first step consists of implementing the io.smallrye.stork.spi.ServiceDiscoveryProvider interface:

package examples;

import io.smallrye.stork.api.ServiceDiscovery;
import io.smallrye.stork.api.config.ServiceConfig;
import io.smallrye.stork.api.config.ServiceDiscoveryAttribute;
import io.smallrye.stork.api.config.ServiceDiscoveryType;
import io.smallrye.stork.spi.StorkInfrastructure;
import io.smallrye.stork.spi.ServiceDiscoveryProvider;

@ServiceDiscoveryType("acme") (1)
@ServiceDiscoveryAttribute(name = "host",
        description = "Host name of the service discovery server.", required = true) (2)
@ServiceDiscoveryAttribute(name = "port",
        description = "Port of the service discovery server.", required = false)
public class AcmeServiceDiscoveryProvider (3)
        implements ServiceDiscoveryProvider<AcmeServiceDiscoveryProviderConfiguration> {

    (4)
    @Override
    public ServiceDiscovery createServiceDiscovery(AcmeServiceDiscoveryProviderConfiguration config,
                                                   String serviceName,
                                                   ServiceConfig serviceConfig,
                                                   StorkInfrastructure storkInfrastructure) {
        return new AcmeServiceDiscovery(config);
    }
}

This implementation is straightforward.

1 @ServiceDiscoveryType annotation defines the type of the service discovery provider. For each ServiceDiscoveryProvider annotated with this annotation, a configuration class will be generated. The name of the configuration class is constructed by appending Configuration to the name of the provider.
2 Use @ServiceDiscoveryAttribute to define configuration properties for services configured with this service discovery provider. Configuration properties are gathered from all properties of a form: quarkus.stork.my-service.service-discovery.attr=value.
3 The provider needs to implement ServiceDiscoveryType typed by the configuration class.
4 createServiceDiscovery method is the factory method. It receives the configuration and access to the name of the service and available infrastructure.

Then, we need to implement the ServiceDiscovery interface:

package examples;

import java.util.Collections;
import java.util.List;

import io.smallrye.mutiny.Uni;
import io.smallrye.stork.api.ServiceDiscovery;
import io.smallrye.stork.api.ServiceInstance;
import io.smallrye.stork.impl.DefaultServiceInstance;
import io.smallrye.stork.utils.ServiceInstanceIds;

public class AcmeServiceDiscovery implements ServiceDiscovery {

    private final String host;
    private final int port;

    public AcmeServiceDiscovery(AcmeServiceDiscoveryProviderConfiguration configuration) {
        this.host = configuration.getHost();
        this.port = Integer.parseInt(configuration.getPort());
    }

    @Override
    public Uni<List<ServiceInstance>> getServiceInstances() {
        // Proceed to the lookup...
        // Here, we just return a DefaultServiceInstance with the configured host and port
        // The last parameter specifies whether the communication with the instance should happen over a secure connection
        DefaultServiceInstance instance =
                new DefaultServiceInstance(ServiceInstanceIds.next(), host, port, false);
        return Uni.createFrom().item(() -> Collections.singletonList(instance));
    }
}

Again, this implementation is simplistic. Typically, instead of creating a service instance with values from the configuration, you would connect to a service discovery backend, look for the service and build the list of service instances accordingly. That’s why the method returns a Uni. Most of the time, the lookup is a remote operation.

Using your service discovery

In the project using it, don’t forget to add the dependency on the module providing your implementation. Then, in the configuration, just add:

quarkus.stork.my-service.service-discovery.type=acme
quarkus.stork.my-service.service-discovery.host=localhost
quarkus.stork.my-service.service-discovery.port=1234

Then, Stork will use your implementation to locate the my-service service.

Implementing a custom service selection / load-balancer

Stork is extensible, and you can implement your own service selection (load-balancer) mechanism.

Dependency

To implement your Load Balancer Provider, make sure your project depends on Core and Configuration Generator. The former brings classes necessary to implement custom load balancer, the latter contains an annotation processor that generates classes needed by Stork.

pom.xml
<dependency>
    <groupId>io.smallrye.stork</groupId>
    <artifactId>stork-core</artifactId>
</dependency>
<dependency>
    <groupId>io.smallrye.stork</groupId>
    <artifactId>stork-configuration-generator</artifactId>
    <!-- provided scope is sufficient for the annotation processor -->
    <scope>provided</scope>
</dependency>
build.gradle
implementation("io.smallrye.stork:stork-core")
compileOnly("io.smallrye.stork:stork-configuration-generator")

Similar to custom discovery providers, if the provider is located in an extension, the configuration generator should be declared in the annotationProcessorPaths section of the runtime module using the default scope.

Implementing a load balancer provider

Load balancer implementation consists of three elements:

  • LoadBalancer which is responsible for selecting service instances for a single Stork service

  • LoadBalancerProvider which creates instances of LoadBalancer for a given load balancer type

  • LoadBalancerProviderConfiguration which is a configuration for the load balancer

A type, for example, acme, identifies each provider. This type is used in the configuration to reference the provider:

quarkus.stork.my-service.load-balancer.type=acme

Similarly to ServiceDiscoveryProvider, a `LoadBalancerProvider implementation needs to be annotated with @LoadBalancerType that defines the type. Any configuration properties that the provider expects should be defined with @LoadBalancerAttribute annotations placed on the provider.

package examples;

import io.smallrye.stork.api.LoadBalancer;
import io.smallrye.stork.api.ServiceDiscovery;
import io.smallrye.stork.api.config.LoadBalancerAttribute;
import io.smallrye.stork.api.config.LoadBalancerType;
import io.smallrye.stork.spi.LoadBalancerProvider;

@LoadBalancerType("acme")
@LoadBalancerAttribute(name = "my-attribute",
        description = "Attribute that alters the behavior of the LoadBalancer")
public class AcmeLoadBalancerProvider implements
        LoadBalancerProvider<AcmeLoadBalancerProviderConfiguration> {

    @Override
    public LoadBalancer createLoadBalancer(AcmeLoadBalancerProviderConfiguration config,
                                           ServiceDiscovery serviceDiscovery) {
        return new AcmeLoadBalancer(config);
    }
}

Note, that similarly to the ServiceDiscoveryProvider, the LoadBalancerProvider interface takes a configuration class as a parameter. This configuration class is generated automatically by the Configuration Generator. Its name is created by appending Configuration to the name of the provider class.

The next step is to implement the LoadBalancer interface:

package examples;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Random;

import io.smallrye.stork.api.LoadBalancer;
import io.smallrye.stork.api.NoServiceInstanceFoundException;
import io.smallrye.stork.api.ServiceInstance;

public class AcmeLoadBalancer implements LoadBalancer {

    private final Random random;

    public AcmeLoadBalancer(AcmeLoadBalancerProviderConfiguration config) {
        random = new Random();
    }

    @Override
    public ServiceInstance selectServiceInstance(Collection<ServiceInstance> serviceInstances) {
        if (serviceInstances.isEmpty()) {
            throw new NoServiceInstanceFoundException("No services found.");
        }
        int index = random.nextInt(serviceInstances.size());
        return new ArrayList<>(serviceInstances).get(index);
    }
}

Again, this implementation is simplistic and just picks a random instance from the received list.

examples.AcmeLoadBalancerProvider

Using your load balancer

In the project using it, don’t forget to add the dependency on the module providing your implementation. Then, in the configuration, just add:

quarkus.stork.my-service.service-discovery.type=...
quarkus.stork.my-service.load-balancer.type=acme

Then, Stork will use your implementation to select the my-service service instance.