springboot+zuul(二)------智慧負載

Aruze發表於2018-11-16

一、參考

參考資料:https://www.cnblogs.com/flying607/p/8330551.html

ribbon+spring retry重試策略原始碼分析:https://blog.csdn.net/xiao_jun_0820/article/details/79320352

 

二、背景

這幾天在做服務的高可用。

為了確保提供服務的某一臺機器出現故障導致客戶的請求不可用,我們需要對這臺伺服器做故障重試或者智慧路由到下一個可用伺服器。

為此,特地上網查了些資料,最後選用了ribbon+spring retry的重試策略。

 

從參考的技術文章中可以看出,故障重試的核心

1是引入spring retry的依賴

        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
            <version>1.2.2.RELEASE</version>
        </dependency>

 

2是開啟zuul和ribbon重試配置

zuul:
  retryable: true    #重試必配
ribbon:
  MaxAutoRetriesNextServer: 2     #更換服務例項次數
  MaxAutoRetries: 0        #當前服務重試次數
OkToRetryOnAllOperations: true #設定成false,只處理get請求故障

 

當然本文的目的並不止於此。

新增完了這些配置後,我們發現依然存在一些侷限性。

1、當提供服務的叢集機器例項小於MaxAutoRetriesNextServer時,只有採用輪詢策略的負載可以正常使用。

2、當提供服務的叢集機器例項大於MaxAutoRetriesNextServer時,採取輪詢或者隨機策略的負載偶爾可以正常使用。

而採用最小併發策略,或者單一負載(一般是為了解決session丟失問題,即同一個客戶端發的請求固定訪問某個伺服器)則

完全不能正常工作。

      為什麼這麼說呢?比如我們有5臺機器提供服務,第一臺機器可以正常提供服務,第二臺併發量最小。

      當第二到第五臺伺服器掛掉以後,採用輪詢方式且MaxAutoRetriesNextServer=2。那麼,ribbon會嘗試訪問第三臺、第四臺伺服器。

      結果不言而喻。當然如果運氣好,第三臺或第四臺伺服器是可以用的,那就能正常提供服務。

      採用隨機策略,同樣要依靠運氣。

      最小併發或單一策略的,則是不論重試幾次則因為總是選擇掛掉的第二個節點而完全失效。
那麼,有什麼解決辦法呢?

 

三、動態設定MaxAutoRetriesNextServer

出現這些問題,一個關鍵是MaxAutoRetriesNextServer被寫死了,而我們的提供server的數量又可能隨著叢集的負載情況增加(減少並不影響)。

總不能因為每次增加伺服器數量就改一次MaxAutoRetriesNextServer配置吧?既然不想改配置,那當然就是動態設定MaxAutoRetriesNextServer的值啊。

 

翻看重試的原始碼 RibbonLoadBalancedRetryPolicy.java

    @Override
    public boolean canRetryNextServer(LoadBalancedRetryContext context) {
        //this will be called after a failure occurs and we increment the counter
        //so we check that the count is less than or equals to too make sure
        //we try the next server the right number of times
        return nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer() && canRetry(context);
    }

 

可以看出MaxAutoRetriesNextServer的值是從DefaultLoadBalancerRetryHandler裡面獲取的。但是DefaultLoadBalancerRetryHandler又不提供設定MaxAutoRetriesNextServer的介面。

往上追溯DefaultLoadBalancerRetryHandler例項化的原始碼

    @Bean
    @ConditionalOnMissingBean
    public RibbonLoadBalancerContext ribbonLoadBalancerContext(ILoadBalancer loadBalancer,
            IClientConfig config, RetryHandler retryHandler) {
        return new RibbonLoadBalancerContext(loadBalancer, config, retryHandler);
    }

    @Bean
    @ConditionalOnMissingBean
    public RetryHandler retryHandler(IClientConfig config) {
        return new DefaultLoadBalancerRetryHandler(config);
    }

