一、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負載均衡等;