diff --git a/docs/src/main/asciidoc/images/stork-getting-started-architecture.png b/docs/src/main/asciidoc/images/stork-getting-started-architecture.png new file mode 100644 index 0000000000000..a73610ccf7108 Binary files /dev/null and b/docs/src/main/asciidoc/images/stork-getting-started-architecture.png differ diff --git a/docs/src/main/asciidoc/images/stork-process.png b/docs/src/main/asciidoc/images/stork-process.png new file mode 100644 index 0000000000000..c9c80abb3bb8c Binary files /dev/null and b/docs/src/main/asciidoc/images/stork-process.png differ diff --git a/docs/src/main/asciidoc/stork-reference.adoc b/docs/src/main/asciidoc/stork-reference.adoc new file mode 100644 index 0000000000000..b26f2c4f9926c --- /dev/null +++ b/docs/src/main/asciidoc/stork-reference.adoc @@ -0,0 +1,291 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Stork Reference Guide + +include::./attributes.adoc[] + +This guide is the companion from the xref:stork.adoc[Stork Getting Started Guide]. +It explains the configuration and usage of SmallRye Stork integration in Quarkus. + +== 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 https://smallrye.io/smallrye-stork[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: + +[source, xml] +---- + + io.smallrye.stork + smallrye-stork-service-discovery-kubernetes + +---- + +For each service expected to be exposed as a Kubernetes Service, configure the lookup: + +[source, properties] +---- +stork.my-service.service-discovery=kubernetes +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. + +== Implementing a custom service discovery + +Stork is extensible, and you can implement your own service discovery mechanism. +Stork uses the SPI mechanism for loading implementations matching the Service Discovery Provider interface. + +=== Dependency +To implement your Service Discovery Provider, make sure your project depends on: + +[source, xml] +---- + + io.smallrye.stork + smallrye-stork-api + +---- + +=== Implementing a service discovery provider + +Stork uses the SPI mechanism for loading implementations matching the Service Discovery Provider interface during its initialization. + +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: + +[source, properties] +---- +stork.my-service.service-discovery=acme +---- + +The first step consists of implementing the `io.smallrye.stork.spi.ServiceDiscoveryProvider` interface: + +[source, java] +---- +package examples; + +import io.smallrye.stork.ServiceDiscovery; +import io.smallrye.stork.config.ServiceConfig; +import io.smallrye.stork.config.ServiceDiscoveryConfig; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; + +public class AcmeServiceDiscoveryProvider implements ServiceDiscoveryProvider { + @Override + public String type() { + return "acme"; + } + + @Override + public ServiceDiscovery createServiceDiscovery(ServiceDiscoveryConfig config, + String serviceName, + ServiceConfig serviceConfig) { + return new AcmeServiceDiscovery(config.parameters()); + } +} +---- + +This implementation is straightforward. +The type method returns the service discovery provider identifier. +The `createServiceDiscovery` method is the factory method. +It receives the instance configuration (a map constructed from all `stork.my-service.service-discovery.attr=value` properties) + +Then, obviously, we need to implement the ServiceDiscovery interface: + +[source, java] +---- +package examples; + +import io.smallrye.mutiny.Uni; +import io.smallrye.stork.DefaultServiceInstance; +import io.smallrye.stork.ServiceDiscovery; +import io.smallrye.stork.ServiceInstance; +import io.smallrye.stork.spi.ServiceInstanceIds; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class AcmeServiceDiscovery implements ServiceDiscovery { + + private final String host; + private final int port; + + public AcmeServiceDiscovery(Map configuration) { + this.host = configuration.get("host"); + this.port = Integer.parseInt(configuration.get("port")); + } + + @Override + public Uni> 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. + +The final step is to declare our `ServiceDiscoveryProvider` in the `META-INF/services/io.smallrye.stork.spi.ServiceDiscoveryProvider` file: + +[source, text] +---- +examples.AcmeServiceDiscoveryProvider +---- + +=== 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: + +[source, properties] +---- +stork.my-service.service-discovery=acme +stork.my-service.service-discovery.host=localhost +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. +Stork uses the SPI mechanism for loading implementations matching the Load Balancer Provider interface. + +=== Dependency +To implement your Load Balancer Provider, make sure your project depends on: + +[source, xml] +---- + + io.smallrye.stork + smallrye-stork-api + +---- + +=== Implementing a load balancer provider + +Stork uses the SPI mechanism for loading implementations matching the Load Balancer Provider interface during its initialization. + +The custom provider is a factory that creates an `io.smallrye.stork.LoadBalancer` instance for each configured service using this load balancer provider. +A type identifies each provider. +You will use that type in the configuration to reference the load-balancer provider you want for each service: + +[source, properties] +---- +stork.my-service.load-balancer=acme +---- + +The first step consists of implementing the `io.smallrye.stork.spi.LoadBalancerProvider` interface: + +[source, java] +---- +package examples; + +import io.smallrye.stork.LoadBalancer; +import io.smallrye.stork.ServiceDiscovery; +import io.smallrye.stork.config.LoadBalancerConfig; +import io.smallrye.stork.config.ServiceDiscoveryConfig; +import io.smallrye.stork.spi.LoadBalancerProvider; +import io.smallrye.stork.spi.ServiceDiscoveryProvider; + +public class AcmeLoadBalancerProvider implements LoadBalance +rProvider { + @Override + public String type() { + return "acme"; + } + + @Override + public LoadBalancer createLoadBalancer(LoadBalancerConfig config, ServiceDiscovery serviceDiscovery) { + return new AcmeLoadBalancer(config); + } +} +---- + +This implementation is straightforward. +The type method returns the load balancer provider identifier. +The `createLoadBalancer` method is the factory method. +It receives the instance configuration (a map constructed from all `stork.my-service.load-balancer.attr=value` properties) + +Then, obviously, we need to implement the `LoadBalancer` interface: + +[source, java] +---- +package examples; + +import io.smallrye.stork.LoadBalancer; +import io.smallrye.stork.ServiceInstance; +import io.smallrye.stork.config.LoadBalancerConfig; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Random; + +public class AcmeLoadBalancer implements LoadBalancer { + + private final Random random; + + public AcmeLoadBalancer(LoadBalancerConfig config) { + random = new Random(); + } + + @Override + public ServiceInstance selectServiceInstance(Collection serviceInstances) { + 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. + +The final step is to declare our `LoadBalancerProvider` in the `META-INF/services/io.smallrye.stork.spi.LoadBalancerProvider` file: + +[source, text] +---- +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: + +[source, properties] +---- +stork.my-service.service-discovery=... +stork.my-service.load-balancer=acme +---- + +Then, Stork will use your implementation to select the `my-service` service instance. + + + + diff --git a/docs/src/main/asciidoc/stork.adoc b/docs/src/main/asciidoc/stork.adoc new file mode 100644 index 0000000000000..efad92d386073 --- /dev/null +++ b/docs/src/main/asciidoc/stork.adoc @@ -0,0 +1,378 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc +//// += Getting Started with SmallRye Stork + +// Temporary until stork in in the BOM +:stork-version: 1.0.0.Beta1 +include::./attributes.adoc[] + +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 https://smallrye.io/smallrye-stork[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 + +include::./status-include.adoc[] + +== Prerequisites + +To complete this guide, you need: + +* less than 15 minutes +* an IDE +* JDK 11+ installed with `JAVA_HOME` configured appropriately +* Apache Maven {maven-version} +* Docker +* GraalVM installed if you want to build a native executable. + +== 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 https://www.consul.io/[Consul]. + +image::stork-getting-started-architecture.png[Architecture of the application,width=50%, align=center] + +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. + +Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. + +The solution is located in the `stork-quickstart` {quickstarts-tree-url}/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 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. + +image::stork-process.png[Discovery and Selection of services,width=50%, align=center] + +== Bootstrapping the project + +Create a Quarkus project using your favorite approach, and select the following extensions: + +* quarkus-rest-client-reactive +* quarkus-resteasy-reactive + +In the generated project, also add the following dependencies: + +[source, xml, subs=attributes+] +---- + + io.smallrye.stork + smallrye-stork-service-discovery-consul + {stork-version} + + + io.smallrye.stork + smallrye-stork-load-balancer-round-robin + {stork-version} + + + io.smallrye.reactive + smallrye-mutiny-vertx-consul-client + +---- + +`smallrye-stork-service-discovery-consul` provides an implementation of service discovery for Consul. +`smallrye-stork-load-balancer-round-robin` provides a round-robin service selection. +Finally, `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. + +Create the `src/main/java/org/acme/services/BlueService.java` with the following content: + +[source, java] +---- +package org.acme.services; + +import io.quarkus.runtime.StartupEvent; +import io.vertx.mutiny.core.Vertx; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import javax.enterprise.context.ApplicationScoped; +import javax.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); + } +} +---- + +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. + +Following the same logic, create the `src/main/java/org/acme/services/RedService.java` with the following content: + +[source, java] +---- + +package org.acme.services; + +import io.quarkus.runtime.StartupEvent; +import io.vertx.mutiny.core.Vertx; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +import javax.enterprise.context.ApplicationScoped; +import javax.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); + } + +} +---- + +This time, it writes "Hello from Red!". + +== Service registration in Consul + +Now that we have implemented our services, we need to register them into Consul. + +NOTE: Stork is not limited to Consul and integrates with other service discovery mechanisms. + +Create the `src/main/java/org/acme/services/Registration.java` file with the following content: + +[source, 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 javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; + +@ApplicationScoped +public class Registration { + + @ConfigProperty(name = "consul.host") String host; + @ConfigProperty(name = "consul.port") int port; + + @ConfigProperty(name = "blue-service-port", defaultValue = "9000") int red; + @ConfigProperty(name = "red-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(blue).setAddress("localhost").setName("my-service").setId("blue")); + client.registerServiceAndAwait( + new ServiceOptions().setPort(red).setAddress("localhost").setName("my-service").setId("red")); + + } +} +---- + +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 + +So far, we didn't use Stork; we just scaffolded the services we will be discovering, selecting, and calling. + +We will call the services using the Reactive REST Client. +Create the `src/main/java/org/acme/MyService.java` file with the following content: + +[source, java] +---- +package org.acme; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import javax.ws.rs.GET; +import javax.ws.rs.Produces; +import javax.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(); +} +---- + +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. + +It does not change how the REST client is used. +Create the `src/main/java/org/acme/FrontendApi.java` file with the following content: + +[source, java] +---- +package org.acme; + +import org.eclipse.microprofile.rest.client.inject.RestClient; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.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(); + } + +} +---- + +It injects and uses the REST client as usual. + +== Stork configuration + +The system is almost complete. We only need to configure Stork and the `Registration` bean. + +In the `src/main/resources/application.properties`, add: + +[source, properties] +---- +consul.host=localhost +consul.port=8500 + +stork.my-service.service-discovery=consul +stork.my-service.service-discovery.consul-host=localhost +stork.my-service.service-discovery.consul-port=8500 +stork.my-service.load-balancer=round-robin +---- + +The first two lines provide the Consul location used by the `Registration` bean. + +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`. +`stork.my-service.service-discovery.consul-host` and `stork.my-service.service-discovery.consul-port` configures the access to Consul. +Finally, `stork.my-service.load-balancer` 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. + +First, start Consul: + +[source, shell script] +---- +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 +---- + +If you start Consul differently, do not forget to edit the application configuration. + +Then, package and run the application: + +[source, shell script] +---- +> ./mvnw package +> java -jar target/quarkus-app/quarkus-run.jar +---- + +In another terminal, run: + +[source, shell script] +---- +> curl http://localhost:8080/api +... +> curl http://localhost:8080/api +... +> curl http://localhost:8080/api +... +---- + +The responses alternate between `Hello from Red!` and `Hello from Blue!`. + +You can compile this application into a native executable and start it with: + +[source, shell script] +---- +> ./mvnw package -Dnative +> ./target/stork-getting-started-1.0.0-SNAPSHOT-runner +---- + +== Going further + +This guide has shown how to use SmallRye Stork to discover and select your services. +You can find more about Stork in: + +- the xref:stork-reference.adoc[Stork reference guide], +- the https://smallrye.io/smallrye-stork[SmallRye Stork website]. \ No newline at end of file diff --git a/extensions/smallrye-stork/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/smallrye-stork/runtime/src/main/resources/META-INF/quarkus-extension.yaml index de5f08f9cdc58..aab5a2bf75079 100644 --- a/extensions/smallrye-stork/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/smallrye-stork/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -5,7 +5,8 @@ metadata: keywords: - "Load Balancing" - "Service Discovery" + guide: "https://quarkus.io/guides/stork" categories: - "web" - status: "experimental" + status: "preview" unlisted: "true" \ No newline at end of file