Обнаружение Spring Cloud для нескольких версий сервиса

Я задаю себе вопрос, но не нахожу на него ответов. Возможно, у кого-то из присутствующих возникнут идеи по этому поводу ;-) Используя реестр служб (Eureka) в Spring Cloud с клиентами RestTemplate и Feign, у меня есть разные версии сборки одного и того же сервиса. Версия сборки документируется через конечную точку 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
}
}

Есть ли способ запросить конкретную версию сборки по звонку клиента? Должен ли я использовать фильтры маршрутизации шлюза, чтобы справиться с этим? Но обнаружение версии, я думаю, останется проблемой ...

Что ж, любое предложение приветствуется.


person Thomas Escolan    schedule 17.02.2017    source источник
comment
Я не думаю, что есть что-то нестандартное, что поможет. Вам также потребуется передать информацию о версии на сервер Eureka, чтобы клиенты Eureka знали версию. Вероятно, вы могли бы сделать это с помощью метаданных Eureka. Что касается клиентов Feign, которые затем используют эту информацию, вам, вероятно, придется использовать API-интерфейсы службы обнаружения, чтобы затем получить метаданные о службе и решить, какой экземпляр вызвать.   -  person Ryan Baxter    schedule 20.02.2017
comment
Я думаю, что вы правы :-( но это реальная проблема, так как вам сначала нужно собрать все экземпляры службы, а затем отфильтровать их по версии   -  person Thomas Escolan    schedule 21.02.2017
comment
Вероятно, хорошее начало: jmnarloch.wordpress.com/ 2015/11/25 /   -  person Thomas Escolan    schedule 21.02.2017
comment
При отладке результата DiscoveryClient # getInstances я вижу, что EurekaDiscoveryClient $ EurekaServiceInstance имеет свойство InstanceInfo, которое содержит имя службы и свойство версии, для которых установлено значение UNKNOWN. Все еще копаю ;-)   -  person Thomas Escolan    schedule 21.02.2017
comment
Моя текущая идея состоит в том, чтобы заставить spring-cloud-netflix-eureka-server собирать информацию о версии с конечной точки Actuator, помещать эту информацию в InstanceInfo и / или метаданные, расширять текущий API для принятия критериев версии И ленточного клиента, чтобы оба использовали API и версии с фильтром клиентов. Просто сделать ;-)   -  person Thomas Escolan    schedule 21.02.2017
comment
+ Приятно иметь: отображение версии на приборной панели   -  person Thomas Escolan    schedule 21.02.2017
comment
на самом деле сбор и отображение информации уже выполняется в Spring Boot Admin. Просто говорю на заметку   -  person Thomas Escolan    schedule 21.02.2017
comment
pb найден для клиента обнаружения: ОБЩИЕ org.springframework.cloud.client.discovery.DiscoveryClient не позволяет получать экземпляры с версией, а org.springframework.cloud.client.ServiceInstance не может нести версию (временное решение: используйте метаданные в этом конкретном случае)   -  person Thomas Escolan    schedule 21.02.2017
comment
InstanceInfo встроен в EurekaConfigBasedInstanceInfoProvider # get. Версия InstanceInfo # устарела. InstanceInfo # MetaData заполняется через EurekaInstanceConfig # MetadataMap инициализируется как bean-компонент в EurekaClientConfigServerAutoConfiguration # init   -  person Thomas Escolan    schedule 22.02.2017
comment
Информация о сборке предоставляется, если доступна в компоненте BuildProperties (см. InfoContributorAutoConfiguration # buildInfoContributor)   -  person Thomas Escolan    schedule 22.02.2017


Ответы (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()));
            }
        });
    }
}

Обратите внимание, что если «составлено пунктиром» (то есть «версия-сборки-экземпляра»), ключ метаданных - принудительный регистр Camel.

И это решение, которое я нашел для фильтрации экземпляров службы в соответствии с их версией:

@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 = ОТЛАДКА

eureka.client.filter.enabled = true

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

