Spring Cloud - 在RestTemplate中重试工作?

时间:2015-11-17 19:06:51

标签: spring-cloud

我一直在将现有应用程序迁移到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版本来做到这一点?

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

由于

2 个答案:

答案 0 :(得分:5)

回答我自己的问题:

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

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();
        }
    }
}

答案 1 :(得分:0)

我遇到了同样的问题,但开箱即用,一切正常(使用 @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