【问题标题】:Spring Cloud discovery for multiple service versions多个服务版本的 Spring Cloud 发现
【发布时间】:2017-07-06 21:21:24
【问题描述】:

我在问自己一个问题,却没有找到答案。也许这里有人对此有想法;-) 在 Spring Cloud 中使用带有 RestTemplate 和 Feign 客户端的服务注册表 (Eureka),我有相同服务的不同 构建版本。通过 Actuator 的 /info 端点记录的构建版本。

{
"build": {
"version": "0.0.1-SNAPSHOT",
"artifact": "service-a",
"name": "service-a",
"group": "com.mycompany",
"time": 1487253409000
}
}
...
{
"build": {
"version": "0.0.2-SNAPSHOT",
"artifact": "service-a",
"name": "service-a",
"group": "com.mycompany",
"time": 1487325340000
}
}

有没有办法在客户的电话中要求特定的构建版本? 我应该使用网关的路由过滤器来管理它吗?但是我猜版本检测仍然是一个问题......

嗯,任何建议表示赞赏。

【问题讨论】:

  • 我认为没有什么可以提供帮助。您还需要将版本信息传送到 Eureka 服务器,以便 Eureka 客户端知道版本。您可能可以通过 Eureka 元数据来做到这一点。至于 Feign 客户端利用这些信息,您可能必须使用发现服务 API 来获取有关服务的元数据并决定调用哪个实例。
  • 我认为你是对的 :-( 但这是一个真正的问题,因为你首先必须收集每个服务实例,然后按版本过滤它们
  • 可能是一个好的开始:jmnarloch.wordpress.com/2015/11/25/…
  • 在调试 DiscoveryClient#getInstances 结果时,我可以看到 EurekaDiscoveryClient$EurekaServiceInstance 有一个 InstanceInfo 属性,其中包含服务名称和版本属性,设置为 UNKNOWN。仍在挖掘 ;-)
  • 我目前的想法是让 spring-cloud-netflix-eureka-server 从 Actuator 的端点收集版本信息,将此信息放入 InstanceInfo 和/或元数据中,扩展当前 API 以接受版本标准AND 功能区客户端同时使用 API 和客户端过滤器版本。刚刚完成;-)

标签: spring spring-cloud netflix-eureka spring-cloud-feign


【解决方案1】:

好的。这是将构建版本注入 Eureka 注册的服务(“service-a”)实例元数据的代码:

@Configuration
@ConditionalOnClass({ EurekaInstanceConfigBean.class, EurekaClient.class })
public class EurekaClientInstanceBuildVersionAutoConfiguration {

    @Autowired(required = false)
    private EurekaInstanceConfig instanceConfig;

    @Autowired(required = false)
    private BuildProperties buildProperties;

    @Value("${eureka.instance.metadata.keys.version:instanceBuildVersion}")
    private String versionMetadataKey;

    @PostConstruct
    public void init() {
        if (this.instanceConfig == null || buildProperties == null) {
            return;
        }
        this.instanceConfig.getMetadataMap().put(versionMetadataKey, buildProperties.getVersion());
    }
}

这是在“service-b”中验证元数据传输的代码:

@Component
public class DiscoveryClientRunner implements CommandLineRunner {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private DiscoveryClient client;

    @Override
    public void run(String... args) throws Exception {
        client.getInstances("service-a").forEach((ServiceInstance s) -> {
            logger.debug(String.format("%s: %s", s.getServiceId(), s.getUri()));
            for (Entry<String, String> md : s.getMetadata().entrySet()) {
                logger.debug(String.format("%s: %s", md.getKey(), md.getValue()));
            }
        });
    }
}

请注意,如果“破折号组合”(即“instance-build-version”),元数据键是骆驼大小写强制。

这是我找到的根据版本过滤服务实例的解决方案:

@Configuration
@EnableConfigurationProperties(InstanceBuildVersionProperties.class)
public class EurekaInstanceBuildVersionFilterAutoConfig {

    @Value("${eureka.instance.metadata.keys.version:instanceBuildVersion}")
    private String versionMetadataKey;

    @Bean
    @ConditionalOnProperty(name = "eureka.client.filter.enabled", havingValue = "true")
    public EurekaInstanceBuildVersionFilter eurekaInstanceBuildVersionFilter(InstanceBuildVersionProperties filters) {
        return new EurekaInstanceBuildVersionFilter(versionMetadataKey, filters);
    }
}

@Aspect
@RequiredArgsConstructor
public class EurekaInstanceBuildVersionFilter {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    private final String versionMetadataKey;
    private final InstanceBuildVersionProperties filters;

    @SuppressWarnings("unchecked")
    @Around("execution(public * org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient.getInstances(..))")
    public Object filterInstances(ProceedingJoinPoint jp) throws Throwable {
        if (filters == null || !filters.isEnabled()) logger.error("Should not be filtering...");
        List<ServiceInstance> instances = (List<ServiceInstance>) jp.proceed();
        return instances.stream()
                .filter(i -> filters.isKept((String) jp.getArgs()[0], i.getMetadata().get(versionMetadataKey))) //DEBUG MD key is Camel Cased!
                .collect(Collectors.toList());
    }
}

@ConfigurationProperties("eureka.client.filter")
public class InstanceBuildVersionProperties {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * Indicates whether or not service instances versions should be filtered
     */
    @Getter @Setter
    private boolean enabled = false;

