Microservices

Vert.x Service Resolver

Preview

The Service Resolver library is a plugin that lets Vert.x clients call services using logical service names instead of network addresses. The service resolver is also able to perform client side load balancing with the usual strategies.

You can use a service resolver as a client to query a resolver or integrated natively with a Vert.x client.

Service resolver client

You can query a service resolver with the service resolver client.

ServiceResolverClient client = ServiceResolverClient.create(vertx, new KubeResolverOptions());

// Resolve a service endpoint
Future<Endpoint> fut = client.resolveEndpoint(ServiceAddress.of("the-service"));

fut.onSuccess(endpoint -> {

  // Print physical servers details
  List<ServerEndpoint> servers = endpoint.servers();
  for (ServerEndpoint server : servers) {
    System.out.println("Available server: " + server.address());
  }
});

You can also let the load balancer select nodes

ServiceResolverClient client = ServiceResolverClient.create(vertx, new KubeResolverOptions());

// Resolve a service endpoint
Future<Endpoint> fut = client.resolveEndpoint(ServiceAddress.of("the-service"));

fut.onSuccess(endpoint -> {

  // Print physical servers details
  List<ServerEndpoint> servers = endpoint.servers();
  for (ServerEndpoint server : servers) {
    System.out.println("Available server: " + server.address());
  }
});

Client integration

The service resolver is integrated with the Vert.x HTTP and Web clients.

Getting started with the Vert.x HTTP Client

Given a resolver, you can configure a Vert.x HTTP Client to use it thanks to an HttpClientBuilder.

HttpClient client = vertx.httpClientBuilder()
  .withAddressResolver(resolver)
  .build();

A service is addressed with a ServiceAddress instead of a SocketAddress.

ServiceAddress serviceAddress = ServiceAddress.of("the-service");

Future<HttpClientRequest> requestFuture = client.request(new RequestOptions()
  .setMethod(HttpMethod.GET)
  .setURI("/")
  .setServer(serviceAddress));

Future<Buffer> resultFuture = requestFuture.compose(request -> request
  .send()
  .compose(response -> {
    if (response.statusCode() == 200) {
      return response.body();
    } else {
      return Future.failedFuture("Invalid status response:" + response.statusCode());
    }
  }));

Getting started with the Vert.x Web Client

Given a resolver, you can configure a Vert.x Web Client to use it thanks to an HttpClientBuilder.

HttpClient httpClient = vertx.httpClientBuilder()
  .withAddressResolver(resolver)
  .build();
WebClient webClient = WebClient.wrap(httpClient);

A service is addressed with a ServiceAddress.

ServiceAddress serviceAddress = ServiceAddress.create("the-service");

Future<HttpResponse<Buffer>> future = webClient
  .request(HttpMethod.GET, new RequestOptions().setServer(serviceAddress))
  .send();

Client side load balancing

The default load balancing behavior is round-robin, you can change the load balancer to use:

HttpClient client = vertx.httpClientBuilder()
  .withAddressResolver(resolver)
  .withLoadBalancer(LoadBalancer.LEAST_REQUESTS)
  .build();

Service resolver implementations

The service resolver integrates with a few discovery services such as Kubernetes and DNS SRV records.

Kubernetes resolver

The Kubernetes resolver locates services within a Kubernetes cluster.

The Kubernetes resolver requires the endpoints resource to be accessible using the service account configured for the pod.

Here is an example configuration of role and role binding for the default service account:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: observe-endpoints
  namespace: default
rules:
  - apiGroups: [""]
    resources: ["endpoints"]
    verbs: ["get", "watch", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: observe-endpoints
  namespace: default
roleRef:
  kind: Role
  name: observe-endpoints
  apiGroup: rbac.authorization.k8s.io
subjects:
  - kind: ServiceAccount
    name: default
    namespace: default
KubeResolverOptions options = new KubeResolverOptions();

KubeResolver resolver = KubeResolver.create(options);

HttpClient client = vertx.httpClientBuilder()
  .withAddressResolver(resolver)
  .build();

The default resolver options values are loaded from the pod environment - KUBERNETES_SERVICE_HOST - KUBERNETES_SERVICE_PORT - /var/run/secrets/kubernetes.io/serviceaccount/token - /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - /var/run/secrets/kubernetes.io/serviceaccount/namespace

You can override these settings.

You can deal with ephemeral tokens using the tokenProvider.

KubeResolverOptions options = new KubeResolverOptions();

KubeResolver resolver = KubeResolver.create(options)
  .tokenProvider(() -> loadToken());

HttpClient client = vertx.httpClientBuilder()
  .withAddressResolver(resolver)
  .build();

The resolver calls the provider when it needs a fresh token. The token is cached by the resolver until it is detetected stale by the resolver (upon a 401 server response code).

Matching specific service ports

When a service exposes more than one port, the resolver retains only a single port, it might not be the expected port.

You can build a specific service address for a given service port number.

ServiceAddress serviceAddress = KubernetesServiceAddressBuilder
  .of("the-service")
  .withPortNumber(8080)
  .build();

Or a given service port name.

ServiceAddress serviceAddress = KubernetesServiceAddressBuilder
  .of("the-service")
  .withPortName(portName)
  .build();

SRV resolver

The SRV resolver uses DNS SRV records to resolve and locate services.

SrvResolverOptions options = new SrvResolverOptions()
  .setServer(SocketAddress.inetSocketAddress(dnsPort, dnsServer));

AddressResolver resolver = SrvResolver.create(options);

HttpClient client = vertx.httpClientBuilder()
  .withAddressResolver(resolver)
  .build();