Client-side load balancing and service discovery
Table of contents
- Creating an EndpointGroup
- Choosing an EndpointSelectionStrategy
- Connecting to an EndpointGroup
- Cleaning up an EndpointGroup
- Removing unhealthy Endpoint with HealthCheckedEndpointGroup
- DNS-based service discovery with DnsEndpointGroup
- ZooKeeper-based service discovery with ZooKeeperEndpointGroup
- Eureka-based service discovery with EurekaEndpointGroup
- Consul-based service discovery with ConsulEndpointGroup
You can configure an Armeria client to distribute its requests to more than one server autonomously, unlike traditional server-side load balancing where the requests go through a dedicated load balancer such as L4 and L7 switches.
There are 3 elements involved in client-side load balancing in Armeria:
Endpoint
represents an individual host (with an optional port number) and its weight.EndpointGroup
represents a set ofEndpoints
.- A user specifies an
EndpointGroup
when building a client.
Creating an EndpointGroup
There are various EndpointGroup
implementations provided out of the box, but let's start simple with
EndpointGroup
which always yields a pre-defined set of Endpoints
specified at construction
time:
import com.linecorp.armeria.client.Endpoint;
import com.linecorp.armeria.client.endpoint.EndpointGroup;
// Create a group of well-known search engine endpoints.
EndpointGroup searchEngineGroup = EndpointGroup.of(
Endpoint.of("www.google.com", 443),
Endpoint.of("www.bing.com", 443),
Endpoint.of("duckduckgo.com", 443));
List<Endpoint> endpoints = searchEngineGroup.endpoints();
assert endpoints.contains(Endpoint.of("www.google.com", 443));
assert endpoints.contains(Endpoint.of("www.bing.com", 443));
assert endpoints.contains(Endpoint.of("duckduckgo.com", 443));
Choosing an EndpointSelectionStrategy
An EndpointGroup
is created with EndpointSelectionStrategy.weightedRoundRobin()
by default,
unless specified otherwise. Armeria currently provides the following EndpointSelectionStrategy
implementations out-of-the-box:
EndpointSelectionStrategy.weightedRoundRobin
for weighted round robin.EndpointSelectionStrategy.roundRobin
for round robin.StickyEndpointSelectionStrategy
for pinning requests based on a criteria such as a request parameter value.- You can also implement your own
EndpointSelectionStrategy
.
An EndpointSelectionStrategy
can usually be specified as an input parameter or via a builder method
when you build an EndpointGroup
:
import com.linecorp.armeria.client.endpoint.EndpointSelectionStrategy;
import com.linecorp.armeria.client.endpoint.dns.DnsAddressEndpointGroup;
EndpointSelectionStrategy strategy = EndpointSelectionStrategy.roundRobin();
EndpointGroup group1 = EndpointGroup.of(
strategy,
Endpoint.of("127.0.0.1", 8080),
Endpoint.of("127.0.0.1", 8081));
EndpointGroup group2 =
DnsAddressEndpointGroup.builder("example.com")
.selectionStrategy(strategy)
.build();
Connecting to an EndpointGroup
Once an EndpointGroup
is created, you can specify it when creating a new client:
import static com.linecorp.armeria.common.SessionProtocol.HTTPS;
import com.linecorp.armeria.client.WebClient;
import com.linecorp.armeria.common.HttpResponse;
import com.linecorp.armeria.common.AggregatedHttpResponse;
// Create an HTTP client that sends requests to the searchEngineGroup.
WebClient client = WebClient.of(SessionProtocol.HTTPS, searchEngineGroup);
// Send a GET request to each search engine.
List<CompletableFuture<?>> futures = new ArrayList<>();
for (int i = 0; i < 3; i++) {
final HttpResponse res = client.get("/");
final CompletableFuture<AggregatedHttpResponse> f = res.aggregate();
futures.add(f.thenRun(() -> {
// And print the response.
System.err.println(f.getNow(null));
}));
}
// Wait until all GET requests are finished.
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
Cleaning up an EndpointGroup
EndpointGroup
extends java.lang.AutoCloseable
, which means you need to call the close()
method once you are done using it, usually when your application terminates:
// Release all resources claimed by the group.
searchEngines.close();
close()
is a no-op for some EndpointGroup
implementations, but not all implementations are so,
especially those which updates the Endpoint
list dynamically, such as refreshing the list periodically.
Removing unhealthy Endpoint
with HealthCheckedEndpointGroup
HealthCheckedEndpointGroup
decorates an existing EndpointGroup
to filter out the unhealthy
Endpoints
from it so that a client has less chance of sending its requests to the unhealthy
Endpoints
. It determines the healthiness by sending so called 'health check request' to each
Endpoint
, which is by default a simple HEAD
request to a certain path. If an Endpoint
responds with non-200 status code or does not respond in time, it will be marked as unhealthy and thus
be removed from the list.
import static com.linecorp.armeria.common.SessionProtocol.HTTP;
import com.linecorp.armeria.client.WebClient;
import com.linecorp.armeria.client.endpoint.healthcheck.HealthCheckedEndpointGroup
// Create an EndpointGroup with 2 Endpoints.
EndpointGroup originalGroup = EndpointGroup.of(
Endpoint.of("192.168.0.1", 80),
Endpoint.of("192.168.0.2", 80));
// Decorate the EndpointGroup with HealthCheckedEndpointGroup
// that sends HTTP health check requests to '/internal/l7check' every 10 seconds.
HealthCheckedEndpointGroup healthCheckedGroup =
HealthCheckedEndpointGroup.builder(originalGroup, "/internal/l7check")
.protocol(SessionProtocol.HTTP)
.retryInterval(Duration.ofSeconds(10))
.build();
// Wait until the initial health check is finished.
healthCheckedGroup.whenReady().get();
// Specify healthCheckedGroup, not the originalGroup.
WebClient client = WebClient.builder(SessionProtocol.HTTP, healthCheckedGroup)
.build();
DNS-based service discovery with DnsEndpointGroup
Armeria provides 3 DNS-based EndpointGroup
implementations:
DnsAddressEndpointGroup
that retrieves theEndpoint
list fromA
andAAAA
recordsDnsServiceEndpointGroup
that retrieves theEndpoint
list fromSRV
recordsDnsTextEndpointGroup
that retrieves theEndpoint
list fromTXT
records
They refresh the Endpoint
list automatically, respecting TTL values, and retry when DNS queries fail.
DnsAddressEndpointGroup
is useful when accessing an external service with multiple public IP addresses:
DnsAddressEndpointGroup group =
DnsAddressEndpointGroup.builder("www.google.com")
// Refresh more often than every 10 seconds and
// less often than every 60 seconds even if DNS server asks otherwise.
.ttl(/* minTtl */ 10, /* maxTtl */ 60)
.build();
// Wait until the initial DNS queries are finished.
group.whenReady().get();
DnsServiceEndpointGroup
is useful when accessing an internal service with
SRV records, which is often found in modern container
environments that leverage DNS for service discovery such as Kubernetes:
import com.linecorp.armeria.client.endpoint.dns.DnsServiceEndpointGroup;
import com.linecorp.armeria.client.retry.Backoff;
DnsServiceEndpointGroup group =
DnsServiceEndpointGroup.builder("_http._tcp.example.com")
// Custom backoff strategy.
.backoff(Backoff.exponential(1000, 16000).withJitter(0.3))
.build();
// Wait until the initial DNS queries are finished.
group.whenReady().get();
DnsTextEndpointGroup
is useful if you need to represent your Endpoints
in a non-standard
form:
import com.linecorp.armeria.client.endpoint.dns.DnsTextEndpointGroup;
// A mapping function must be specified.
DnsTextEndpointGroup group = DnsTextEndpointGroup.of("example.com", (byte[] text) -> {
Endpoint e = /* Convert 'text' into an Endpoint here. */;
return e
});
// Wait until the initial DNS queries are finished.
group.whenReady().get();
ZooKeeper-based service discovery with ZooKeeperEndpointGroup
First, You need the armeria-zookeeper3
dependency:
dependencies {
implementation platform('com.linecorp.armeria:armeria-bom:1.31.3')
...
implementation 'com.linecorp.armeria:armeria-zookeeper3'
}
Then, use ZooKeeperEndpointGroup
and ZooKeeperDiscoverySpec
to retrieve the information
of servers:
import com.linecorp.armeria.client.zookeeper.ZooKeeperDiscoverySpec;
import com.linecorp.armeria.client.zookeeper.ZooKeeperEndpointGroup;
String zkConnectionStr = "myZooKeeperHost:2181";
String znodePath = "/myProductionEndpoints";
ZooKeeperDiscoverySpec discoverySpec = ZooKeeperDiscoverySpec.curator(serviceName);
ZooKeeperEndpointGroup myEndpointGroup =
ZooKeeperEndpointGroup.builder(zkConnectionStr, znodePath, discoverySpec)
.sessionTimeoutMillis(10000)
.build();
The ZooKeeperEndpointGroup
is used to fetch the binary representation of server information.
The ZooKeeperDiscoverySpec
converts the binary representation to an Endpoint
.
ZooKeeperDiscoverySpec.curator()
uses the format of
Curator Service Discovery which is compatible
with Spring Cloud Zookeeper. If you
registered your server with ZooKeeperRegistrationSpec.curator()
, you must use
ZooKeeperDiscoverySpec.curator()
.
You can use an existing CuratorFramework instance instead of the ZooKeeper connection string at this time as well.
import org.apache.curator.framework.CuratorFramework;
CuratorFramework client = ...
String znodePath = ...
ZooKeeperDiscoverySpec discoverySpec = ...
ZooKeeperEndpointGroup myEndpointGroup =
ZooKeeperEndpointGroup.builder(client, znodePath, discoverySpec)
.build();
For more information, please refer to the API documentation of the com.linecorp.armeria.client.zookeeper package.
Eureka-based service discovery with EurekaEndpointGroup
First, You need the armeria-eureka
dependency:
dependencies {
implementation platform('com.linecorp.armeria:armeria-bom:1.31.3')
...
implementation 'com.linecorp.armeria:armeria-eureka'
}
Then, use EurekaEndpointGroup
to retrieve the information of servers:
import com.linecorp.armeria.client.eureka.EurekaEndpointGroup;
EurekaEndpointGroup eurekaEndpointGroup =
EurekaEndpointGroup.of("https://eureka.com:8001/eureka/v2");
If you want to retrieve the information of certain regions or a service,
use EurekaEndpointGroupBuilder
:
import com.linecorp.armeria.client.eureka.EurekaEndpointGroupBuilder;
EurekaEndpointGroupBuilder builder =
EurekaEndpointGroup.builder("https://eureka.com:8001/eureka/v2");
.regions("aws")
.appName("myApplication");
EurekaEndpointGroup eurekaEndpointGroup = builder.build();
For more information, please refer to the API documentation of the com.linecorp.armeria.client.eureka package.
Consul-based service discovery with ConsulEndpointGroup
First, You need the armeria-consul
dependency:
dependencies {
implementation platform('com.linecorp.armeria:armeria-bom:1.31.3')
...
implementation 'com.linecorp.armeria:armeria-consul'
}
Then, use ConsulEndpointGroup
to retrieve the information of servers:
import com.linecorp.armeria.client.consul.ConsulEndpointGroup;
ConsulEndpointGroup consulEndpointGroup =
ConsulEndpointGroup.of("http://my-consul.com:8500", "my-service");
If you want to get the information of certain datacenter or
filter endpoints, use ConsulEndpointGroupBuilder
:
import com.linecorp.armeria.client.consul.ConsulEndpointGroupBuilder;
ConsulEndpointGroupBuilder builder =
ConsulEndpointGroup.builder("http://my-consul.com:8500", "my-service");
.datacenter("ds1")
.filter("ServicePort == 8080")
.build();
ConsulEndpointGroup consulEndpointGroup = builder.build();
For more information, please refer to the API documentation of the com.linecorp.armeria.client.consul package.