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.
This technology is considered preview. In preview, backward compatibility and presence in the ecosystem is not guaranteed. Specific improvements might require changing configuration or APIs, and plans to become stable are under way. Feedback is welcome on our mailing list or as issues in our GitHub issue tracker. 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:
<dependency>
<groupId>io.smallrye.stork</groupId>
<artifactId>stork-service-discovery-kubernetes</artifactId>
</dependency>
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.
<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>
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
|
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<AcmeConfiguration> {
(4)
@Override
public ServiceDiscovery createServiceDiscovery(AcmeConfiguration 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. This configuration class is generated automatically by the Configuration Generator. Its name is created by appending Configuration to the service discovery type, such as AcmeConfiguration . |
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(AcmeConfiguration 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.
<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>
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 |
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 ofLoadBalancer
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.