Создайте собственный ленточный клиент Spring Cloud Netflix

Я использую ленту Spring Cloud Netflix в сочетании с Eureka в среде Cloud Foundry.

Вариант использования, который я пытаюсь реализовать, следующий:

  • У меня есть запущенное приложение CF с именем address-service, в котором создано несколько экземпляров.

  • Экземпляры регистрируются в Eureka по имени службы address-service

  • Я добавил пользовательские метаданные в экземпляры службы, используя
    eureka.instance.metadata-map.applicationId: ${vcap.application.application_id}

  • Я хочу использовать информацию в Eureka InstanceInfo (в частности, метаданные и количество доступных экземпляров службы) для установки заголовка CF HTTP «X-CF-APP-INSTANCE», как описано здесь.

  • Идея состоит в том, чтобы отправить заголовок, подобный "X-CF-APP-INSTANCE":"appIdFromMetadata:instanceIndexCalculatedFromNoOfServiceInstances", и, таким образом, «отменить» Go-Router CF, когда дело доходит до балансировки нагрузки, как описано в нижней части этого вопроса.

Я считаю, что для установки заголовков мне нужно создать пользовательскую реализацию ЛентаКлиент — т. е. в простых терминах Netflix подкласс AbstractLoadBalancerAwareClient, как описано здесь — и переопределить методы execute().

Однако это не работает, так как лента Spring Cloud Netflix не будет читать имя класса моего CustomRibbonClient из application.yml. Также кажется, что Spring Cloud Netflix оборачивает довольно много классов вокруг простых вещей Netflix.

Я попытался реализовать подкласс RetryableRibbonLoadBalancingHttpClient и RibbonLoadBalancingHttpClient, которые являются классами Spring. Я пытался указать имена их классов в application.yml, используя ribbon.ClientClassName, но это не сработало. Я пытался переопределить bean-компоненты, определенные в Spring Cloud HttpClientRibbonConfiguration, но не могу заставить его работать.

Итак, у меня есть два вопроса:

  1. правильно ли мое предположение, что мне нужно создать пользовательскую ленту Клиент и что bean-компоненты, определенные здесь и здесь, не помогут?

  2. Как это сделать правильно?

Любые идеи приветствуются, так что заранее спасибо!

Обновление-1

Я еще немного покопался в этом и нашел ЛентаАвтоконфигурация.

Это создает SpringClientFactory, который предоставляет метод getClient(), который используется только в RibbonClientHttpRequestFactory (также объявленный в RibbonAutoConfiguration).

К сожалению, RibbonClientHttpRequestFactory жестко кодирует клиент для Netflix RestClient. И не представляется возможным переопределить ни SpringClientFactory, ни RibbonClientHttpRequestFactory бобы.

Интересно, возможно ли это вообще?

@spencergibb Я действительно надеюсь, что вы сможете пролить свет на это. Я прочитал довольно много ваших статей на Github и StackOverflow, и я думаю, что это что-то для вас. :)

FloW 13.03.2019 22:07

Вы должны опубликовать код. Переопределение bean-компонентов в конфигурации SpringBoot должно работать.

Strelok 13.03.2019 23:09

Я обновил пост. Я не могу публиковать код для вещей, для которых я не знаю, как он должен работать. К сожалению, это нигде не описано, и я искал решение по всему Интернету.

FloW 13.03.2019 23:56

Мы не рекомендуем использовать клиент ленточной паузы, а только балансировщик нагрузки. Вместо этого мы рекомендуем RestTemplate или WebClient

spencergibb 14.03.2019 01:40

@spencergibb Спасибо. На самом деле я использую @LoadBalanced RestTemplates и FeignClients, под которыми скрыта лента. Лента не видна снаружи, но это информация, полученная с ленты, которая мне нужна (в основном, Эврика InstanceInfo), чтобы установить заголовки HTTP. Как это сделать с помощью RestTemplates, FeignClient или WebClient? У вас есть информация о внутренней проводке с Ribbon?

FloW 14.03.2019 07:56

Я думаю, в основном мой вопрос сводится к следующему: как я могу внедрить экземпляры ILoadBalancer, IPing, ServerList<Server> и т. д., созданные для определенного клиента ленты, в мое приложение? В документации @RibbonClient говорится, что вы должны внедрить SpringClientFactory и получить оттуда LoadBalancer. Тем не менее, этот класс не имеет явного доступа к таким вещам, как IPing, кроме вызова getInstance(name, IPing.class). Это действительно правильный путь?

FloW 14.03.2019 10:33

