我在Cloud Foundry环境中将Spring Cloud Netflix Ribbon与Eureka结合使用。
我试图实现的用例如下:
address-service
的正在运行的CF应用程序,其中有几个实例已经生成。address-service
注册到Eurekaeureka.instance.metadata-map.applicationId: ${vcap.application.application_id}
InstanceInfo
中的信息(特别是元数据和可用的服务实例数)来设置CF HTTP头“X-CF-APP-INSTANCE”,如here所述。"X-CF-APP-INSTANCE":"appIdFromMetadata:instanceIndexCalculatedFromNoOfServiceInstances"
这样的Header,从而“覆盖”CF的Go-Router,就at the bottom of this issue所描述的负载平衡而言。我相信设置标题,我需要创建一个自定义的RibbonClient实现 - 即在简单的Netflix术语中描述here描述的AbstractLoadBalancerAwareClient的子类 - 并覆盖execute()
方法。
但是,这不起作用,因为Spring Cloud Netflix Ribbon不会从CustomRibbonClient
读取我的application.yml
的类名。似乎Spring Cloud Netflix围绕着简单的Netflix内容包含了相当多的类。
我尝试实现了一个RetryableRibbonLoadBalancingHttpClient
和RibbonLoadBalancingHttpClient
的子类,它们是Spring类。我尝试使用application.yml
在ribbon.ClientClassName
中给出他们的类名,但这不起作用。我试图覆盖Spring Cloud的HttpClientRibbonConfiguration
中定义的bean,但我无法让它工作。
所以我有两个问题:
非常感谢任何想法,所以提前感谢!
更新1
我已经挖了一些,发现了RibbonAutoConfiguration。
这创建了一个SpringClientFactory,它提供了getClient()
方法,该方法仅用于RibbonClientHttpRequestFactory
(也在RibbonAutoConfiguration
中声明)。
不幸的是,RibbonClientHttpRequestFactory
将客户硬编码为Netflix RestClient
。而且似乎无法覆盖SpringClientFactory
和RibbonClientHttpRequestFactory
bean。
我想知道这是否可行。
好的,我会自己回答这个问题,万一其他人可能会在将来需要这个问题。 实际上,我终于设法实现了它。
TLDR - 解决方案在这里:https://github.com/TheFonz2017/Spring-Cloud-Netflix-Ribbon-CF-Routing
解决方案:
理解这一点的关键是Spring Cloud有自己的LoadBalancer
框架,Ribbon只是一种可能的实现。同样重要的是要理解,Ribbon仅用作负载均衡器而不是HTTP客户端。换句话说,Ribbon的ILoadBalancer
实例仅用于从服务器列表中选择服务实例。对所选服务器实例的请求由Spring Cloud的AbstractLoadBalancingClient
实现完成。使用Ribbon时,这些是RibbonLoadBalancingHttpClient
和RetryableRibbonLoadBalancingHttpClient
的子类。
因此,我最初为Ribbon的HTTP客户端发送的请求添加HTTP标头的方法没有成功,因为Ribbon的HTTP / Rest客户端实际上根本不被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();
}
}
}
该类正在寻找在Eureka返回的服务实例元数据中设置CF App Instance Routing头所需的信息。
那个信息是
您需要在服务的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
最后,您需要在服务使用者的Spring配置中注册LoadBalancerRequestTransformer
实现(使用功能区内的Ribbon):
@Bean
public LoadBalancerRequestTransformer customRequestTransformer() {
return new CFLoadBalancerRequestTransformer();
}
因此,如果在服务使用者中使用@LoadBalanced RestTemplate
,模板将调用Ribbon以在服务实例上做出选择以发送请求,将发送请求并且拦截器将注入路由头。 Go-Router将请求路由到路由头中指定的确切实例,而不执行任何会干扰Ribbon选择的额外负载平衡。如果需要重试(针对相同或一个或多个下一个实例),拦截器将再次注入相应的路由头 - 这次是由Ribbon选择的可能不同的服务实例。这允许您有效地使用Ribbon作为负载均衡器,并且事实上禁用Go-Router的负载平衡,将其降级为单纯的代理。好处是Ribbon可以影响(以编程方式),而对Go-Router几乎没有影响。
注意:这是针对@LoadBalanced RestTemplate
的测试和工作。但是,对于@FeignClient
s来说,这种方式不起作用。我在this post中描述了最接近解决此问题的方法,但是,其中描述的解决方案使用的拦截器无法访问(Ribbon-)所选服务实例,因此不允许访问所需的元数据。
到目前为止还没有为FeignClient
找到解决方案。