發現DefaultLoadBalancerRetryHandler物件可以從RibbonLoadBalancerContext例項中獲取, 而RibbonLoadBalancerContext卻可以從SpringClientFactory獲取,那麼我們只要新建retryHandler並重新賦值給RibbonLoadBalancerContext就可以了。

 

程式碼:

1、將IClientConfig託管到spring上

    @Bean
    public IClientConfig ribbonClientConfig() {
        DefaultClientConfigImpl config = new DefaultClientConfigImpl();
        config.loadProperties(this.name);
        config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
        config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
        return config;
    }

 

2、新建retryHandler並更新到RibbonLoadBalancerContext

private void setMaxAutoRetiresNextServer(int size) {  //size: 提供服務的叢集數量
        SpringClientFactory factory = SpringContext.getBean(SpringClientFactory.class); //獲取spring託管的單例物件
        IClientConfig clientConfig = SpringContext.getBean(IClientConfig.class);
        int retrySameServer = clientConfig.get(CommonClientConfigKey.MaxAutoRetries, 0);//獲取配置檔案中的值, 預設0
        boolean retryEnable = clientConfig.get(CommonClientConfigKey.OkToRetryOnAllOperations, false);//預設false。
        RetryHandler retryHandler = new DefaultLoadBalancerRetryHandler(retrySameServer, size, retryEnable);//新建retryHandler
        factory.getLoadBalancerContext(name).setRetryHandler(retryHandler);
    }

MaxAutoRetriesNextServer動態設定的問題就解決了。

 

四、剔除不可用的服務。

Eureka好像有提供服務的剔除和恢復功能,所以如果有用Eureka註冊中心,就不用往下看了。具體配置我也不太清楚。

因為我們沒用到eureka,所以在故障重試的時候,獲取到的服務列表裡依然包含了掛掉的伺服器。

這樣會導致最小併發策略和單一策略的負載出現問題。

跟蹤原始碼,我們發現伺服器故障後會呼叫canRetryNextServer方法,那麼不如就在這個方法裡面做文章吧。

 

自定義RetryPolicy 繼承RibbonLoadBalancedRetryPolicy並且重寫canRetryNextServer

public class ServerRibbonLoadBalancedRetryPolicy extends RibbonLoadBalancedRetryPolicy {

    private RetryTrigger trigger;
    public ServerRibbonLoadBalancedRetryPolicy(String serviceId, RibbonLoadBalancerContext context, ServiceInstanceChooser loadBalanceChooser, IClientConfig clientConfig) {
        super(serviceId, context, loadBalanceChooser, clientConfig);
    }

    public void setTrigger(RetryTrigger trigger) {
        this.trigger = trigger;
    }

    @Override
    public boolean canRetryNextServer(LoadBalancedRetryContext context) {
        boolean retryEnable = super.canRetryNextServer(context);
        if (retryEnable && trigger != null) {
            //回撥觸發
            trigger.exec(context);
        }
        return retryEnable;
    }

    @FunctionalInterface
    public interface RetryTrigger {
        void exec(LoadBalancedRetryContext context);
    }
}    

 

自定義RetryPolicyFactory繼承RibbonLoadBalancedRetryPolicyFactory並重寫create方法

public class ServerRibbonLoadBalancedRetryPolicyFactory extends RibbonLoadBalancedRetryPolicyFactory {
    private SpringClientFactory clientFactory;
    private ServerRibbonLoadBalancedRetryPolicy policy;
    private ServerRibbonLoadBalancedRetryPolicy.RetryTrigger trigger;

    public ServerRibbonLoadBalancedRetryPolicyFactory(SpringClientFactory clientFactory) {
        super(clientFactory);
        this.clientFactory = clientFactory;
    }

    @Override
    public LoadBalancedRetryPolicy create(String serviceId, ServiceInstanceChooser loadBalanceChooser) {
        RibbonLoadBalancerContext lbContext = this.clientFactory
                .getLoadBalancerContext(serviceId);
        policy = new ServerRibbonLoadBalancedRetryPolicy(serviceId, lbContext, loadBalanceChooser, clientFactory.getClientConfig(serviceId));
        policy.setTrigger(trigger);
        return policy;
    }

