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