1. 程式人生 > 其它 >SpringCloud元件--Ribbon入門解析

SpringCloud元件--Ribbon入門解析

一、Ribbon簡介

分散式系統中,當服務提供者叢集部署時,服務消費者就需要從多個服務提供者當中選擇一個進行服務呼叫,那麼此時就會涉及負載均衡,將請求分發到不同的服務例項上。

常用的負載均衡有兩種實現方式,一種是獨立部署負載均衡程式,比如nginx;一種是將負載均衡的邏輯嵌入到服務消費者端的程式中。

前者程式碼程式碼侵入性低但是需要獨立部署負載均衡元件;後者有一定的程式碼侵入性但是不需要獨立部署負載均衡元件,可以降低伺服器成本。

Netfilx的開源Ribbon就是以第二種方式實現的負載均衡元件,將負載均衡的邏輯封裝在客戶端。

Ribbon預設提供了七種負載均衡策略

1、BestAvailableRule:選擇最小請求數

2、ClientConfigEnabledRoundRobinRule:輪詢

3、RondomRule:隨機選擇server

4、RoundRobinRule:輪詢選擇server

5、RetryRule:根據輪詢的方式重試

6、WeightedResponseTimeRule:根據相應時間分配一個權重weight,權重越低被選擇的概率越小

7、ZoneAvoidanceRule:根據server的zone區域和可用性輪詢選擇

二、Ribbon實現原理

2.1、@LoadBalanced註解

Ribbon的使用比較簡單,首先需要新增Ribbon相關依賴

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
        </dependency>

然後只需要在注入HTTP客戶端如RestTemplate例項時新增@LoadBalanced註解即可,那麼該RestTemplate進行HTTP請求呼叫服務時就會根據本地快取的服務提供者列表資訊進行負載均衡呼叫。

    @Autowired
    @LoadBalanced
    private RestTemplate restTemplate;

所以研究Ribbon的原理主要是從@LoadBalanced註解入手,@LoadBalanced註解定義如下:

@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced { }

@LoadBalanced註解本身沒有任何邏輯,所以可以猜測@LoadBalanced註解只是一個標記的作用,核心邏輯在LoadBalancerAutoConfiguration中,

    @LoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();

    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
            final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
        return () -> restTemplateCustomizers.ifAvailable(customizers -> {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                for (RestTemplateCustomizer customizer : customizers) {
                    customizer.customize(restTemplate);
                }
            }
        });
    }

首先注入了所有被@LoadBalanced註解修飾的RestTemplate例項,然後再遍歷呼叫所有RestTemplateCustomizer的customize進行定製化處理,實現類位於LoadBalancerAutoConfiguration的內部類LoadBalancerInterceptorConfig中,如下示:

    @Configuration
    @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
    static class LoadBalancerInterceptorConfig {
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor( LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
            /** 初始化負載均衡攔截器*/
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }

        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
            return restTemplate -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                        restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                /** 給RestTemplate設定攔截器*/
                restTemplate.setInterceptors(list);
            };
        }
    }

實際就是給RestTemplate物件添加了一個攔截器LoadBalancerInterceptor, 那麼就可以得出結論, RestTemplate的方法呼叫會通過LoadBalancerInterecptor的intercept方法進行攔截處理,所以負載均衡的邏輯就全部在LoadBalancerInterceptor中。

2.2、LoadBalancerInterceptor

LoadBalancerInterceptor內部持有負載均衡客戶端LoadBalancerClient的例項,實際的負載均衡邏輯也都委託給了LoadBalancerClient例項來處理,原始碼如下:

/** 負載均衡客戶端 */
    private LoadBalancerClient loadBalancer;

    /** LoadBalancerInterceptor 攔截方法 */
    @Override
    public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
                                        final ClientHttpRequestExecution execution) throws IOException {
        final URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        /** 呼叫負載均衡客戶端的execute方法進行攔截處理 */
        return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
    }

LoadBalancerClient的實現類是RibbonLoadBalancerClient,execute方法實現邏輯如下:

@Override
    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        /** 1.根據服務ID獲取 ILoadBalancer 物件 */
        ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
        /** 2.根據 ILoadBalancer物件選取適合的服務例項 */
        Server server = getServer(loadBalancer);
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        }
        /** 3.構建RibbonServer物件 */
        RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
                serviceId), serverIntrospector(serviceId).getMetadata(server));
        /** 4.向目標伺服器傳送請求 */
        return execute(serviceId, ribbonServer, request);
    }

這裡核心兩步分別是構造ILoadBalancer物件和根據ILoadBalancer物件選取目標伺服器,而選取目標伺服器的getServer方法實際就是呼叫了ILoadBalancer物件的chooseServer方法,所以核心就在於ILoadBalancer物件

2.3、ILoadBalancer

ILoadBalancer介面定義了一系列負載均衡的方法,原始碼如下:

 1 public interface ILoadBalancer {
 2 
 3         /**
 4          * 新增伺服器列表
 5          */
 6         public void addServers(List<Server> newServers);
 7 
 8         /**
 9          * 根據key選擇伺服器
10          */
11         public Server chooseServer(Object key);
12 
13         /**
14          * 標記伺服器下線
15          */
16         public void markServerDown(Server server);
17 
18         /**
19          * 獲取可用伺服器列表
20          */
21         public List<Server> getReachableServers();
22 
23         /**
24          * 獲取所有伺服器列表
25          */
26         public List<Server> getAllServers();
27     }

