Search results
Multi-version Service Discovery using Spring Cloud Netflix Eureka and Ribbon
1. OVERVIEW
As services evolve over time, changes are made to the domain that very-likely require API versioning to prevent breaking backwards compatibility.
Unless you work in a very controlled environment, for instance, where it’s known that client applications are also distributed with the API services and there is no possibility of an old client using newer API services deployment, I’m a firm believer you should never break backwards compatibility.
I’ll say it again because this is very important, you should never break backwards compatibility, if you do, customers and partners get very angry and fixing it afterwards normally requires more effort than versioning from the beginning.
The question now is, how does versioning work in a microservice architecture where services are registered with and discovered using a Discovery infrastructure microservice such as Netflix Eureka?
In this post I’ll explain how to register and discover multiple version of a service using Spring Cloud Netflix Eureka and Ribbon.
2. REQUIREMENTS
- Java 7+.
- Maven 3.2+.
- Familiarity with Spring Framework.
- A Eureka server instance for the Demo Service 1 to register with and the Demo Service 2 to fetch the registry from.
3. CREATE THE DEMO SERVICE 1
This is a simple Spring Boot application which implements two versions of the same endpoint and registers both versions with a Eureka server instance.
This command will create a Maven project in a folder named demo-multiversion-registration-api-1
with most of the dependencies used in the accompanying source code.
bootstrap.yml
:
This is the name the service will use to register with Eureka.
application.yml
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
...
eureka:
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8000/eureka/
instance:
hostname: ${hostName}
statusPageUrlPath: ${management.context-path}/info
healthCheckUrlPath: ${management.context-path}/health
preferIpAddress: true
metadataMap:
instanceId: ${spring.application.name}:${server.port}
---
spring:
profiles: v1
eureka:
instance:
metadataMap:
versions: v1
---
spring:
profiles: v1v2
eureka:
instance:
metadataMap:
versions: v1,v2
...
This file includes Eureka-related configuration settings, already explained here.
The interesting part is the Spring profiles, where a new metadata (key, value) pair is added to each profile. These (key, value) pairs make to Eureka through the registration process and will be used by Demo Service 2 to filter out service instances.
Application.java
:
This is the application entry point telling the Spring container to scan com.asimio.api.multiversion.demo1.rest
package for @Component or derivate annotations.
...v1.ActorResource.java
:
...v2.ActorResource.java
:
The resources implementation details doesn’t matter, just that ...v1.Actor
and ...v2.Actor
classes are different either in the number of attributes or their names.
Lets start a Eureka instance and Demo Service 1 and take a look at its registration metadata.
4. RUNNING THE EUREKA SERVER
Available in the source code section, the Eureka server could be downloaded, built and started as:
For more running options please refer to Microservices Registration and Discovery using Spring Cloud Eureka Ribbon and Feign.
5. RUNNING THE DEMO SERVICE 1
Also available in the source code area, let’s start two instances, each corresponding to a different Spring profile:
Once registered with the Eureka server, demo-multiversion-registration-api-1
metadata looks like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
curl http://localhost:8000/eureka/apps/DEMO-MULTIVERSION-REGISTRATION-API-1
<application>
<name>DEMO-MULTIVERSION-REGISTRATION-API-1</name>
<instance>
<instanceId>orlandos-mbp.3velopers.net:demo-multiversion-registration-api-1:8601</instanceId>
...
<metadata>
<instanceId>demo-multiversion-registration-api-1:8601</instanceId>
<versions>v1</versions>
</metadata>
...
</instance>
<instance>
<instanceId>orlandos-mbp.3velopers.net:demo-multiversion-registration-api-1:8602</instanceId>
...
<metadata>
<instanceId>demo-multiversion-registration-api-1:8602</instanceId>
<versions>v1,v2</versions>
</metadata>
...
</instance>
...
</application>
Notice in line the presence of <versions>v1</versions>
and <versions>v1,v2</versions>
elements which corresponds with the new metadata added in Demo Service 1’s applicataion.yml.
6. CREATE THE DEMO SERVICE 2
Demo Service 2 is also a simple Spring Boot application exposing two endpoints and acting as a client of Demo Service 1 to demonstrate sending requests to different API versions.
Creating it is similar to Demo Service 1 creation but changing baseDir
and artifactId
parameters:
application.yml
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
eureka:
client:
registerWithEureka: false
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8000/eureka/
demo-multiversion-registration-api1-v1:
ribbon:
# Eureka vipAddress of the target service
DeploymentContextBasedVipAddresses: demo-multiversion-registration-api-1
NIWSServerListClassName: com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
# Interval to refresh the server list from the source (ms)
ServerListRefreshInterval: 30000
demo-multiversion-registration-api1-v2:
ribbon:
# Eureka vipAddress of the target service
DeploymentContextBasedVipAddresses: demo-multiversion-registration-api-1
NIWSServerListClassName: com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList
# Interval to refresh the server list from the source (ms)
ServerListRefreshInterval: 30000
...
Two Ribbon clients are defined with ids: demo-multiversion-registration-api1-v1
and demo-multiversion-registration-api1-v2
. Both target the same service: demo-multiversion-registration-api-1
via service discovery using Eureka.
Application.java
:
Similarly to Demo Service 1’s Application.java, this is the application’s entry point also indicating the Spring IoC container to scan com.asimio.api.multiversion.demo2.config
and com.asimio.api.multiversion.demo2.rest
packages for @Component-related annotations.
AppConfig.java
:
This class instantiates the RestTemplate bean to send requests to Demo Service 1. @LoadBalanced indicates this RestTemplate instance will use a Ribbon client as the internal HTTP client to send requests.
It also defines two Ribbon clients where the names match the client keys found in Demo Service 2’s application.yml and configured by @RibbonClient configuration
’s value.
RibbonConfigDemoApi1V1.java
:
This is the first Ribbon client configuration implementation. It provides a specific ServerListFilter bean, reviewed here, but it could have also provided other beans such as implementation of:
IPing |
IRule |
ILoadBalancer |
ServerListFilter |
IClientConfig |
Spring Cloud Netflix creates an ApplicationContext for each Ribbon client and configures the client components with these beans.
RibbonConfigDemoApi1V2.java
is similar to RibbonConfigDemoApi1V1.java
but using enum DEMO_REGISTRATION_API1_V2.
RibbonClientApi.java
:
A enum with service and version information. demo-multiversion-registration-api-1
is the service name found in the Eureka registry.
VersionedNIWSServerListFilter.java
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.asimio.api.multiversion.demo2.niws.loadbalancer;
...
public class VersionedNIWSServerListFilter<T extends Server> extends DefaultNIWSServerListFilter<T> {
private static final String VERSION_KEY = "versions";
private final DiscoveryClient discoveryClient;
private final RibbonClientApi ribbonClientApi;
public VersionedNIWSServerListFilter(DiscoveryClient discoveryClient, RibbonClientApi ribbonClientApi) {
this.discoveryClient = discoveryClient;
this.ribbonClientApi = ribbonClientApi;
}
@Override
public List<T> getFilteredListOfServers(List<T> servers) {
List<T> result = new ArrayList<>();
List<ServiceInstance> serviceInstances = this.discoveryClient.getInstances(this.ribbonClientApi.serviceId);
for (ServiceInstance serviceInstance : serviceInstances) {
List<String> versions = this.getInstanceVersions(serviceInstance);
if (versions.isEmpty() || versions.contains(this.ribbonClientApi.version)) {
result.addAll(this.findServerForVersion(servers, serviceInstance));
}
}
return result;
}
private List<String> getInstanceVersions(ServiceInstance serviceInstance) {
List<String> result = new ArrayList<>();
String rawVersions = serviceInstance.getMetadata().get(VERSION_KEY);
if (StringUtils.isNotBlank(rawVersions)) {
result.addAll(Arrays.asList(rawVersions.split(",")));
}
return result;
}
...
}
This is the ServerListFilter implementation used to configure each Ribbon client such the ones defined with @RibbonClient annotation.
All it does is filtering the server instances by id
and version
so that a Ribbon client doesn’t sends requests to a host that doesn’t support the requested version.
AggregationResource.java
:
Demo Service 2 implements APIs that delegates to either Demo Service 1’s v1
or v2
. It does so using a Ribbon-configured RestTemplate bean. Notice the service ids used in both endpoints match Ribbon client keys as configured in Demo Service 2’s application.yml and AppConfig.java.
7. RUNNING THE DEMO SERVICE 2
Assuming the Eureka server is running and Demo Service 1 instances using v1
and v1v2
are also running, lets start Demo Service 2:
Let’s send a requests to v1
:
Notice "lastName":"8601"
and "lastName":"8602"
, it means both Demo Service 1 instances are being load-balanced.
Let’s send a requests to v2
:
The Demo Service 1 instance listening on 8602
is the only one servicing v2
. Once it gets shutdown requests fail as follow:
That’s all for this post.
Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.
8. SOURCE CODE
Accompanying source code for this blog post can be found at: