如何使用Netflix Eureka和Ribbon根据版本定位服务。

14
我们目前正在探索Netflix的Flux Capacitor,以研究他们对微服务架构的实现。我们目前的兴趣集中在服务注册和动态查找功能上。
浏览代码、示例和配置,但有些东西不清楚;服务版本控制。如果eureka提供了发现服务,而ribbon是基于eureka的REST客户端,那么客户端如何说它需要服务1.2的服务fooBar?客户端从哪里存储/获取该版本号;从像this这样的本地配置文件,还是通过archaius动态获取?

你能用一个现实世界的场景来解释一下你对服务版本控制的需求吗? - java_geek
2
非常有趣的问题和链接,感谢您指引我朝着这个方向! - ChrLipp
1
示例场景:假设我想部署某个“customerReview”服务的新版本,引入不向后兼容的更改,而无需一次性迁移调用它的所有其他服务。因此,我必须保持旧版本和新版本的customerReview运行,至少是暂时的。为了保持代码清洁,我希望避免在同一服务实例中重复使用“有版本的”端点。到目前为止,我想不到比在不同的spring.application.name下部署并在URL中使用版本控制来路由请求更好的方法了... - yoann-h
@yoann-h 你说得对,这需要部署多个版本的同一服务,这是任何微服务架构的典型特征。 - raffian
2个回答

7
在Eureka REST API的文档中,我没有看到任何处理服务版本的内置方法。
因此,我认为处理这个问题的最佳方法是在服务ID中包含版本信息。
假设我们有4个服务:用户、统计、登录和oAuth。
我们刚刚更新了用户服务API以满足登录服务中一些新需求所需的功能。但是,这些更改与oAuth正在使用的API不兼容,而且没有人时间更新该服务。统计服务没有使用任何这些功能,因此它不关心使用哪个API版本。这意味着我们需要同时运行新版本的用户服务(1.2)和旧版本(1.1)。
我们可以这样设置:
  • 用户1.1服务的配置说要注册为“user-1.1”和“user”
  • 用户1.2服务的配置说要注册为“user-1.2”和“user”
  • oAuth服务的配置说用户服务的ID是“user-1.1”
  • 登录服务的配置说用户服务的ID是“user-1.2”
  • 统计服务的配置说用户服务的ID是“user”
这样,oAuth服务将只与旧的用户服务通信,登录服务将与新的用户服务通信,统计服务将与任何用户服务通信。
您应该可以使用Archaius将配置传播到服务器。
请注意,如果发布用户版本1.3,它会引入与统计服务正在使用的1.2和1.1部分不兼容的更改,则必须告诉用户1.1和1.2服务也注册为其他内容(例如“user-statistics-safe”),并告诉统计服务使用该ID,或者告诉统计服务使用“user-1.1”或“user-1.2”。

好的。您能否详细说明一下如何实现多重注册?(目前我只是使用基本的注册,根据spring.application.name属性来命名服务) - yoann-h
使用Java库似乎有点棘手。REST API肯定支持它,但是底层类似乎只能处理一个实例配置。您可以为每个别名创建单独的“DiscoveryClient”。不过,您必须自己设置它,请查看spring-cloud如何进行启发(https://github.com/spring-cloud/spring-cloud-netflix/blob/master/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/eureka/EurekaDiscoveryClientConfiguration.java)。 - Raniz
我还建议您提交功能请求,以支持DiscoveryClient中的别名。 - Raniz
1
确实,这可能会成为一个功能请求的理由。我会再深入挖掘并提交一个请求。这也可能导致一些替代解决方案的解释。我会尽力更新这个主题,分享我的发现。 - yoann-h
如果在用户-1.6到用户-1.1中发现了一个错误,怎么办? 难道这种方法需要每个版本都有一个分支吗?每个版本都需要一个CI/CD流水线吗?这是否意味着将所有复杂性传递给DevOps? - ootero

1

服务1使用Eureka注册v1v2

服务2使用不同的Ribbon客户端发现并向服务1的v1和v2发送请求

我通过使用Spring Cloud使这个演示工作,博客可以在此找到:http://tech.asimio.net/2017/03/06/Multi-version-Service-Discovery-using-Spring-Cloud-Netflix-Eureka-and-Ribbon.html

我的想法是让RestTemplate为每个版本使用不同的Ribbon客户端,因为每个客户端都有自己的ServerListFilter

多版本服务将版本包含在注册元数据中。


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

服务 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(查看Ribbon客户端名称与在application.yml中找到的Ribbon键如何匹配)

...
@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与之类似,但使用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;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接