【问题标题】:Create a Custom Spring Cloud Netflix Ribbon Client创建自定义 Spring Cloud Netflix Ribbon 客户端
【发布时间】:2020-02-23 04:01:58
【问题描述】:

我在 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”,如 here 所述.

  • 这个想法是发送一个像"X-CF-APP-INSTANCE":"appIdFromMetadata:instanceIndexCalculatedFromNoOfServiceInstances" 这样的标头,从而在at the bottom of this issue 描述的负载平衡方面“推翻”CF 的Go-Router。

我相信要设置标题,我需要创建一个自定义 RibbonClient 实现 - 即用简单的 Netflix 术语来说,AbstractLoadBalancerAwareClient 的子类,如 here 所述 - 并覆盖execute() 方法。

但是,这不起作用,因为 Spring Cloud Netflix Ribbon 不会从 application.yml 读取我的 CustomRibbonClient 的类名。 Spring Cloud Netflix 似乎也围绕简单的 Netflix 内容包装了很多类。

我尝试实现 RetryableRibbonLoadBalancingHttpClientRibbonLoadBalancingHttpClient 的子类,它们是 Spring 类。我尝试使用ribbon.ClientClassNameapplication.yml 中给出他们的班级名称,但这不起作用。我试图覆盖 Spring Cloud 的 HttpClientRibbonConfiguration 中定义的 bean,但我无法让它工作。

所以我有两个问题:

  1. 我的假设是否正确,即我需要创建一个自定义功能区 Client 并且定义 herehere 的 bean 不能解决问题?

  2. 如何正确操作?

任何想法都非常感谢,所以提前致谢!

Update-1

我对此进行了更多研究并找到了RibbonAutoConfiguration

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

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

我想知道这是否可能。

【问题讨论】:

  • @spencergibb 我真的希望你能对此有所了解。我已经阅读了你在 Github 和 StackOverflow 上的一些贡献,我认为这对你来说很合适。 :)
  • 你应该发布代码。在 SpringBoot 配置中覆盖 bean 应该可以工作。
  • 我更新了帖子。我无法将代码发布到我不知道它应该如何工作的东西上。不幸的是,这在任何地方都没有描述,我一直在整个网络上搜索解决方案。
  • 我们不推荐使用ribbon rest客户端,只推荐使用负载均衡器。我们建议改为RestTemplateWebClient
  • @spencergibb 谢谢。我实际上正在使用@LoadBalanced RestTemplates 和 FeignClients,它们的下方已剥离了 Ribbon。功能区从外部看不到,但它是我需要的功能区检索信息(基本上是 Eureka InstanceInfo)来设置 HTTP 标头。如何使用 RestTemplates、FeignClient 或 WebClient 来做到这一点?您有关于 Ribbon 内部布线的任何信息吗?

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


【解决方案1】:

好的,我会自己回答这个问题,以防其他人将来需要。
其实我终于实现了。

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 的实现完成。使用 Ribbon 时,这些是 RibbonLoadBalancingHttpClientRetryableRibbonLoadBalancingHttpClient 的子类。

所以,我最初为 Ribbon 的 HTTP 客户端发送的请求添加 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 中提供该信息,如下所示:

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 来选择发送请求的服务实例,发送请求,拦截器将注入路由标头. Go-Router 会将请求路由到路由标头中指定的确切实例,并且不会执行任何会干扰 Ribbon 选择的额外负载平衡。 如果需要重试(针对相同或一个或多个下一个实例),拦截器将再次注入相应的路由标头 - 这次是针对 Ribbon 选择的可能不同的服务实例。 这使您可以有效地将 Ribbon 用作负载均衡器,并事实上禁用 Go-Router 的负载均衡,将其降级为单纯的代理。好处是 Ribbon 是您可以(以编程方式)影响的东西,而您对 Go-Router 几乎没有影响。

注意:这是针对@LoadBalanced RestTemplate 的测试并且有效。 但是,对于@FeignClients,它不能以这种方式工作。 在this post 中描述了我为 Feign 解决此问题的最接近的方法,但是,那里描述的解决方案使用了一个无法访问(功能区)选择的服务实例的拦截器,因此不允许访问所需的元数据。
到目前为止还没有为FeignClient找到解决方案。

【讨论】:

    猜你喜欢
    • 2022-12-05
    • 2020-12-21
    • 2018-02-26
    • 2017-01-02
    • 2018-10-08
    • 2018-06-18
    • 2021-04-13
    • 2016-12-25
    • 2022-08-18
    相关资源
    最近更新 更多