创建一个自定义Spring Cloud Netflix Ribbon客户端 [英] Create a Custom Spring Cloud Netflix Ribbon Client

查看:119
本文介绍了创建一个自定义Spring Cloud Netflix Ribbon客户端的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在Cloud Foundry环境中结合使用Spring Cloud Netflix Ribbon和Eureka.

I am using Spring Cloud Netflix Ribbon in combination with Eureka in a Cloud Foundry environment.

我要实现的用例如下:

  • 我有一个名为address-service的正在运行的CF应用程序,其中产生了多个实例.

  • I have a running CF application named address-service with several instances spawned.

实例正在通过服务名称address-service

The instances are registering to Eureka by the service name address-service

我已使用
将自定义元数据添加到服务实例 eureka.instance.metadata-map.applicationId: ${vcap.application.application_id}

I have added custom metadata to service instances using
eureka.instance.metadata-map.applicationId: ${vcap.application.application_id}

我想使用Eureka的InstanceInfo中的信息(特别是元数据和可用的服务实例数)来设置CF HTTP标头"X-CF-APP-INSTANCE",如此处.

I want to use the information in Eureka's InstanceInfo (in particular the metadata and how many service instances are available) for setting a CF HTTP header "X-CF-APP-INSTANCE" as described here.

这个想法是发送一个像"X-CF-APP-INSTANCE":"appIdFromMetadata:instanceIndexCalculatedFromNoOfServiceInstances"这样的Header,从而在涉及到负载均衡时否决" CF的Go-Router,如

The idea is to send a Header like "X-CF-APP-INSTANCE":"appIdFromMetadata:instanceIndexCalculatedFromNoOfServiceInstances" and thus "overrule" CF's Go-Router when it comes to load balancing as described at the bottom of this issue.

我相信要设置标题,我需要创建一个自定义的 RibbonClient 实现-即以Netflix的普通术语来说,是 AbstractLoadBalancerAwareClient 的子类,如

I believe to set headers, I need to create a custom RibbonClient implementation - i.e. in plain Netflix terms a subclass of AbstractLoadBalancerAwareClient as described here - and override the execute() methods.

但是,这不起作用,因为Spring Cloud Netflix Ribbon无法从application.yml读取我的CustomRibbonClient的类名.似乎Spring Cloud Netflix在普通的Netflix内容周围包装了很多类.

However, this does not work, as Spring Cloud Netflix Ribbon won't read the class name of my CustomRibbonClient from application.yml. It also seems Spring Cloud Netflix wraps quite a bit of classes around the plain Netflix stuff.

我尝试实现RetryableRibbonLoadBalancingHttpClientRibbonLoadBalancingHttpClient的子类,它们是Spring类.我尝试使用ribbon.ClientClassNameapplication.yml中给它们的类名,但这是行不通的.我试图覆盖Spring Cloud HttpClientRibbonConfiguration中定义的bean,但无法使其正常工作.

I tried implementing a subclass of RetryableRibbonLoadBalancingHttpClient and RibbonLoadBalancingHttpClient which are Spring classes. I tried giving their class names in application.yml using ribbon.ClientClassName but that does not work. I tried to override beans defined in Spring Cloud's HttpClientRibbonConfiguration but I cannot get it to work.

所以我有两个问题:

  1. 我的假设是正确的,我需要创建自定义功能区 Client ,并且bean定义了

  1. is my assumption correct that I need to create a custom Ribbon Client and that the beans defined here and here won't do the trick?

如何正确执行?

任何想法都将不胜感激,所以在此先感谢!

Any ideas are greatly appreciated, so thanks in advance!

Update-1

我对此进行了更多研究,发现

I have dug into this some more and found RibbonAutoConfiguration.

这将创建一个

This creates a SpringClientFactory which provides a getClient() method that is only used in RibbonClientHttpRequestFactory (also declared in RibbonAutoConfiguration).

不幸的是,RibbonClientHttpRequestFactory将客户端硬编码到Netflix RestClient.而且似乎无法覆盖SpringClientFactoryRibbonClientHttpRequestFactory豆.

Unfortunately, RibbonClientHttpRequestFactory hard-codes the client to Netflix RestClient. And it does not seem possible to override either SpringClientFactory nor RibbonClientHttpRequestFactory beans.

我想知道这是否完全可能.

I wonder if this is possible at all.

推荐答案

好,我会自己回答这个问题,以防将来有人需要.
实际上,我终于设法实现了.

Ok, I'll answer this question myself, in case someone else may need that in the future.
Actually, I finally managed to implement it.

TLDR -解决方案在这里: https://github.com/TheFonz2017/Spring-Cloud-Netflix-Ribbon-CF-Routing

TLDR - the solution is here: https://github.com/TheFonz2017/Spring-Cloud-Netflix-Ribbon-CF-Routing