@spencergibb Я создал пример проекта, который пробует его - я считаю, что это так, как вы предложили. Посмотреть здесь. Не могли бы вы сообщить мне, является ли это правильным способом «внедрить» LoadBalancer и другие компоненты в свое приложение? Также обратите внимание на раздел Проблема в файле readme, так как эта маршрутизация «на уровне приложения» не является моей целью. Я бы предпочел повторить запросы, сделанные в результате обнаружения сбоев ленты, также получить обновленные заголовки CF. Есть предположения?

FloW 14.03.2019 14:31
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Версия Java на основе версии загрузки
Версия Java на основе версии загрузки
Если вы зайдете на официальный сайт Spring Boot , там представлен start.spring.io , который упрощает создание проектов Spring Boot, как показано ниже.
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
1
7
1 581
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Хорошо, я сам отвечу на этот вопрос, вдруг кому-то это понадобится в будущем.
Собственно, наконец-то мне удалось это реализовать.

TLDR - решение здесь: https://github.com/TheFonz2017/Spring-Cloud-Netflix-Ribbon-CF-Routing

Решение:

  • Позволяет использовать Ribbon в Cloud Foundry, переопределяя балансировку нагрузки Go-Router.
  • Добавляет настраиваемый заголовок маршрутизации в запросы балансировки нагрузки ленты (включая повторные попытки), чтобы указать Go-Router CF направлять запросы экземпляру службы, выбранному лентой (а не его собственному балансировщику нагрузки).
  • Показывает, как перехватывать запросы балансировки нагрузки

Ключом к пониманию этого является то, что у Spring Cloud есть собственная LoadBalancer структура, для которой Ribbon является лишь одной из возможных реализаций. Также важно понимать, что Ribbon используется только как балансировщик нагрузки нет как HTTP-клиент. Другими словами, экземпляр ленты ILoadBalancer используется только для Выбрать экземпляра службы из списка серверов. Запросы к выбранным экземплярам сервера выполняются реализацией Spring Cloud AbstractLoadBalancingClient. При использовании Ribbon это подклассы RibbonLoadBalancingHttpClient и RetryableRibbonLoadBalancingHttpClient.

Итак, мой первоначальный подход к добавлению HTTP-заголовка к запросам, отправляемым HTTP-клиентом Ribbon, не увенчался успехом, поскольку HTTP/Rest-клиент Ribbon фактически вообще не используется Spring Cloud.

Решение состоит в том, чтобы реализовать Spring Cloud LoadBalancerRequestTransformer, который (вопреки его названию) является перехватчиком запросов.

В моем решении используется следующая реализация:

public class CFLoadBalancerRequestTransformer implements LoadBalancerRequestTransformer {
    public static final String CF_APP_GUID = "cfAppGuid";
    public static final String CF_INSTANCE_INDEX = "cfInstanceIndex";
    public static final String ROUTING_HEADER = "X-CF-APP-INSTANCE";

    @Override
    public HttpRequest transformRequest(HttpRequest request, ServiceInstance instance) {

        System.out.println("Transforming Request from LoadBalancer Ribbon).");

        // First: Get the service instance information from the lower Ribbon layer.
        //        This will include the actual service instance information as returned by Eureka. 
        RibbonLoadBalancerClient.RibbonServer serviceInstanceFromRibbonLoadBalancer = (RibbonLoadBalancerClient.RibbonServer) instance;

        // Second: Get the the service instance from Eureka, which is encapsulated inside the Ribbon service instance wrapper.
        DiscoveryEnabledServer serviceInstanceFromEurekaClient = (DiscoveryEnabledServer) serviceInstanceFromRibbonLoadBalancer.getServer();

        // Finally: Get access to all the cool information that Eureka provides about the service instance (including metadata and much more).
        //          All of this is available for transforming the request now, if necessary.
        InstanceInfo instanceInfo = serviceInstanceFromEurekaClient.getInstanceInfo();

        // If it's only the instance metadata you are interested in, you can also get it without explicitly down-casting as shown above.  
        Map<String, String> metadata = instance.getMetadata();
        System.out.println("Instance: " + instance);

        dumpServiceInstanceInformation(metadata, instanceInfo);

        if (metadata.containsKey(CF_APP_GUID) && metadata.containsKey(CF_INSTANCE_INDEX)) {
            final String headerValue = String.format("%s:%s", metadata.get(CF_APP_GUID), metadata.get(CF_INSTANCE_INDEX));

            System.out.println("Returning Request with Special Routing Header");
            System.out.println("Header Value: " + headerValue);

            // request.getHeaders might be immutable, so we return a wrapper that pretends to be the original request.
            // and that injects an extra header.
            return new CFLoadBalancerHttpRequestWrapper(request, headerValue);
        }

        return request;
    }

