Создание собственного клиента ленты Netflix Spring Spring Cloud - PullRequest
0 голосов
/ 13 марта 2019

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

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

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

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

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

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

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

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

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

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

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

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

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

Любые идеи очень ценятся, поэтому заранее спасибо!

Update-1

Я углубился в это и обнаружил RibbonAutoConfiguration .

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

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

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

1 Ответ

0 голосов
/ 18 марта 2019

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

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

Решение:

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

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

Итак, мой первоначальный подход к добавлению заголовка HTTP к запросам, отправляемым HTTP-клиентом Ribbon, не увенчался успехом, поскольку Spring-клиент 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 направит запрос к точному экземпляру, указанному в заголовке маршрутизации, и не будет выполнять какую-либо дополнительную балансировку нагрузки, которая мешала бы выбору ленты. В случае необходимости повторной попытки (для того же или одного или нескольких следующих экземпляров) перехватчик снова вводит соответствующий заголовок маршрутизации - на этот раз для потенциально другого экземпляра службы, выбранного с помощью ленты. Это позволяет эффективно использовать ленту в качестве балансировщика нагрузки и де-факто отключить балансировку нагрузки Go-Router, превратив ее в простой прокси. Преимущество заключается в том, что лента - это то, на что вы можете влиять (программно), тогда как у вас мало или нет влияния на Go-Router.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...