2017-02-17 17 views
3

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

{ 
"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 
} 
} 

是否有任何意思要求在客户的电话的特定版本版本? 我应该使用网关的路由过滤器来管理它吗?但版本检测将仍然是一个问题,我猜...

好吧,任何建议赞赏。

+1

我不觉得有什么开箱,这将有助于。您需要将版本信息传送给Eureka服务器,以便Eureka客户端知道该版本。你可以通过Eureka元数据来完成。至于Feign客户端利用这些信息,您可能必须使用发现服务API来获取关于该服务的元数据并决定调用哪个实例。 –

+0

我认为你是对的:-(但这是一个真正的问题,因为你首先必须收集每个服务实例,然后通过版本 –

+0

进行过滤可能是一个好的开始:https://jmnarloch.wordpress.com/2015/11/ 25/spring-cloud-ribbon-dynamic-routing/ –

回答

1

好的。这是代码注入生成版本到服务(“服务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()); 
    } 
} 

这是验证一个“服务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())); 
      } 
     }); 
    } 
} 

注意,如果“虚线组成的”(即“实例构建版本”),元数据键为驼峰强制。

这是我发现根据自己的版本来筛选服务实例的解决方案:

@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 =真

eureka.client.filter.services.service-a.versions = 0.0。 1-SNAPSHOT

请作为意见提交任何建议。 Thx

+0

请参阅备用解决方案的注释以简化注册过程的一部分:您可以添加具有简单属性的元数据。 –

0

这是黑客使用Eureka仪表板的技巧。 加入这个AspectJ的方面(因为InstanceInfo在EurekaController使用的是不是一个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; 
    } 
} 

您也必须添加不首先提供一个依赖性:

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

而且激活LTW一个运行时与VM精氨酸,当然:

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

服务1寄存器V1V2尤里卡

服务2个发现并发送请求到服务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; 
    } 
} 
+0

非常感谢,我会仔细看看。但是这似乎并没有解决我的用例:服务(1)为自己声明/发布多个版本有什么用?这可能违反了SRP。我需要部署两个自治版本的Service 1,然后从客户端(服务2)端过滤不支持的版本。另外我的解决方案避免了操纵多个HTTP(Ribbon)客户端。 –

+0

顺便说一句,你有一个非常好的观点,建议eureka.instance属性提供元数据(我错过了它);因此我的示例中我的EurekaClientInstanceBuildVersionAutoConfiguration类可以替换为“eureka.instance.metadata-map.instanceBuildVersion = @ pom.version @”。 Thx –

+0

“服务(1)为自己声明/发布多个版本有什么用?” 我相信API不应该破坏compat,除非它们(客户端和服务)部署在非常受控制的环境中,例如现有的客户端从不碰到更新的API。 – ootero