解决方案:

  • 允许在Cloud Foundry上使用Ribbon,从而覆盖Go-Router的负载平衡.
  • 在功能区负载平衡请求(包括重试)中添加自定义路由头,以指示CF的Go-Router将请求路由到功能区(而不是由其自己的负载平衡器)选择的服务实例.
  • 显示如何拦截负载平衡请求

了解这一点的关键是Spring Cloud有自己的LoadBalancer框架,Ribbon只是其中一种可能的实现.同样重要的是要理解,Ribbon仅用作负载平衡器,而不能用作HTTP客户端.换句话说,Ribbon的ILoadBalancer实例仅用于从服务器列表中选择服务实例.对选定服务器实例的请求是通过Spring Cloud AbstractLoadBalancingClient的实现来完成的.使用功能区时,它们是RibbonLoadBalancingHttpClientRetryableRibbonLoadBalancingHttpClient的子类.

The key to understanding this, is that Spring Cloud has its own LoadBalancer framework, for which Ribbon is just one possible implementation. It is also important to understand, that Ribbon is only used as a load balancer not as an HTTP client. In other words, Ribbon's ILoadBalancer instance is only used to select the service instance from the server list. Requests to the selected server instances are done by an implementation of Spring Cloud's AbstractLoadBalancingClient. When using Ribbon, these are sub-classes of RibbonLoadBalancingHttpClient and RetryableRibbonLoadBalancingHttpClient.

因此,我最初将HTTP标头添加到Ribbon的HTTP客户端发送的请求的最初方法没有成功,因为Spring Cloud实际上根本没有使用Ribbon的HTTP/Rest客户端.

So, my initial approach to add an HTTP header to the requests sent by Ribbon's HTTP client did not succeed, since Ribbon's HTTP / Rest client is actually not used by Spring Cloud at all.

解决方案是实现一个Spring Cloud LoadBalancerRequestTransformer,它(与其名称相反)是一个请求拦截器.

The solution is to implement a Spring Cloud LoadBalancerRequestTransformer which (contrary to its name) is a request interceptor.

我的解决方案使用以下实现:

My solution uses the following implementation:

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实例路由标头.

The class is looking for the information required for setting the CF App Instance Routing header in the service instance metadata returned by Eureka.

该信息是

  • 实现服务的CF应用程序的GUID,其中有几个实例用于负载平衡.
  • 应将请求路由到的服务/应用程序实例的索引.

您需要像这样在服务application.yml中提供该信息:

You need to provide that in the application.yml of your service like so:

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实现:

Finally, you need to register the LoadBalancerRequestTransformer implementation in the Spring configuration of your service consumers (which use Ribbon under the hood):

@Bean
public LoadBalancerRequestTransformer customRequestTransformer() {
  return new CFLoadBalancerRequestTransformer();
}

因此,如果在服务使用者中使用@LoadBalanced RestTemplate,则模板将调用Ribbon在服务实例上进行选择以将请求发送至,将发送请求,并且拦截器将注入路由标头. Go-Router会将请求路由到路由头中指定的确切实例,并且不执行任何其他会影响Ribbon功能区选择的负载平衡. 如果需要重试(针对相同或一个或多个下一个实例),则拦截器将再次注入相应的路由头-这次是针对功能区选择的可能不同的服务实例. 这使您可以有效地将Ribbon用作负载平衡器,并事实上禁用Go-Router的负载平衡,从而将其降级为纯代理.好处是,Ribbon是可以(以编程方式)影响的东西,而对Go-Router几乎没有影响.

As a result, if you use a @LoadBalanced RestTemplate in your service consumer, the template will call Ribbon to make a choice on the service instance to send the request to, will send the request and the interceptor will inject the routing header. Go-Router will route the request to the exact instance that was specified in the routing header and not perform any additional load balancing that would interfere with Ribbon's choice. In case a retry were necessary (against the same or one or more next instances), the interceptor would again inject the according routing header - this time for a potentially different service instance selected by Ribbon. This allows you to use Ribbon effectively as the load balancer and de-facto disable load balancing of Go-Router, demoting it to a mere proxy. The benefit being that Ribbon is something you can influence (programmatically) whereas you have little to no influence over Go-Router.

注意:这已经针对@LoadBalanced RestTemplate进行了测试. 但是,对于@FeignClient来说,它不是这种方式. 在 this中,我为Feign解决这个问题的方法最接近,但是在那里描述的解决方案使用了一个拦截器,该拦截器无法访问(Ribbon)选择的服务实例,因此不允许访问所需的元数据.
到目前为止,尚未找到FeignClient的解决方案.

Note: this was tested for @LoadBalanced RestTemplate's and works. However, for @FeignClients it does not work this way. The closest I have come to solving this for Feign is described in this post, however, the solution described there uses an interceptor that does not get access to the (Ribbon-)selected service instance, thus not allowing access to the required metadata.
Haven't found a solution so far for FeignClient.

这篇关于创建一个自定义Spring Cloud Netflix Ribbon客户端的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