Я использую ленту 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, но не могу заставить его работать.
Итак, у меня есть два вопроса:
правильно ли мое предположение, что мне нужно создать пользовательскую ленту Клиент и что bean-компоненты, определенные здесь и здесь, не помогут?
Как это сделать правильно?
Любые идеи приветствуются, так что заранее спасибо!
Обновление-1
Я еще немного покопался в этом и нашел ЛентаАвтоконфигурация.
Это создает SpringClientFactory, который предоставляет метод getClient(), который используется только в RibbonClientHttpRequestFactory (также объявленный в RibbonAutoConfiguration).
К сожалению, RibbonClientHttpRequestFactory жестко кодирует клиент для Netflix RestClient. И не представляется возможным переопределить ни SpringClientFactory, ни RibbonClientHttpRequestFactory бобы.
Интересно, возможно ли это вообще?
Вы должны опубликовать код. Переопределение bean-компонентов в конфигурации SpringBoot должно работать.
Я обновил пост. Я не могу публиковать код для вещей, для которых я не знаю, как он должен работать. К сожалению, это нигде не описано, и я искал решение по всему Интернету.
Мы не рекомендуем использовать клиент ленточной паузы, а только балансировщик нагрузки. Вместо этого мы рекомендуем RestTemplate или WebClient
@spencergibb Спасибо. На самом деле я использую @LoadBalanced RestTemplates и FeignClients, под которыми скрыта лента. Лента не видна снаружи, но это информация, полученная с ленты, которая мне нужна (в основном, Эврика InstanceInfo), чтобы установить заголовки HTTP. Как это сделать с помощью RestTemplates, FeignClient или WebClient? У вас есть информация о внутренней проводке с Ribbon?
Я думаю, в основном мой вопрос сводится к следующему: как я могу внедрить экземпляры ILoadBalancer, IPing, ServerList<Server> и т. д., созданные для определенного клиента ленты, в мое приложение? В документации @RibbonClient говорится, что вы должны внедрить SpringClientFactory и получить оттуда LoadBalancer. Тем не менее, этот класс не имеет явного доступа к таким вещам, как IPing, кроме вызова getInstance(name, IPing.class). Это действительно правильный путь?
@spencergibb Я создал пример проекта, который пробует его - я считаю, что это так, как вы предложили. Посмотреть здесь. Не могли бы вы сообщить мне, является ли это правильным способом «внедрить» LoadBalancer и другие компоненты в свое приложение? Также обратите внимание на раздел Проблема в файле readme, так как эта маршрутизация «на уровне приложения» не является моей целью. Я бы предпочел повторить запросы, сделанные в результате обнаружения сбоев ленты, также получить обновленные заголовки CF. Есть предположения?




Хорошо, я сам отвечу на этот вопрос, вдруг кому-то это понадобится в будущем.
Собственно, наконец-то мне удалось это реализовать.
TLDR - решение здесь: https://github.com/TheFonz2017/Spring-Cloud-Netflix-Ribbon-CF-Routing
Решение:
Ключом к пониманию этого является то, что у 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.
Эта информация
Вам нужно указать это в 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.
@spencergibb Я действительно надеюсь, что вы сможете пролить свет на это. Я прочитал довольно много ваших статей на Github и StackOverflow, и я думаю, что это что-то для вас. :)