    public void setTrigger(ServerRibbonLoadBalancedRetryPolicy.RetryTrigger trigger) {
        policy.setTrigger(trigger);//跟上面是setTrigger不知道誰會先觸發,所以兩邊都設定了。
        this.trigger = trigger;
    }
}

 

把LoadBalancedRetryPolicyFactory託管到spring

    @Bean
    @ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
    public LoadBalancedRetryPolicyFactory loadBalancedRetryPolicyFactory(SpringClientFactory clientFactory) {
        return new ServerRibbonLoadBalancedRetryPolicyFactory(clientFactory);
    }

 

然後我們就可以在我們rule類上面實現RetryTrigger方法。

public class ServerLoadBalancerRule extends AbstractLoadBalancerRule implements ServerRibbonLoadBalancedRetryPolicy.RetryTrigger {

    private static final Logger LOGGER = LoggerFactory.getLogger(ServerLoadBalancerRule.class);
    /**
     * 不可用的伺服器
     */
    private Map<String, List<String>> unreachableServer = new HashMap<>(256);
    /**
     * 上一次請求標記
     */
    private String lastRequest;

    @Autowired
    LoadBalancedRetryPolicyFactory policyFactory;

    @Override
    public Server choose(Object key) {
        //初始化重試觸發器
        retryTrigger();
        return getServer(getLoadBalancer(), key);
    }

    private Server getServer(ILoadBalancer loadBalancer, Object key) {
    //從資料庫獲取服務列表
    List<ServerAddress> addressList = getServerAddress();
    setMaxAutoRetriesNextServer(addressList.size());

       //過濾不可用服務
    }

    private void retryTrigger() {
        RequestContext ctx = RequestContext.getCurrentContext();
        String batchNo = (String) ctx.get(Constant.REQUEST_BATCH_NO);
        if (!isLastRequest(batchNo)) {
            //不是同一次請求,清理所有快取的不可用服務
            unreachableServer.clear();
        }

        if (policyFactory instanceof ServerRibbonLoadBalancedRetryPolicyFactory) {
            ((ServerRibbonLoadBalancedRetryPolicyFactory) policyFactory).setTrigger(this);
        }
    }

    private boolean isLastRequest(String batchNo) {
        return batchNo != null && batchNo.equals(lastRequest);
    }

    @Override
    public void exec(LoadBalancedRetryContext context) {
        RequestContext ctx = RequestContext.getCurrentContext();
     //UUID,故障重試不會發生變化。客戶每次請求時會產生新的batchNo,可以在preFilter中生成。 
        String batchNo = (String) ctx.get(Constant.REQUEST_BATCH_NO);
        lastRequest = batchNo;

        List<String> hostAndPorts = unreachableServer.get((String) ctx.get(Constant.REQUEST_BATCH_NO));
        if (hostAndPorts == null) {
            hostAndPorts = new ArrayList<>();
        }
        if (context != null && context.getServiceInstance() != null) {
            String host = context.getServiceInstance().getHost();
            int port = context.getServiceInstance().getPort();
            if (!hostAndPorts.contains(host + Constant.COLON + port))
                hostAndPorts.add(host + Constant.COLON + port);
            unreachableServer.put((String) ctx.get(Constant.REQUEST_BATCH_NO), hostAndPorts);
        }
    }
}

 

這樣,我們就拿到了不可用的服務了,然後在重試的時候過濾掉unreachableServer中的服務就可以了。

這裡有一點要注意的是,MaxAutoRetriesNextServer的值必須是沒有過濾的服務列表的大小。

 

當然,有人會有疑問,如果伺服器數量過多,重試時間超過ReadTimeout怎麼辦?我這裡也沒關於超時的設定,因為本身讓客戶等待過久就不是很合理的需求

所以配置檔案裡面設定一個合理的ReadTimeout就好了,在這個時間段裡面如果重試沒取到可用的服務就直接拋超時的資訊給客戶。

原始碼地址: https://github.com/rxiu/study-on-road/tree/master/trickle-gateway

相關文章