Spring Cloud - 在 RestTemplate 中重试工作?

Posted

技术标签:

【中文标题】Spring Cloud - 在 RestTemplate 中重试工作?【英文标题】:Spring Cloud - Getting Retry Working In RestTemplate? 【发布时间】:2016-02-19 07:15:58 【问题描述】:

我一直在将现有应用程序迁移到 Spring Cloud 的服务发现、功能区负载平衡和断路器。该应用程序已经广泛使用了 RestTemplate,并且我已经能够成功地使用模板的负载平衡版本。但是,我一直在测试有两个服务实例并且我将其中一个实例停止运行的情况。我希望 RestTemplate 故障转移到下一个服务器。从我所做的研究来看,故障转移逻辑似乎存在于 Feign 客户端和使用 Zuul 时。 LoadBalancedRest 模板似乎没有故障转移逻辑。在深入研究代码时,看起来 RibbonClientHttpRequestFactory 正在使用 netflix RestClient(它似乎有重试的逻辑)。

那么我该从哪里开始工作呢?

我宁愿不使用 Feign 客户端,因为我必须扫描大量代码。 我发现这个link 建议使用@Retryable 注释和@HystrixCommand 但这似乎应该是负载平衡休息模板的一部分。

我对 RibbonClientHttpRequestFactory.RibbonHttpRequest 的代码做了一些研究:

protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException 

    try 
        addHeaders(headers);
        if (outputStream != null) 
            outputStream.close();
            builder.entity(outputStream.toByteArray());
        

        HttpRequest request = builder.build();
        HttpResponse response = client.execute(request, config);

        return new RibbonHttpResponse(response);
    
    catch (Exception e) 
        throw new IOException(e);
    

看来,如果我重写此方法并将其更改为使用“client.executeWithLoadBalancer()”,我可能能够利用 RestClient 中内置的重试逻辑?我想我可以创建自己的 RibbonClientHttpRequestFactory 版本来做到这一点?

只是寻找有关最佳方法的指导。

谢谢

【问题讨论】:

【参考方案1】:

回答我自己的问题:

在我进入细节之前,一个警示故事:

在我的本地机器上测试故障转移时,Eureka 的自我保护模式让我陷入了困境。我建议在进行测试时关闭自我保护模式。因为我以固定速率删除节点然后重新启动(使用随机值使用不同的实例 ID),所以我触发了 Eureka 的自我保存模式。我最终在 Eureka 中找到了许多指向同一台机器、同一端口的实例。故障转移实际上正在工作,但选择的下一个节点恰好是另一个死实例。一开始很困惑!

我能够使用修改后的 RibbonClientHttpRequestFactory 版本进行故障转移。因为 RibbonAutoConfiguration 使用这个工厂创建了一个负载平衡的 RestTemplate,而不是注入这个 rest 模板,所以我用我修改过的请求工厂版本创建了一个新模板:

protected RestTemplate restTemplate;

@Autowired
public void customizeRestTemplate(SpringClientFactory springClientFactory, LoadBalancerClient loadBalancerClient) 
    restTemplate = new RestTemplate();

    // Use a modified version of the http request factory that leverages the load balacing in netflix's RestClient.
    RibbonRetryHttpRequestFactory lFactory = new RibbonRetryHttpRequestFactory(springClientFactory, loadBalancerClient);
    restTemplate.setRequestFactory(lFactory);

修改后的 Request Factory 只是 RibbonClientHttpRequestFactory 的一个副本,有两个小改动:

1) 在 createRequest 中,我删除了从负载均衡器中选择服务器的代码,因为 RestClient 会为我们执行此操作。 2) 在内部类 RibbonHttpRequest 中,我将 executeInternal 更改为调用“executeWithLoadBalancer”。

全班:

@SuppressWarnings("deprecation")
public class RibbonRetryHttpRequestFactory implements ClientHttpRequestFactory 

    private final SpringClientFactory clientFactory;
    private LoadBalancerClient loadBalancer;

    public RibbonRetryHttpRequestFactory(SpringClientFactory clientFactory, LoadBalancerClient loadBalancer) 
        this.clientFactory = clientFactory;
        this.loadBalancer = loadBalancer;
    

    @Override
    public ClientHttpRequest createRequest(URI originalUri, HttpMethod httpMethod) throws IOException 
        String serviceId = originalUri.getHost();
        IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);

        RestClient client = clientFactory.getClient(serviceId, RestClient.class);
        HttpRequest.Verb verb = HttpRequest.Verb.valueOf(httpMethod.name());
        return new RibbonHttpRequest(originalUri, verb, client, clientConfig);
    

    public class RibbonHttpRequest extends AbstractClientHttpRequest 

        private HttpRequest.Builder builder;
        private URI uri;
        private HttpRequest.Verb verb;
        private RestClient client;
        private IClientConfig config;
        private ByteArrayOutputStream outputStream = null;

        public RibbonHttpRequest(URI uri, HttpRequest.Verb verb, RestClient client, IClientConfig config) 
            this.uri = uri;
            this.verb = verb;
            this.client = client;
            this.config = config;
            this.builder = HttpRequest.newBuilder().uri(uri).verb(verb);
        

        @Override
        public HttpMethod getMethod() 
            return HttpMethod.valueOf(verb.name());
        

        @Override
        public URI getURI() 
            return uri;
        

        @Override
        protected OutputStream getBodyInternal(HttpHeaders headers) throws IOException 
            if (outputStream == null) 
                outputStream = new ByteArrayOutputStream();
            
            return outputStream;
        

        @Override
        protected ClientHttpResponse executeInternal(HttpHeaders headers) throws IOException 
            try 
                addHeaders(headers);
                if (outputStream != null) 
                    outputStream.close();
                    builder.entity(outputStream.toByteArray());
                
                HttpRequest request = builder.build();
                HttpResponse response = client.executeWithLoadBalancer(request, config);
                return new RibbonHttpResponse(response);
            
            catch (Exception e) 
                throw new IOException(e);
            

            //TODO: fix stats, now that execute is not called
            // use execute here so stats are collected
            /*
            return loadBalancer.execute(this.config.getClientName(), new LoadBalancerRequest<ClientHttpResponse>() 
                @Override
                public ClientHttpResponse apply(ServiceInstance instance) throws Exception 
            );
            */
        

        private void addHeaders(HttpHeaders headers) 
            for (String name : headers.keySet()) 
                // apache http RequestContent pukes if there is a body and
                // the dynamic headers are already present
                if (!isDynamic(name) || outputStream == null) 
                    List<String> values = headers.get(name);
                    for (String value : values) 
                        builder.header(name, value);
                    
                
            
        

        private boolean isDynamic(String name) 
            return name.equals("Content-Length") || name.equals("Transfer-Encoding");
        
    

    public class RibbonHttpResponse extends AbstractClientHttpResponse 

        private HttpResponse response;
        private HttpHeaders httpHeaders;

        public RibbonHttpResponse(HttpResponse response) 
            this.response = response;
            this.httpHeaders = new HttpHeaders();
            List<Map.Entry<String, String>> headers = response.getHttpHeaders().getAllHeaders();
            for (Map.Entry<String, String> header : headers) 
                this.httpHeaders.add(header.getKey(), header.getValue());
            
        

        @Override
        public InputStream getBody() throws IOException 
            return response.getInputStream();
        

        @Override
        public HttpHeaders getHeaders() 
            return this.httpHeaders;
        

        @Override
        public int getRawStatusCode() throws IOException 
            return response.getStatus();
        

        @Override
        public String getStatusText() throws IOException 
            return HttpStatus.valueOf(response.getStatus()).name();
        

        @Override
        public void close() 
            response.close();
        
    

【讨论】:

当您在 PR 中向 spring-cloud 提交等效代码时,我可以假设此答案中的代码在 Apache License Version 2 下可用吗? 它基于 Spring 代码库的其余部分所依据的任何许可证。 像魅力一样工作。谢谢!【参考方案2】:

我遇到了同样的问题,但是开箱即用,一切正常(使用@LoadBalanced RestTemplate)。我正在使用 Finchley 版本的 Spring Cloud,我认为我的问题是我没有在我的 pom 配置中明确添加 spring-retry。我将把我的 spring-retry 相关的 yml 配置留在这里(请记住,这仅适用于 @LoadBalanced RestTemplateZuulFeign):

spring:
# Ribbon retries on
  cloud:
    loadbalancer:
      retry:
        enabled: true

# Ribbon service config
my-service:
  ribbon:
    MaxAutoRetries: 3
    MaxAutoRetriesNextServer: 1
    OkToRetryOnAllOperations: true
    retryableStatusCodes: 500, 502

【讨论】:

以上是关于Spring Cloud - 在 RestTemplate 中重试工作?的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cloud 学习 Spring Cloud Config

Spring Cloud 学习——7. Spring Cloud Config

如何在 spring-cloud-gateway 合约测试中从 spring-cloud-contract 中设置带有 StubRunner 端口的 url

2021-11-22 spring-cloud-nacos配置优先级

spring-cloud 和 spring-cloud-gcp 是不是有通用 BOM 文件?

Spring Cloud高可用的分布式配置中心 Spring Cloud Config