创建自定义Spring Cloud Netflix Ribbon客户客户端

时间:2019-03-13 20:57:45

标签: spring-boot ribbon spring-cloud-netflix netflix

我正在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标头“ X-CF-APP-INSTANCE”,{ {3}}。

  • 该想法是发送"X-CF-APP-INSTANCE":"appIdFromMetadata:instanceIndexCalculatedFromNoOfServiceInstances"之类的标头,从而在here所述的负载平衡方面“否决” CF的Go-Router。

我相信要设置标题,我需要创建一个自定义的 RibbonClient 实现-即以Netflix的普通术语来说,是{strong> AbstractLoadBalancerAwareClient 的子类,如at the bottom of this issue所述-并且覆盖execute()方法。

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

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

所以我有两个问题:

  1. 我的假设是正确的,我需要创建自定义功能区 Client ,并且定义了herehere的bean不会成功吗? / p>

  2. 如何正确执行?

任何想法都会受到赞赏,所以在此先感谢!

Update-1

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

这将创建一个RibbonAutoConfiguration,该SpringClientFactory提供一种getClient()方法,该方法仅在RibbonClientHttpRequestFactory中使用(也在RibbonAutoConfiguration中声明)。

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

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

1 个答案:

答案 0 :(得分:0)

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

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

解决方案:

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

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

因此,我最初将HTTP标头添加到Ribbon的HTTP客户端发送的请求的方法没有成功,因为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();
        }
    }  
}

该类正在Eureka返回的服务实例元数据中寻找设置CF App Instance Routing头所需的信息。

该信息是

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

您需要像这样:在您的服务中提供{}:

application.yml

最后,您需要在服务使用者(他们在后台使用Ribbon)的Spring配置中注册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

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

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