Пожалуйста, отправляйте в качестве комментариев любые предложения. Спасибо

person Thomas Escolan    schedule 23.02.2017
comment
См. комментарии к альтернативным решениям, чтобы упростить регистрацию части процесса: вы можете добавлять метаданные с простыми свойствами. - person Thomas Escolan; 02.03.2017

Служба 1 регистрирует v1 и v2 в Eureka.

Служба 2 обнаруживает и отправляет запросы к службе 1 v1 и v2, используя разные клиенты Ribbon.

Я заставил эту демонстрацию работать и напишу об этом в ближайшие пару дней.

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 имя клиента совпадает с Ribbon ключом в application.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;
    }
}
person ootero    schedule 27.02.2017
comment
Большое спасибо, я внимательно посмотрю. Но, похоже, это не относится к моему варианту использования: какая польза от службы (1), чтобы объявлять / публиковать несколько версий для себя? Вероятно, это нарушает SRP. Мне нужно было развернуть две автономные версии Службы 1, а затем отфильтровать неподдерживаемые версии со стороны клиента (Служба 2). Кроме того, мои решения позволяют избежать манипулирования несколькими клиентами HTTP (ленты). - person Thomas Escolan; 01.03.2017
comment
Между прочим, вы получили очень хороший отзыв о предложении свойства eureka.instance для предоставления метаданных (я его пропустил); поэтому мой класс EurekaClientInstanceBuildVersionAutoConfiguration можно заменить на eureka.instance.metadata-map.instanceBuildVersion = @ pom.version @ в моих примерах. Спасибо - person Thomas Escolan; 01.03.2017
comment
какой смысл сервису (1) объявлять / публиковать несколько версий для себя? Я считаю, что API-интерфейсы никогда не должны нарушать обратную совместимость, если только они (клиент и службы) не развернуты в строго контролируемой среде, например, существующий клиент никогда не обращается к более новым API. - person ootero; 01.03.2017
comment
Кроме того, мои решения позволяют избежать манипулирования несколькими клиентами HTTP (ленты). Это было всего лишь демонстрацией того, как это сделать. В реальном сценарии обновление клиентов, которые будут отправлять запросы к API v1, может быть медленным, в то время как другие клиенты могут использовать API v2. Если основной бизнес медленно обновляемого клиента напрямую не связан с тем, что предлагают API v1, v2, им могут потребоваться годы для обновления, подумайте о картах Google v2 и v3. Я твердо уверен, что заставлять клиентов обновляться в мгновение ока - плохая практика. - person ootero; 01.03.2017
comment
Большое спасибо за просмотр, @ootero. Я считаю, что нет необходимости в обслуживании двух последовательных API в одном экземпляре. Использование полосы пропускания может быть разным для разных версий, и поэтому вы не сможете масштабировать их по отдельности. Я больше думал о нескольких экземплярах каждой доступной версии, уменьшая количество экземпляров v1, поскольку для внедрения v2 потребовались бы более современные экземпляры служб. В этом подходе нет перерывов, нет необходимости отключать службу, поскольку балансировка нагрузки допускает переход. - person Thomas Escolan; 02.03.2017
comment
Уменьшение использования v1 и внедрение новой v2 могут занять годы, подумайте о Google Maps v2. Что делать, если в v1 обнаружена ошибка? Были бы у вас теперь два репозитория исходного кода? один для xxxx-service-v1 и xxxx-service-v2 и передает всю сложность DevOps (репозиторий исходного кода, конвейеры CICD, QA, Staging, ... среды? Несколько двоичных файлов из одного репо? Это не то, что приложение 12 факторов манифест предлагает. - person ootero; 07.11.2017

Это уловка для взлома Eureka Dashboard. Добавьте этот аспект 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 с аргументом виртуальной машины:

-javaagent:D:\.m2\repository\org\aspectj\aspectjweaver\1.8.9\aspectjweaver-1.8.9.jar
person Thomas Escolan    schedule 27.02.2017