如何使用 Netflix eureka 和功能区按版本定位服务

Posted

技术标签:

【中文标题】如何使用 Netflix eureka 和功能区按版本定位服务【英文标题】:How to locate services by version with Netflix eureka and ribbon 【发布时间】:2014-11-09 16:08:16 【问题描述】:

我们目前正在探索 Netflix 的 Flux Capacitor,以研究他们对微服务架构的实现。目前,我们的兴趣集中在服务注册和动态查找功能上。

浏览代码、示例和配置,但有些不清楚; 服务版本控制。如果eureka提供发现服务,而ribbon是基于eureka的REST客户端,那么客户端怎么说需要服务1.2的服务fooBar呢?客户端在哪里存储/获取该版本号;来自this 之类的本地配置文件,还是通过archaius 动态获取?

【问题讨论】:

您能解释一下您对服务版本控制的需求吗? 非常有趣的问题和链接,感谢您指出这个方向! 示例场景:假设我想部署一些“customerReview”服务的新版本,引入非向后兼容的更改,而不必迁移所有其他调用它的服务。所以我必须让新旧版本的 customerReview 都运行,至少暂时是这样。为了保持代码干净,我想避免在同一个服务实例中出现重复的“版本化”端点。到目前为止,我想不出比在不同的 spring.application.name 下部署并在 URL 中使用版本控制来路由请求更好的方法了…… @yoann-h 你是对的,它需要部署同一服务的多个版本,这是任何微服务环境的典型 【参考方案1】:

在 Eureka REST API 的 documentation 中,我看不到任何处理服务版本的内置方式。

因此,我认为处理此问题的最佳方法是将版本信息合并到您的服务 ID 中。

假设我们有 4 个服务:UserStatisticsLoginoAuth

我们刚刚更新了 User 服务 API,以更改 Login 服务中一些新要求所需的功能。然而,这些更改与 oAuth 正在使用的 API 不兼容,并且没有人有时间更新该服务。 Statistics 服务没有使用任何此功能,因此它不关心使用的是哪个版本的 API。这意味着我们需要同时运行新版本的 User 服务 (1.2) 和旧版本 (1.1)。

我们可以这样设置:

User 1.1 服务的配置说要注册为“user-1.1”“user” User 1.2 服务的配置说要注册为“user-1.2”“user” oAuth 服务的配置说 User 服务的 ID 是“user-1.1” Login 服务的配置说 User 服务的 ID 是“user-1.2” Statistics 服务的配置说 User 服务的 ID 是“user”

这样 oAuth 服务将只与旧的 User 服务通信,Login 服务与新的 User em> 服务,以及带有任何 User 服务的 Statistics 服务。

您应该能够使用 Archaius 将配置传播到服务器。

请注意,如果您发布的 User 版本 1.3 引入了与 Statistics 服务正在使用的 1.2 和 1.1 的部分不兼容的更改,您要么必须告诉 em>User 1.1 和 1.2 服务也将自己注册为其他东西(例如“user-statistics-safe”)并告诉 Statistics 服务使用该 ID,或告诉 Statistics 服务使用“user-1.1”或“user-1.2”。

【讨论】:

听起来不错。您能否详细说明如何实现多重注册? (现在我只使用基于 spring.application.name 属性的服务名称的基本注册) 用 Java 库来实现似乎有点棘手。 REST API 绝对支持它,但底层类似乎只适用于一种实例配置。您可以做的是为每个别名创建一个单独的DiscoveryClient。不过,您必须自己进行设置,因此请查看 spring-cloud does it 如何获得灵感。 我还建议您sumbit a feature request 支持DiscoveryClient 中的别名。 确实,这可能证明功能请求是合理的。我会挖掘更多并提交一个。这也可能导致对一些替代解决方案的解释。我会尝试用我的发现更新这个主题。 如果在 user-1.6 到 user-1.1 中发现错误怎么办?这种方法不是每个版本都需要一个分支吗?每个版本都有一个 CI/CD 管道?这不是意味着将所有复杂性都交给 DevOps 吗?【参考方案2】:

Service 1Eureka 注册 v1v2

Service 2 使用不同的 Ribbon 客户端发现并向 Service 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;
    

【讨论】:

以上是关于如何使用 Netflix eureka 和功能区按版本定位服务的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Netflix/Eureka 执行故障转移?

Eureka

探秘微服务治理之Spring Cloud Netflix Eureka

Eureka原理

SpringCloud注册中心Eureka

Netflix Eureka