    /**
     * Dumps metadata and InstanceInfo as JSON objects on the console.
     * @param metadata the metadata (directly) retrieved from 'ServiceInstance'
     * @param instanceInfo the instance info received from the (downcast) 'DiscoveryEnabledServer' 
     */
    private void dumpServiceInstanceInformation(Map<String, String> metadata, InstanceInfo instanceInfo) {
        ObjectMapper mapper = new ObjectMapper();
        String json;
        try {
            json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(metadata);
            System.err.println("-- Metadata: " );
            System.err.println(json);

            json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(instanceInfo);
            System.err.println("-- InstanceInfo: " );
            System.err.println(json);
        } catch (JsonProcessingException e) {
            System.err.println(e);
        }
    }

    /**
     * Wrapper class for an HttpRequest which may only return an
     * immutable list of headers. The wrapper immitates the original 
     * request and will return the original headers including a custom one
     * added when getHeaders() is called. 
     */
    private class CFLoadBalancerHttpRequestWrapper implements HttpRequest {

        private HttpRequest request;
        private String headerValue;

        CFLoadBalancerHttpRequestWrapper(HttpRequest request, String headerValue) {
            this.request = request;
            this.headerValue = headerValue;
        }

        @Override
        public HttpHeaders getHeaders() {
            HttpHeaders headers = new HttpHeaders();
            headers.putAll(request.getHeaders());
            headers.add(ROUTING_HEADER, headerValue);
            return headers;
        }

        @Override
        public String getMethodValue() {
            return request.getMethodValue();
        }

        @Override
        public URI getURI() {
            return request.getURI();
        }
    }  
}

Класс ищет информацию, необходимую для установки заголовка Маршрутизация экземпляра приложения CF, в метаданных экземпляра службы, возвращаемых Eureka.

Эта информация

  • GUID приложения CF, которое реализует службу и несколько экземпляров которого существуют для балансировки нагрузки.
  • Индекс экземпляра службы/приложения, на который должен быть направлен запрос.

Вам нужно указать это в application.yml вашего оказание услуг следующим образом:

eureka:
  instance: 
    hostname: ${vcap.application.uris[0]:localhost}
    metadata-map:
      # Adding information about the application GUID and app instance index to 
      # each instance metadata. This will be used for setting the X-CF-APP-INSTANCE header
      # to instruct Go-Router where to route.
      cfAppGuid:       ${vcap.application.application_id}
      cfInstanceIndex: ${INSTANCE_INDEX}

  client: 
    serviceUrl:
      defaultZone: https://eureka-server.<your cf domain>/eureka

Наконец, вам нужно зарегистрировать реализацию LoadBalancerRequestTransformer в конфигурации Spring вашего потребители услуг (который использует ленту под капотом):

@Bean
public LoadBalancerRequestTransformer customRequestTransformer() {
  return new CFLoadBalancerRequestTransformer();
}

В результате, если вы используете @LoadBalanced RestTemplate в своем потребителе службы, шаблон вызовет ленту, чтобы сделать выбор экземпляра службы для отправки запроса, отправит запрос, а перехватчик вставит заголовок маршрутизации. Go-Router направит запрос точно в тот экземпляр, который был указан в заголовке маршрутизации, и не будет выполнять какую-либо дополнительную балансировку нагрузки, которая могла бы помешать выбору ленты. В случае необходимости повторной попытки (против того же или одного или нескольких следующих экземпляров) перехватчик снова вставит соответствующий заголовок маршрутизации — на этот раз для потенциально другого экземпляра службы, выбранного лентой. Это позволяет эффективно использовать Ribbon в качестве балансировщика нагрузки и де-факто отключить балансировку нагрузки Go-Router, превратив его в простой прокси-сервер. Преимущество заключается в том, что на Ribbon можно влиять (программно), в то время как на Go-Router у вас практически нет влияния.

Примечание: это было проверено на @LoadBalanced RestTemplate и работает. Однако для @FeignClients это не работает. Ближе всего я подошел к решению этой проблемы для Feign, описанной в эта почта, однако описанное там решение использует перехватчик, который не получает доступ к (ленточному) выбранному экземпляру службы, таким образом не разрешая доступ к требуемым метаданным.
Пока не нашел решения для FeignClient.

Другие вопросы по теме