创建自定义 Spring Cloud Netflix 功能区客户端
Create a Custom Spring Cloud Netflix Ribbon Client
我在 Cloud Foundry 环境中结合使用 Spring Cloud Netflix Ribbon 和 Eureka。
我尝试实现的用例如下:
我有一个名为 address-service
的 运行 CF 应用程序,其中生成了多个实例。
实例正在通过服务名称address-service
注册到 Eureka
我已经使用
向服务实例添加了自定义元数据
eureka.instance.metadata-map.applicationId: ${vcap.application.application_id}
我想使用 Eureka 的 InstanceInfo
中的信息(特别是元数据和有多少服务实例可用)来设置 CF HTTP header "X-CF-APP-INSTANCE" 描述 here.
想法是发送一个 Header 像 "X-CF-APP-INSTANCE":"appIdFromMetadata:instanceIndexCalculatedFromNoOfServiceInstances"
这样 "overrule" CF 的 Go-Router 当涉及负载平衡时 at the bottom of this issue.
我相信要设置 headers,我需要创建一个自定义 RibbonClient 实现 - 即在普通的 Netflix 术语中 [= 的子 class 77=]AbstractLoadBalancerAwareClient 如所述 here - 并覆盖 execute()
方法。
但是,这不起作用,因为 Spring Cloud Netflix 功能区不会从 application.yml
读取我的 CustomRibbonClient
的 class 名称。 Spring Cloud Netflix 似乎也围绕着普通的 Netflix 内容包装了相当多的 classes。
我尝试实现 RetryableRibbonLoadBalancingHttpClient
和 RibbonLoadBalancingHttpClient
的子 class,它们是 Spring classes。我尝试使用 ribbon.ClientClassName
在 application.yml
中给出他们的 class 名称,但这不起作用。我试图覆盖 Spring Cloud 的 HttpClientRibbonConfiguration
中定义的 beans,但我无法让它工作。
所以我有两个问题:
非常感谢任何想法,提前致谢!
Update-1
我对此进行了深入研究,发现 RibbonAutoConfiguration。
这将创建一个 SpringClientFactory which provides a getClient()
method that is only used in RibbonClientHttpRequestFactory
(也在 RibbonAutoConfiguration
中声明)。
不幸的是,RibbonClientHttpRequestFactory
hard-codes Netflix RestClient
的客户端。而且似乎不可能覆盖 SpringClientFactory
或 RibbonClientHttpRequestFactory
bean。
我想知道这是否可能。
好的,我会自己回答这个问题,以防将来其他人可能需要。
其实我终于实现了。
TLDR - 解决方案在这里:https://github.com/TheFonz2017/Spring-Cloud-Netflix-Ribbon-CF-Routing
解决方法:
- 允许在 Cloud Foundry 上使用 Ribbon,覆盖 Go-Router 的负载平衡。
- 向 Ribbon 负载平衡请求(包括重试)添加自定义路由 header 以指示 CF Go-Router 将请求路由到由 Ribbon select 编辑的服务实例(而不是它自己的负载平衡器)。
- 显示如何拦截负载平衡请求
理解这一点的关键是,Spring Cloud 有自己的 LoadBalancer
框架,Ribbon 只是其中一种可能的实现。同样重要的是要理解,Ribbon 仅用作负载平衡器 而不是 作为 HTTP 客户端。也就是说,Ribbon的ILoadBalancer
实例只用于select服务器列表中的服务实例。对 selected 服务器实例的请求是由 Spring 云的 AbstractLoadBalancingClient
的实现完成的。使用功能区时,这些是 RibbonLoadBalancingHttpClient
和 RetryableRibbonLoadBalancingHttpClient
中的 sub-classes。
因此,我最初向 Ribbon 的 HTTP 客户端发送的请求添加 HTTP header 的方法没有成功,因为 Spring Cloud 实际上根本不使用 Ribbon 的 HTTP / Rest 客户端.
解决方案是实现一个 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();
}
}
}
class在Eureka返回的服务实例元数据中寻找设置CF App Instance Routingheader所需的信息
该信息是
- 实现服务的 CF 应用程序的 GUID,其中有多个实例用于负载平衡。
- 请求应路由到的服务/应用程序实例的索引。
您需要在 服务 的 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
最后,您需要在 服务消费者 (在底层使用 Ribbon)的 Spring 配置中注册 LoadBalancerRequestTransformer
实现:
@Bean
public LoadBalancerRequestTransformer customRequestTransformer() {
return new CFLoadBalancerRequestTransformer();
}
因此,如果您在服务使用者中使用 @LoadBalanced RestTemplate
,模板将调用 Ribbon 来选择要将请求发送到的服务实例,将发送请求,拦截器将注入路由 header。 Go-Router 会将请求路由到路由 header 中指定的确切实例,并且不会执行任何会干扰 Ribbon 选择的额外负载平衡。
如果需要重试(针对相同或一个或多个下一个实例),拦截器将再次注入相应的路由 header - 这次是针对由 Ribbon 编辑的可能不同的服务实例 select。
这允许您有效地使用 Ribbon 作为负载平衡器,并且 de-facto 禁用 Go-Router 的负载平衡,将其降级为纯粹的代理。好处是功能区是您可以(以编程方式)影响的东西,而您对 Go-Router.
几乎没有影响
注意:这已针对 @LoadBalanced RestTemplate
进行了测试并且有效。
但是,对于 @FeignClient
s 来说,它不会以这种方式工作。
this post 中描述了我为 Feign 解决这个问题的最接近方法,但是,那里描述的解决方案使用了一个无法访问 (Ribbon-)selected 服务实例的拦截器,因此不是允许访问所需的元数据。
目前还没有找到 FeignClient
.
的解决方案
我在 Cloud Foundry 环境中结合使用 Spring Cloud Netflix Ribbon 和 Eureka。
我尝试实现的用例如下:
我有一个名为
address-service
的 运行 CF 应用程序,其中生成了多个实例。实例正在通过服务名称
address-service
注册到 Eureka
我已经使用
向服务实例添加了自定义元数据eureka.instance.metadata-map.applicationId: ${vcap.application.application_id}
我想使用 Eureka 的
InstanceInfo
中的信息(特别是元数据和有多少服务实例可用)来设置 CF HTTP header "X-CF-APP-INSTANCE" 描述 here.想法是发送一个 Header 像
"X-CF-APP-INSTANCE":"appIdFromMetadata:instanceIndexCalculatedFromNoOfServiceInstances"
这样 "overrule" CF 的 Go-Router 当涉及负载平衡时 at the bottom of this issue.
我相信要设置 headers,我需要创建一个自定义 RibbonClient 实现 - 即在普通的 Netflix 术语中 [= 的子 class 77=]AbstractLoadBalancerAwareClient 如所述 here - 并覆盖 execute()
方法。
但是,这不起作用,因为 Spring Cloud Netflix 功能区不会从 application.yml
读取我的 CustomRibbonClient
的 class 名称。 Spring Cloud Netflix 似乎也围绕着普通的 Netflix 内容包装了相当多的 classes。
我尝试实现 RetryableRibbonLoadBalancingHttpClient
和 RibbonLoadBalancingHttpClient
的子 class,它们是 Spring classes。我尝试使用 ribbon.ClientClassName
在 application.yml
中给出他们的 class 名称,但这不起作用。我试图覆盖 Spring Cloud 的 HttpClientRibbonConfiguration
中定义的 beans,但我无法让它工作。
所以我有两个问题:
非常感谢任何想法,提前致谢!
Update-1
我对此进行了深入研究,发现 RibbonAutoConfiguration。
这将创建一个 SpringClientFactory which provides a getClient()
method that is only used in RibbonClientHttpRequestFactory
(也在 RibbonAutoConfiguration
中声明)。
不幸的是,RibbonClientHttpRequestFactory
hard-codes Netflix RestClient
的客户端。而且似乎不可能覆盖 SpringClientFactory
或 RibbonClientHttpRequestFactory
bean。
我想知道这是否可能。
好的,我会自己回答这个问题,以防将来其他人可能需要。
其实我终于实现了。
TLDR - 解决方案在这里:https://github.com/TheFonz2017/Spring-Cloud-Netflix-Ribbon-CF-Routing
解决方法:
- 允许在 Cloud Foundry 上使用 Ribbon,覆盖 Go-Router 的负载平衡。
- 向 Ribbon 负载平衡请求(包括重试)添加自定义路由 header 以指示 CF Go-Router 将请求路由到由 Ribbon select 编辑的服务实例(而不是它自己的负载平衡器)。
- 显示如何拦截负载平衡请求
理解这一点的关键是,Spring Cloud 有自己的 LoadBalancer
框架,Ribbon 只是其中一种可能的实现。同样重要的是要理解,Ribbon 仅用作负载平衡器 而不是 作为 HTTP 客户端。也就是说,Ribbon的ILoadBalancer
实例只用于select服务器列表中的服务实例。对 selected 服务器实例的请求是由 Spring 云的 AbstractLoadBalancingClient
的实现完成的。使用功能区时,这些是 RibbonLoadBalancingHttpClient
和 RetryableRibbonLoadBalancingHttpClient
中的 sub-classes。
因此,我最初向 Ribbon 的 HTTP 客户端发送的请求添加 HTTP header 的方法没有成功,因为 Spring Cloud 实际上根本不使用 Ribbon 的 HTTP / Rest 客户端.
解决方案是实现一个 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();
}
}
}
class在Eureka返回的服务实例元数据中寻找设置CF App Instance Routingheader所需的信息
该信息是
- 实现服务的 CF 应用程序的 GUID,其中有多个实例用于负载平衡。
- 请求应路由到的服务/应用程序实例的索引。
您需要在 服务 的 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
最后,您需要在 服务消费者 (在底层使用 Ribbon)的 Spring 配置中注册 LoadBalancerRequestTransformer
实现:
@Bean
public LoadBalancerRequestTransformer customRequestTransformer() {
return new CFLoadBalancerRequestTransformer();
}
因此,如果您在服务使用者中使用 @LoadBalanced RestTemplate
,模板将调用 Ribbon 来选择要将请求发送到的服务实例,将发送请求,拦截器将注入路由 header。 Go-Router 会将请求路由到路由 header 中指定的确切实例,并且不会执行任何会干扰 Ribbon 选择的额外负载平衡。
如果需要重试(针对相同或一个或多个下一个实例),拦截器将再次注入相应的路由 header - 这次是针对由 Ribbon 编辑的可能不同的服务实例 select。
这允许您有效地使用 Ribbon 作为负载平衡器,并且 de-facto 禁用 Go-Router 的负载平衡,将其降级为纯粹的代理。好处是功能区是您可以(以编程方式)影响的东西,而您对 Go-Router.
注意:这已针对 @LoadBalanced RestTemplate
进行了测试并且有效。
但是,对于 @FeignClient
s 来说,它不会以这种方式工作。
this post 中描述了我为 Feign 解决这个问题的最接近方法,但是,那里描述的解决方案使用了一个无法访问 (Ribbon-)selected 服务实例的拦截器,因此不是允许访问所需的元数据。
目前还没有找到 FeignClient
.