    /**
     * Map of service instance version filters.
     * The key is the service name and the value configures a filter set for services instances
     */
    @Getter
    private Map<String, InstanceBuildVersionFilter> services = new HashMap<>();

    public boolean isKept(String serviceId, String instanceVersion) {
        logger.debug("Considering service {} instance version {}", serviceId, instanceVersion);
        if (services.containsKey(serviceId) && StringUtils.hasText(instanceVersion)) {
            InstanceBuildVersionFilter filter = services.get(serviceId);
            String[] filteredVersions = filter.getVersions().split("\\s*,\\s*");    // trimming
            logger.debug((filter.isExcludeVersions() ? "Excluding" : "Including") + " instances: " + Arrays.toString(filteredVersions));
            return contains(filteredVersions, instanceVersion) ? !filter.isExcludeVersions() : filter.isExcludeVersions();
        }
        return true;
    }

    @Getter @Setter
    public static class InstanceBuildVersionFilter {
        /**
         * Comma separated list of service version labels to filter
         */
        private String versions;
        /**
         * Indicates whether or not to keep the associated instance versions.
         * When false, versions are kept, otherwise they will be filtered out
         */
        private boolean excludeVersions = false;
    }
}

您可以为每个使用的服务指定一个预期或避免的版本列表,并且会相应地过滤发现。

logging.level.com.mycompany.demo=DEBUG

eureka.client.filter.enabled=true

eureka.client.filter.services.service-a.versions=0.0.1-SNAPSHOT

请作为 cmets 提交任何建议。谢谢

【讨论】:

  • 请参阅替代解决方案的 cmets 以简化注册过程的一部分:您可以添加具有简单属性的元数据。
【解决方案2】:

Service 1Eureka 注册 v1v2

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

我完成了这个演示,并将在接下来的几天内发布关于它的博客。

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

【讨论】:

  • 非常感谢,我会仔细看看。但这似乎并没有解决我的用例:服务(1)为自己声明/发布多个版本有什么用?这可能违反了 SRP。我需要部署两个自治版本的服务 1,然后从客户端(服务 2)端过滤不支持的版本。另外,我的解决方案避免操纵多个 HTTP(功能区)客户端。
  • 顺便说一句,你有一个很好的观点,建议 eureka.instance 属性提供元数据(我错过了);所以在我的示例中,我的 EurekaClientInstanceBuildVersionAutoConfiguration 类可以替换为“eureka.instance.metadata-map.instanceBuildVersion=@pom.version@”。谢谢
  • “服务 (1) 为自己声明/发布多个版本有什么用?”我认为 API 永远不应该破坏向后兼容,除非它们(客户端和服务)部署在一个非常受控的环境中,例如现有客户端永远不会遇到更新的 API。
  • "加上我的解决方案避免操纵多个 HTTP (Ribbon) 客户端" 这只是一个演示方法。在现实世界的场景中,可能会有“升级缓慢”的客户端向 API v1 发送请求,而其他客户端可能正在访问 API v2。如果“客户端升级缓慢”的核心业务与 API v1、v2 提供的内容没有直接关系,那么他们可能需要数年才能升级,想想谷歌地图 v2 和 v3。我坚信强迫客户在一夜之间升级是一种不好的做法。
  • 非常感谢您的意见,@ootero。我的想法是不需要在同一个实例中提供 2 个连续的 API。不同版本的带宽使用可能不同,因此您无法单独扩展它们。我更多地考虑每个可用版本的几个实例,减少 v1 实例的数量,因为采用 v2 需要更多“现代”服务实例。这种方法没有中断,无需切断服务,因为负载平衡允许过渡。
【解决方案3】:

这是破解 Eureka Dashboard 的技巧。 将这个 AspectJ 方面(因为 EurekaController 中使用的 InstanceInfo 不是 Spring Bean)添加到 @EnableEurekaServer 项目中:

@Configuration
@Aspect
public class EurekaDashboardVersionLabeler {

    @Value("${eureka.instance.metadata.keys.version:instanceBuildVersion}")
    private String versionMetadataKey;

    @Around("execution(public * com.netflix.appinfo.InstanceInfo.getId())")
    public String versionLabelAppInstances(ProceedingJoinPoint jp) throws Throwable {
        String instanceId = (String) jp.proceed();
        for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
            // limit to EurekaController#populateApps in order to avoid side effects
            if (ste.getClassName().contains("EurekaController")) {
                InstanceInfo info = (InstanceInfo) jp.getThis();
                String version = info.getMetadata().get(versionMetadataKey);
                if (StringUtils.hasText(version)) {
                    return String.format("%s [%s]", instanceId, version);
                }
                break;
            }
        }
        return instanceId;
    }

    @Bean("post-construct-labeler")
    public EurekaDashboardVersionLabeler init() {
        return EurekaDashboardVersionLabeler.aspectOf();
    }

    private static EurekaDashboardVersionLabeler instance = new EurekaDashboardVersionLabeler();
    /** Singleton pattern used by LTW then Spring */
    public static EurekaDashboardVersionLabeler aspectOf() {
        return instance;
    }
}

您还必须添加 starters 未提供的依赖项:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

当然,还要使用 VM arg 激活 LTW 运行时:

-javaagent:D:\.m2\repository\org\aspectj\aspectjweaver\1.8.9\aspectjweaver-1.8.9.jar

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-11-17
    • 2018-04-15
    • 2017-02-20
    • 2017-06-26
    • 1970-01-01
    • 1970-01-01
    • 2017-08-08
    • 2020-02-10
    相关资源
    最近更新 更多