带有 Eureka 的 Spring Cloud Gateway 得到 504(网关超时)

Posted

技术标签:

【中文标题】带有 Eureka 的 Spring Cloud Gateway 得到 504(网关超时)【英文标题】:Spring Cloud Gateway with Eureka getting 504 (Gateway Time-out) 【发布时间】:2018-11-25 18:57:17 【问题描述】:

假设我有一个 Eureka Server、一个 Spring Cloud Gateway 和一个运行两个实例的 ServiceA

当我部署该服务的更新版本时,在短时间内,eureka 将为 ServiceA 保存四个实例,一个不再存在,一个刚刚启动。

在 Eureka 驱逐不再有效的两个实例之前,在网关上,Ribbon 仍将在不再存在的实例之间进行负载平衡,从而生成 ConnectTimeoutException 并因此生成 504 (Gateway Time-out)。我已经使用以下 retry 配置在网关中配置了这些路由。

val retry = RetryGatewayFilterFactory.RetryConfig()
                 .setExceptions(ConnectException::class.java)

这允许功能区在异常为 ConnectException: Connection refused 时立即重试,但在 ConnectTimeoutException 时不会重试。

我可以调整功能区和 eureka 客户端的刷新间隔,但我宁愿不碰这些。

所以,我对此有两个问题。

如何在过滤器中捕获超时? 是否有更好的方法来处理此问题以实现零停机时间?

谢谢

【问题讨论】:

你在使用 Hystrix 吗? @spencergibb 还没有,我应该这样做吗? 【参考方案1】:

试试ribbon.retryableStatusCodes=404,502,504


更新:

首先在我看来,ConnectException 应该与代码 502 相关联,而 504SocketTimeoutException。如有错误请指正。

抱歉,云网关没有深入研究,但是它的LB可以选择使用Ribbon。

假设您使用 Ribbon 和 OKHttp via ribbon.OkHttp.enabled

OkHttpRibbonConfiguration 将初始化一个 OkHttpLoadBalancingClient bean,它执行请求。在其execute() 中,它在每次请求执行期间首先创建一个RetryCallback 匿名实现对象。 RetryCallback负责执行请求和重试执行。

我们来看看RetryCallback的逻辑。收到响应后,它会检查响应状态码是否可重试。如果类加载器中存在RetryTemplate类,则loadBalancedRetryPolicyFactory的实现为RibbonLoadBalancedRetryPolicyFactory。它用于创建RibbonLoadBalancedRetryPolicy 对象(以下retryPlicy)。

executeWithRetry()RetryPolicy 创建一个RetryTemplate。 (前提条件,通过将请求设置为可重试,在spring cloud gateway中启用重试)。

public OkHttpRibbonResponse execute(...) throws Exception 
    final LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryPolicyFactory.create(this.getClientName(), this);
    ...
    final Request request = newRequest.toRequest();
    Response response = httpClient.newCall(request).execute();
    if(retryPolicy.retryableStatusCode(response.code())) 
        ResponseBody responseBody = response.peekBody(Integer.MAX_VALUE);
        response.close();
        throw new OkHttpStatusCodeException(RetryableOkHttpLoadBalancingClient.this.clientName,
                            response, responseBody, newRequest.getURI());
    
    return new OkHttpRibbonResponse(response, newRequest.getUri());        
    

private OkHttpRibbonResponse executeWithRetry(...) throws Exception 
    RetryTemplate retryTemplate = new RetryTemplate();
    BackOffPolicy backOffPolicy = loadBalancedBackOffPolicyFactory.createBackOffPolicy(this.getClientName());
    retryTemplate.setBackOffPolicy(backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy);
    RetryListener[] retryListeners = this.loadBalancedRetryListenerFactory.createRetryListeners(this.getClientName());
    if (retryListeners != null && retryListeners.length != 0) 
        retryTemplate.setListeners(retryListeners);
    
    boolean retryable = isRequestRetryable(request); //HERE
    retryTemplate.setRetryPolicy(retryPolicy == null || !retryable ? new NeverRetryPolicy()
            : new RetryPolicy(request, retryPolicy, this, this.getClientName()));
    return retryTemplate.execute(callback, recoveryCallback);


private boolean isRequestRetryable(ContextAwareRequest request) 
    return request.getContext() == null ? true :
        BooleanUtils.toBooleanDefaultIfNull(request.getContext().getRetryable(), true);

retryableStatusCode()中,检查retryableStatusCodes是否包含响应状态码。

public class RibbonLoadBalancedRetryPolicy implements LoadBalancedRetryPolicy 
    public static final IClientConfigKey<String> RETRYABLE_STATUS_CODES = new CommonClientConfigKey<String>("retryableStatusCodes") ;
    ....
    List<Integer> retryableStatusCodes = new ArrayList<>();
        public RibbonLoadBalancedRetryPolicy(String serviceId, RibbonLoadBalancerContext context, ServiceInstanceChooser loadBalanceChooser,
                                         IClientConfig clientConfig) 
        this.serviceId = serviceId;
        this.lbContext = context;
        this.loadBalanceChooser = loadBalanceChooser;
        String retryableStatusCodesProp = clientConfig.getPropertyAsString(RETRYABLE_STATUS_CODES, "");
        String[] retryableStatusCodesArray = retryableStatusCodesProp.split(",");
        for(String code : retryableStatusCodesArray) 
            if(!StringUtils.isEmpty(code)) 
                try 
                    retryableStatusCodes.add(Integer.valueOf(code.trim()));
                 catch (NumberFormatException e) 
                    //TODO log
                
            
        
    
    ...
    @Override
    public boolean retryableStatusCode(int statusCode) 
        return retryableStatusCodes.contains(statusCode);
    

如果包含,则抛出扩展RetryableStatusCodeExceptionOkHttpStatusCodeException。并且RibbonRecoveryCallback 捕捉到这个异常,检查异常是否是RetryableStatusCodeException 的实现。如果是,则RetryTemplate 继续重试。或者,抛出不可重试的 Expcetion 来中断重试。

【讨论】:

尝试:解释你的答案。 @Anthony,最后一个答案来自手机。只需追加更多。

以上是关于带有 Eureka 的 Spring Cloud Gateway 得到 504(网关超时)的主要内容,如果未能解决你的问题,请参考以下文章

带有临时端口的 Spring-cloud brixton、eureka 和 docker

带有 Spring Cloud 和 Eureka java.lang.IllegalStateException 的功能区:没有可用于 localhost 的实例

Spring Cloud:ZUUL + Eureka + NodeJS

spring cloud 之 Eureka

spring cloud 之eureka配置

Spring Cloud:服务治理——Spring Cloud Eureka