ILoadBalancer的實現類為DynamicServerListLoadBalancer,DynamicServerListLoadBalancer建構函式如下:

public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
                                         ServerList<T> serverList, ServerListFilter<T> filter,
                                         ServerListUpdater serverListUpdater) {
        super(clientConfig, rule, ping);
        this.serverListImpl = serverList;
        this.filter = filter;
        this.serverListUpdater = serverListUpdater;
        if (filter instanceof AbstractServerListFilter) {
            ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
        }
        restOfInit(clientConfig);
    }

呼叫父類BaseLoadBalancer的建構函式初始化,然後和呼叫了restOfInit方法進行初始化,在restOfInit方法中會執行updateListOfServer()方法,該方法的邏輯是呼叫ServerList介面的實現類的getUpdateListOfServers()方法,

最終會呼叫Eureka客戶端的獲取服務註冊列表的功能。另外ILoadBalancer並非只從Eureka伺服器拉取一次服務列表,而是建立了一個PingTask定時任務,每隔10秒遍歷向所有服務例項傳送一次ping心跳,如果返回的結果和預期的不一樣,

就會重新從Eureka伺服器拉取最新的服務註冊列表。所以DynamicServerListLoadBalancer內部是快取了所有伺服器例項列表,並且通過向伺服器例項傳送心跳的方式檢測服務例項是否存活。

當ILoadBalancer例項有了服務列表,接下來就需要選擇伺服器了,實現方法在BaseLoadBalancer的chooseServer方法,原始碼如下:

 /**
     * BaseLoadBalanacer的 選取伺服器方法
     * */
    public Server chooseServer(Object key) {
        /** 建立計數器*/
        if (counter == null) {
            counter = createCounter();
        }
        /** 計數器計數*/
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                /** 呼叫IRule的choose方法 */
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }

執行的是IRule的例項的choose方法進行處理,IRule介面定義如下:

public interface IRule {
        /**
         * 選擇可用服務例項
         */
        public Server choose(Object key);

        /**
         * 設定 ILoadBalancer物件
         */
        public void setLoadBalancer(ILoadBalancer lb);

        /**
         * 獲取 ILoadBalancer物件
         */
        public ILoadBalancer getLoadBalancer();
    }

IRule的實現類比較多,根據不同負載均衡策略有不同的實現類,根據不同的負載均衡策略有對應的實現類,共有BestAvailableRule、ClientConfigEnabledRoundRobinRule、RandomRule、RoundRobinRule、RetryRule、WeightedResponseTimeRule、

ZoneAvoidanceRule等七種策略。不同的策略都是需要根據ILoadBalancer獲取全部服務例項列表和當前線上服務例項列表,然後根據對應的演算法選取合適的伺服器例項。

如BestAvailableRule策略就是根據最小請求數選擇伺服器,實現邏輯就是本地快取每一個伺服器例項的選擇次數,然後選擇次數最小的一臺伺服器即可;而RoundRobinRule實現邏輯就是記錄每次訪問的伺服器索引,依次遞增從伺服器列表中選擇下一個服務例項。

2.4、總結

Ribbon的使用通過@LoadBalance註解來配置, 被@LoadBalance註解修飾的RestTemplate在初始化時LoadBalancerAutoConfiguration會給RestTemplat新增攔截器LoadBalanceInterceptor;

RestTemplate呼叫HTTP介面時就會通過攔截器進行攔截並進行負載均衡處理。攔截器將請求交給ILoadBalancer處理,ILoadBalancer初始化時會從Eureka伺服器拉取註冊的服務列表,並且建立PingTask定時任務每10秒進行一次伺服器ping判斷是否可用;

ILoadBalancer處理請求時由負載均衡規則IRule物件的choose方法進行伺服器選擇,根據不同的策略選擇合適的伺服器資訊。

三、負載均衡元件對比

負載均衡高可用框架的必不可少的元件之一,可以提升系統的整體高可用性,通過負載均衡將流量分發到多個伺服器,同時多伺服器能夠消除這部分伺服器的單點故障。

負載均衡通常有兩種實現模式,一種是獨立部署負載均衡伺服器,客戶端所有請求都經過負載均衡伺服器,負載均衡伺服器根據路由規則將請求在分發到目標伺服器;還有一種是將負載均衡邏輯整合在客戶端,客戶端本地維護可用伺服器資訊列表,在請求時

根據負載均衡策略選擇目標伺服器。

兩種負載均衡實現方式各有優缺點

獨立部署:優點是客戶端無需關心負載均衡邏輯,不需要維護伺服器資訊列表,伺服器資訊由負載均衡伺服器集中式管理;缺點是負載均衡伺服器需要獨立部署,且同樣需要保證高可用性;

客戶端整合:優點是無需獨立部署,部署簡單;缺點是客戶端本地需要維護伺服器資訊,且需要定時重新整理和傳送心跳確保伺服器可用;

Ribbon的負載均衡實現就是客戶端整合的負載均衡,而集中式負載均衡的實現最熱門的就是nginx,包括熱門的SLB負載均衡等;