Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to locate services by version with Netflix eureka and ribbon

We're currently exploring Flux Capacitor from Netflix to study their implementation of micro-service architectures. Our interests, at the moment, are focused on service registration and dynamic look-up functionality.

Browsing through the code, samples, and configuration, but something's not clear; service versioning. If eureka provides discovery services, and ribbon is eureka-based REST client, how does a client say it needs service 1.2 of service fooBar? Where does the client store/get that version number; from a local config file like this, or is it dynamically-obtained through archaius?

like image 803
raffian Avatar asked Sep 16 '14 02:09

raffian


2 Answers

I can't see any built-in way of handling service versions in the documentation for the Eureka REST API.

Therefore I think the best way of handling this would be to incorporate versioning information in your service IDs.

Let's say we have 4 services: User, Statistics, Login, and oAuth.

We've just updated the User service API to change functionality required for some new requirements in the Login service. These changes are however incompatible with the API that oAuth is using and no one has the time to update that service. The Statistics service isn't using any of this functionality so it doesn't care which version of the API is used. This means we need both the new version of the User service (1.2) and the old version (1.1) running at the same time.

We can set it up like this:

  • The configuration for the User 1.1 services says to register as "user-1.1" and "user"
  • The configuration for the User 1.2 services says to register as "user-1.2" and "user"
  • The configuration for the oAuth service says that the ID of the User service is "user-1.1"
  • The configuration for the Login service says that the ID of the User service is "user-1.2"
  • The configuration for the Statistics service says that the ID of the User service is "user"

This way the oAuth service will only communicate with the old User service, the Login service with the new User service, and the Statistics service with any User service.

You should be able to use Archaius to propagate the configuration to the servers.

Note that if you release User version 1.3 that introduces changes incompatible with the parts of 1.2 and 1.1 that the Statistics service is using you will either have to tell the User 1.1 and 1.2 services to register themselves as something else too (like "user-statistics-safe") and tell the Statistics service to use that ID, or tell the Statistics service to use either "user-1.1" or "user-1.2".

like image 121
Raniz Avatar answered Oct 25 '22 21:10

Raniz


Service 1 registers v1 and v2 with Eureka

Service 2 discovers and sends requests to Service 1's v1 and v2 using different Ribbon clients

I got this demo to work using Spring Cloud though blog could found at: http://tech.asimio.net/2017/03/06/Multi-version-Service-Discovery-using-Spring-Cloud-Netflix-Eureka-and-Ribbon.html

The idea I followed was for RestTemplate to use a different Ribbon client for each version because each client has its own ServerListFilter.

The multi-version service includes the version in registration metadata.


Service 1

application.yml

...
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
...

Service 2

application.yml

...
eureka:
  client:
    registerWithEureka: false
    fetchRegistry: true
    serviceUrl:
      defaultZone: http://localhost:8000/eureka/

demo-multiversion-registration-api-1-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-api-1-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
...

Application.java

...
@SpringBootApplication(scanBasePackages = {
    "com.asimio.api.multiversion.demo2.config",
    "com.asimio.api.multiversion.demo2.rest"
})
@EnableDiscoveryClient
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

AppConfig.java (See how the Ribbon client name matches the Ribbon key found in application.yml

...
@Configuration
@RibbonClients(value = {
    @RibbonClient(name = "demo-multiversion-registration-api-1-v1", configuration = RibbonConfigDemoApi1V1.class),
    @RibbonClient(name = "demo-multiversion-registration-api-1-v2", configuration = RibbonConfigDemoApi1V2.class)
})
public class AppConfig {

    @Bean(name = "loadBalancedRestTemplate")
    @LoadBalanced
    public RestTemplate loadBalancedRestTemplate() {
        return new RestTemplate();
    }
}

RibbonConfigDemoApi1V1.java

...
public class RibbonConfigDemoApi1V1 {

    private DiscoveryClient discoveryClient;

    @Bean
    public ServerListFilter<Server> serverListFilter() {
        return new VersionedNIWSServerListFilter<>(this.discoveryClient, RibbonClientApi.DEMO_REGISTRATION_API_1_V1);
    }

    @Autowired
    public void setDiscoveryClient(DiscoveryClient discoveryClient) {
        this.discoveryClient = discoveryClient;
    }
}

RibbonConfigDemoApi1V2.java is similar but using RibbonClientApi.DEMO_REGISTRATION_API_1_V2

RibbonClientApi.java

...
public enum RibbonClientApi {

    DEMO_REGISTRATION_API_1_V1("demo-multiversion-registration-api-1", "v1"),

    DEMO_REGISTRATION_API_1_V2("demo-multiversion-registration-api-1", "v2");

    public final String serviceId;
    public final String version;

    private RibbonClientApi(String serviceId, String version) {
        this.serviceId = serviceId;
        this.version = version;
    }
}

VersionedNIWSServerListFilter.java

...
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;
    }
...

AggregationResource.java

...
@RestController
@RequestMapping(value = "/aggregation", produces = "application/json")
public class AggregationResource {

    private static final String ACTORS_SERVICE_ID_V1 = "demo-multiversion-registration-api-1-v1";
    private static final String ACTORS_SERVICE_ID_V2 = "demo-multiversion-registration-api-1-v2";

    private RestTemplate loadBalancedRestTemplate;

    @RequestMapping(value = "/v1/actors/{id}", method = RequestMethod.GET)
    public com.asimio.api.multiversion.demo2.model.v1.Actor findActorV1(@PathVariable(value = "id") String id) {
        String url = String.format("http://%s/v1/actors/{id}", ACTORS_SERVICE_ID_V1);
        return this.loadBalancedRestTemplate.getForObject(url, com.asimio.api.multiversion.demo2.model.v1.Actor.class, id);
    }

    @RequestMapping(value = "/v2/actors/{id}", method = RequestMethod.GET)
    public com.asimio.api.multiversion.demo2.model.v2.Actor findActorV2(@PathVariable(value = "id") String id) {
        String url = String.format("http://%s/v2/actors/{id}", ACTORS_SERVICE_ID_V2);
        return this.loadBalancedRestTemplate.getForObject(url, com.asimio.api.multiversion.demo2.model.v2.Actor.class, id);
    }

    @Autowired
    public void setLoadBalancedRestTemplate(RestTemplate loadBalancedRestTemplate) {
        this.loadBalancedRestTemplate = loadBalancedRestTemplate;
    }
}
like image 1
ootero Avatar answered Oct 25 '22 21:10

ootero