Spring Cloud discovery для нескольких версий служб
Я задаю себе вопрос, не находя на него ответов. Может быть, у кого-то здесь есть идеи по этому поводу. ;-) Используя реестр служб (Eureka) в Spring Cloud с клиентами RestTemplate и Feign, у меня есть разные версиисборки одного и того же сервиса. Версия сборки документируется через конечную точку привода /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
}
}
Есть ли какой-либо способ запросить конкретную версию сборки по вызову клиента? Должен ли я использовать фильтры маршрутизации шлюза для управления это? Но обнаружение версии останется проблемой, я думаю...
Что ж, любое предложение приветствуется.
3 ответа:
Хорошо. Это код для вставки версии сборки в метаданные экземпляра сервиса ("service-a"), которые должны быть зарегистрированы Eureka:
@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())); } }); } }
Обратите внимание, что если "dashed composed" (то есть" 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; } }
Вы можете указать для каждого потребленная услуга список ожидаемых или избегаемых версий и обнаружение будет отфильтровано соответствующим образом.
Лесозаготовки.уровне.ком.название_компании.демо=отладка
Эврика.клиент.фильтр.enabled=true
Эврика.клиент.фильтр.сервисы.service-a. versions=0.0.1-SNAPSHOT
Пожалуйста, представьте в качестве комментариев любое предложение. Thx
Это трюк для взлома приборной панели Эврики. Добавьте этот аспект 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; } }
Вы также должны добавить зависимость, не предусмотренную 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>
И активировать LTW a runtime с помощью VM arg, конечно:
-javaagent:D:\.m2\repository\org\aspectj\aspectjweaver\1.8.9\aspectjweaver-1.8.9.jar
Сервис 1 регистры v1 и v2 С Эврикой
Служба 2 обнаруживает и отправляет запросы к службе 1 ' s v1 и v2, используя различные ленточные клиенты
Я получил эту демо-версию для работы и буду писать о ней в блоге в ближайшие пару дней.Идея I далее следовало для
RestTemplate
использовать другойRibbon
клиент для каждой версии, потому что каждый клиент имеет свой собственныйServerListFilter
.
Служба 1
Применение.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
Применение.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 ...
Применение.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
совпадает с ключомRibbon
, найденным в применение.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 аналогична, но использует
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; } }