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:
Endpointrepresents an individual host (with an optional port number) and its weight.EndpointGrouprepresents a set ofEndpoints.- A user specifies an
EndpointGroupwhen 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.weightedRoundRobinfor weighted round robin.EndpointSelectionStrategy.roundRobinfor round robin.StickyEndpointSelectionStrategyfor 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:
DnsAddressEndpointGroupthat retrieves theEndpointlist fromAandAAAArecordsDnsServiceEndpointGroupthat retrieves theEndpointlist fromSRVrecordsDnsTextEndpointGroupthat retrieves theEndpointlist fromTXTrecords
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.33.4')
...
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.33.4')
...
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.33.4')
...
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");
ConsulEndpointGroup consulEndpointGroup = builder.build();For more information, please refer to the API documentation of the com.linecorp.armeria.client.consul package.