(三)使用Ribbon實現(xiàn)客戶端側(cè)負載均衡

文章目錄

1.Ribbon 簡介

1.1 Ribbon + Eureka 架構(gòu)

1.2 Ribbon 負載均衡器組件架構(gòu)

2 深入剖析Ribbon

2.1 Ribbon實現(xiàn)REST請求負載均衡邏輯

2.2 LoadBalancerInterceptor 的實現(xiàn)

2.3 LoadBalancerClient的實現(xiàn)

2.4ILoadBalancer負載均衡器的實現(xiàn)(結(jié)合Eureka)

2.5 IRule負載均衡策略

3 Ribbon實戰(zhàn):為服務(wù)消費者整合Ribbon

3.1需求場景

3.2編寫一個消費者

4 其他

4.1 配置項

4.2 **重試機制**

4.3 饑餓加載

5 參考文獻

1.Ribbon 簡介

Ribbon 是Netflix 發(fā)布的基于HTTP和TCP的客戶端負載均衡器,將我們服務(wù)間的REST模板請求進行攔截封裝,轉(zhuǎn)發(fā)到合適的服務(wù)提供者實例上。

1.1 Ribbon + Eureka 架構(gòu)

Spring Cloud 中,Ribbon與Eureka 配合使用時,Ribbon可自動通過Eureka Client獲取服務(wù)提供者的地址列表,并基于某種負載均衡算法,請求其中一個服務(wù)提供實例。

Ribbon + Eureka 負載均衡架構(gòu)如下圖所示:

1.2 Ribbon 負載均衡器組件架構(gòu)

Rebbion 負載均衡器組件介紹:

ServerList:服務(wù)提供者實例列表,當(dāng)和Eureka結(jié)合時,可以通過Eureka Client動態(tài)獲取可用的服務(wù)實例列表,也可以配置靜態(tài)的服務(wù)列表。

ServerListFilter:服務(wù)過濾器,通過該過濾器將會從serverList中過濾掉不符合規(guī)則的服務(wù)。

IPing:心跳檢查,定時監(jiān)測ServerList服務(wù)列表中服務(wù)狀態(tài)。

IRule:均衡策略,將最終的ServerList按照某種負載均衡策略選擇要使用的服務(wù)實例。

通過上面的組件架構(gòu)流程我們可以為服務(wù)的每個請求選擇一個可用的服務(wù)DiscoveryEnabledServer。

Spring Cloud為每個服務(wù)提供者的Rebbion 負載均衡器 維護了一個子應(yīng)用上下文,我們可以為不同服務(wù)提供者配置不同的均衡策略,當(dāng)然也可以配置全局的均衡策略(詳見:配置項)。

2 深入剖析Ribbon

前面介紹了Ribbon是將我們服務(wù)間的REST請求通過封裝轉(zhuǎn)發(fā),來實現(xiàn)負載均衡,那么具體是怎么實現(xiàn)的?

2.1 Ribbon實現(xiàn)REST請求負載均衡邏輯

當(dāng)服務(wù)啟動時,會為我們應(yīng)用中每個有@LoadBalanced 的RestTemplate實例,注入一個Ribbon負載均衡器的攔截器(LoadBalancerInterceptor ),當(dāng)服務(wù)在向某個服務(wù)提供者發(fā)起首次請求時,會初始化該服務(wù)提供者負載均衡器,這個加載過程也可以通過配置在服務(wù)啟動時被加載完成(詳見:《饑餓加載》章節(jié))

2.2 LoadBalancerInterceptor 的實現(xiàn)

LoadBalancerClient:負載均衡器客戶端,負載均衡入口,下一章節(jié)將詳解

LoadBalancerRequestFactory:負載均衡的請求創(chuàng)建工廠

在集成了Ribbon負載均衡之后不可能在使用IP:PORT 這樣的方式去發(fā)起請求,而是將IP:PORT換成了每個服務(wù)提供者的ServerName。

2.3 LoadBalancerClient的實現(xiàn)

Spring Cloud 集成了Ribbion,使用RibbonLoadBalancerClient 實現(xiàn)了LoadBalancerClient 以下主要接口:

1.execute(String serviceId, LoadBalancerRequest request) throws IOException

主要流程:

代碼實現(xiàn):

2.URI reconstructURI(ServiceInstance instance, URI original);

前面在攔截器中介紹到,在集成了Ribbon負載均衡之后不可能在使用IP:PORT 這樣的方式去發(fā)起請求,而是將IP:PORT換成了每個服務(wù)提供者的ServerName,但是最終還是轉(zhuǎn)成IP:PORT發(fā)起REST請求,reconstructURI實現(xiàn)了從RibbonServer服務(wù)實例向URI的轉(zhuǎn)化。

2.4ILoadBalancer負載均衡器的實現(xiàn)(結(jié)合Eureka)

接口組件:

ServerList:服務(wù)實例列表,當(dāng)Ribbon與Eureka聯(lián)合使用時,ServerList會被DiscoveryEnabledNIWSServerList重寫擴展成從Eureka注冊中心中獲取服務(wù)實例列表,并注冊。當(dāng)Eureka Client定時從Eureka注冊中心獲取服務(wù)后會觸發(fā)DynamicServerListLoadBalancer 更新事件更新ServerList。

1 )初始化Serverlist和啟動監(jiān)聽Eureker Client發(fā)送獲取服務(wù)(Get Register)的請求監(jiān)聽.

?

2 )注冊監(jiān)聽:EurekaNotificationServerListUpdater.start(final UpdateAction updateAction)

if(eurekaClient==null){eurekaClient=eurekaClientProvider.get();}if(eurekaClient!=null){eurekaClient.registerEventListener(updateListener);}

更新事件:com.netflix.loadbalancer.DynamicServerListLoadBalancer.updateListOfServers()

@VisibleForTesting

public void updateListOfServers() {

? ? List<T> servers = new ArrayList<T>();

? ? if (serverListImpl != null) {

? ? ? ? servers = serverListImpl.getUpdatedListOfServers();

? ? ? ? LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",

? ? ? ? ? ? ? ? getIdentifier(), servers);

? ? ? ? if (filter != null) {

? ? ? ? ? ? servers = filter.getFilteredListOfServers(servers);

? ? ? ? ? ? LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",

? ? ? ? ? ? ? ? ? ? getIdentifier(), servers);

? ? ? ? }

? ? }

? ? updateAllServerList(servers);

}

ServerListFilter:服務(wù)過濾器,通過該過濾器將會從serverList中過濾掉不符合規(guī)則的服務(wù)。

org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter.getFilteredListOfServers()

@OverridepublicList<Server>getFilteredListOfServers(List<Server>servers){List<Server>output=super.getFilteredListOfServers(servers);if(this.zone!=null&&output.size()==servers.size()){List<Server>local=newArrayList<Server>();for(Server server:output){if(this.zone.equalsIgnoreCase(server.getZone())){local.add(server);}}if(!local.isEmpty()){returnlocal;}}returnoutput;}

IPing:檢測ServerList中的是否可用,當(dāng)Ribbon與Eureka聯(lián)合使用時,NIWSDiscoveryPing來取代IPing,它將職責(zé)委托給Eureka來確定服務(wù)端是否已經(jīng)啟動。

1)啟動時開始定時任務(wù)(默認10s):

BaseLoadBalancer.setupPingTask()

void setupPingTask() {

if (canSkipPing()) {

? ? ? ? return;

? ? }

? ? if (lbTimer != null) {

? ? ? ? lbTimer.cancel();

? ? }

? ? lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,

? ? ? ? ? ? true);

? ? lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);

? ? forceQuickPing();

}

2)檢測NIWSDiscoveryPing.isAlive():

publicbooleanisAlive(Server server){booleanisAlive=true;if(server!=null&&serverinstanceofDiscoveryEnabledServer){DiscoveryEnabledServer dServer=(DiscoveryEnabledServer)server;InstanceInfo instanceInfo=dServer.getInstanceInfo();if(instanceInfo!=null){InstanceStatus status=instanceInfo.getStatus();if(status!=null){isAlive=status.equals(InstanceStatus.UP);}}}returnisAlive;}

IRule:均衡策略,將最終的ServerList按照一定的策略選擇最終要使用的服務(wù)實例。

2.5 IRule負載均衡策略

隨機策略RandomRule

從ServerList中隨機選擇一個Server實例。

關(guān)鍵代碼:

intindex=rand.nextInt(serverCount);server=upList.get(index);

輪詢策略RoundRobinRule

輪詢Serverlist選擇下個Server實例

關(guān)鍵代碼:

intnextServerIndex=incrementAndGetModulo(serverCount);server=allServers.get(nextServerIndex);

incrementAndGetModulo():

privateintincrementAndGetModulo(intmodulo){for(;;){intcurrent=nextServerCyclicCounter.get();intnext=(current+1)%modulo;if(nextServerCyclicCounter.compareAndSet(current,next))returnnext;}}

權(quán)重策略WeightedResponseTimeRule

WeightedResponseTimeRule繼承了RoundRobinRule,開始時沒有權(quán)重列表,采用父類(RoundRobinRule)的輪詢方式;啟動一個定時任務(wù)(默認30s),定時任務(wù)會根據(jù)實例的響應(yīng)時間來更新權(quán)重列表,choose方法中,用一個(0,1)的隨機double數(shù)乘以最大的權(quán)重得到randomWeight,然后遍歷權(quán)重列表,找出第一個比randomWeight大的實例下標(biāo),然后返回該實例。

具體實現(xiàn),請參考類:com.netflix.loadbalancer.WeightedResponseTimeRule

請求數(shù)最少策略BestAvailableRule

public Server choose(Object key) {

? ? if (loadBalancerStats == null) {

? ? ? ? return super.choose(key);

? ? }

? ? List<Server> serverList = getLoadBalancer().getAllServers();

? ? int minimalConcurrentConnections = Integer.MAX_VALUE;

? ? long currentTime = System.currentTimeMillis();

? ? Server chosen = null;

? ? for (Server server: serverList) {

? ? ? ? ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);

? ? ? ? if (!serverStats.isCircuitBreakerTripped(currentTime)) {

? ? ? ? ? ? int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);

? ? ? ? ? ? if (concurrentConnections < minimalConcurrentConnections) {

? ? ? ? ? ? ? ? minimalConcurrentConnections = concurrentConnections;

? ? ? ? ? ? ? ? chosen = server;

? ? ? ? ? ? }

? ? ? ? }

? ? }

? ? if (chosen == null) {

? ? ? ? return super.choose(key);

? ? } else {

? ? ? ? return chosen;

? ? }

}

AvailabilityFilteringRule

過濾掉那些因為一直連接失敗的被標(biāo)記為circuit tripped的后端server,并過濾掉那些高并發(fā)的的后端server(active connections 超過配置的閾值),在使用RoundRobinRule 選擇一個服務(wù)

ZoneAvoidanceRule(Ribbon集合Eureka時,默認IRule)

使用ZoneAvoidancePredicate過濾掉不可用的zone下的所有Server實例,再使用AvailabilityFiltering過濾掉過濾掉那些高并發(fā)的的后端server(active connections 超過配置的閾值),在輪詢選擇一個服務(wù)實例。

3 Ribbon實戰(zhàn):為服務(wù)消費者整合Ribbon

3.1需求場景

學(xué)生查詢已下單股票列表時,需要去股票服務(wù)中獲取股票詳情,補全股票信息。

3.2編寫一個消費者

1.創(chuàng)建一個ArtifactId是finace-training-student的Maven工程,并為項目添加以下依賴。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-eureka-server</artifactId><exclusions><exclusion><artifactId>spring-retry</artifactId><groupId>org.springframework.retry</groupId></exclusion></exclusions></dependency>

2.在配置文件application.yml中添加如下內(nèi)容。

server:port:9000spring:application:name:finace-training-studenteureka:client:serviceUrl:defaultZone:http://localhost:8761/eureka/,http://localhost:8762/eureka/instance:prefer-ip-address:true

3.編寫啟動類,在啟動類上添加@EnableDiscoveryClient注解,聲明這是一個Eureka Client,RestTemplate加上ribbon注解@LoadBalanced

packagecom.myhexin.finace.training.server.main;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.cloud.client.discovery.EnableDiscoveryClient;importorg.springframework.cloud.client.loadbalancer.LoadBalanced;importorg.springframework.context.annotation.Bean;importorg.springframework.web.client.RestTemplate;@SpringBootApplication(scanBasePackages="com.myhexin")@EnableDiscoveryClientpublicclassServierApplication{@Bean@LoadBalancedpublicRestTemplaterestTemplate(){returnnewRestTemplate();}publicstaticvoidmain(String[]args){SpringApplication.run(ServierApplication.class,args);}}

4.編寫消費者調(diào)用代碼:

packagecom.myhexin.finace.training.server.controller;importjava.util.ArrayList;importjava.util.List;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.cloud.client.ServiceInstance;importorg.springframework.cloud.client.discovery.DiscoveryClient;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;importorg.springframework.web.client.RestTemplate;importcom.myhexin.finace.training.api.stock.dto.StockDTO;@RestController@RequestMapping(value="/student",produces="application/json;charset=UTF-8")publicclassStudentController{@AutowiredprivateRestTemplate restTemplate;@AutowiredprivateDiscoveryClient discoveryClient;@GetMapping("/{id}/ownStcoks")publicList<StockDTO>ownStcoks(@PathVariableString id){String[]ownStockCodes={"300033","000001"};List<StockDTO>ownStocks=newArrayList<>(ownStockCodes.length);for(String code:ownStockCodes){// 補全股票信息//StockDTO stock = restTemplate.getForObject(this.getInstance("finace-training-stock") + "/stock/" + code, StockDTO.class);StockDTO stock=restTemplate.getForObject("http://finace-training-stock"+"/stock/"+code,StockDTO.class);ownStocks.add(stock);}returnownStocks;}privateStringgetInstance(String serviceId){List<ServiceInstance>instances=discoveryClient.getInstances(serviceId);if(instances.isEmpty()){returnnull;}returninstances.get(0).getUri().toString();}}

4 其他

4.1 配置項

全局默認配置

Spring Cloud Ribbon自動化配置了默認接口配置:

IClientConfig:Ribbon的客戶端配置,默認采用DefaultClientConfigImpl。

IRule:Ribbon的負載均衡策略,默認采用ZoneAvoidanceRule,該策略能夠在多區(qū)域環(huán)境下選出最佳區(qū)域的實例進行訪問。

IPing:Ribbon的實例檢查策略,默認采用DummyPing實現(xiàn),檢查實例狀態(tài)為UP,則返回true。

ServerList:服務(wù)實例清單的維護機制,默認采用 ConfigurationBasedServerList實現(xiàn)。

ServerListFilter:服務(wù)實例清單過濾機制,默認采用ZonePrefenceServerListFilter,優(yōu)先過濾出與請求調(diào)用方處于同區(qū)域的服務(wù)實例。

使用屬性自定義Ribbon配置

從Spring Cloud Netflix 1.2.0 (即從Spring Cloud Camden RELEASE開始),Ribbon支持使用屬性自定義(即可定義在appplication.yml中)。

配置前綴.ribbon.屬性;是RibbonClient的名稱,如果省略則表示全部配置。

屬性:

NFLoadBalancerClassName:配置ILoadBalancer的實現(xiàn)類

NFLoadBalancerPingClassName:配置IPing的實現(xiàn)類

NFLoadBalancerRuleClassName:配置IRule的實現(xiàn)類

NIWSServerListClassName:配置ServerList的實現(xiàn)類

NIWSServerListFilterClassName:配置ServerListFilter的實現(xiàn)類

例如:

finace-training-stock:ribbon:NFLoadBalancerRuleClassName:com.netflix.loadbalancer.RandomRule

將finace-training-stock的Ribbon Client的負載均衡策略改為隨機策略。

?

例如:

ribbon:NFLoadBalancerRuleClassName:com.netflix.loadbalancer.RandomRule

將所有的Ribbon Client的負載均衡策略改為隨機策略。

使用java代碼自定義Ribbon配置

4.2?重試機制

? 由于Spring Cloud Eureka實現(xiàn)的服務(wù)治理機制強調(diào)了CAP原理中的AP,為了實現(xiàn)更高的服務(wù)可用性,犧牲了一定的一致性,在極端情況下它寧愿接受故障實例也不要丟掉“健康”實例,比如,當(dāng)服務(wù)注冊中心的網(wǎng)絡(luò)繁盛故障斷開時,由于所有的服務(wù)實例無法維持持續(xù)心跳,在強調(diào)AP的服務(wù)治理中會把所有服務(wù)實例都踢出掉,而Eureka則會因為超過85%的實例丟失心跳二回觸發(fā)保護機制,注冊中心江湖保留此時的所有節(jié)點,以實現(xiàn)服務(wù)間依然可以進行互相調(diào)用的場景,即使其中有部分故障節(jié)點,但這樣做可以繼續(xù)保障大多數(shù)服務(wù)正常消費。

? 所以服務(wù)調(diào)用的時候通常會加入一些重試機制。從Camden SR2版本開始,Spring Cloud整合了Spring Retry來增強RestTemplate的重試能力,對于開發(fā)者來說只需通過簡單的配置,原來那些通過RestTemplate實現(xiàn)的服務(wù)訪問就會自動根據(jù)配置來實現(xiàn)重試策略。

重試機制只有在引入了RetryTemplate才會生效。

<dependency>

<artifactId>spring-retry</artifactId>

<groupId>org.springframework.retry</groupId>

</dependency>

重試機制屬性配置策略:

spring.cloud.loadbalancer.retry.enabled:該參數(shù)用來開啟重試機制,它默認是關(guān)閉的

hystrix.command.default.execution.isolation.thread.timeoutInMillseconds:斷路器的超時時間需要大于Ribbon的超時時間,不然不會觸發(fā)重試。

.ribbon.ConnectTimeout:請求連接的超時時間。

.ribbon.ReadTimeout:請求處理的超時時間。

.ribbon.OkToRetryOnAllOperations:對所有操作請求都進行重試,默認只對GET請求重試。

.ribbon.MaxAutoRetriesNextServer:切換實例的重試次數(shù)。

.ribbon.MaxAutoRetries:對當(dāng)前實例的重試次數(shù)。

具體可參考:

org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.RetryAutoConfiguration

4.3 饑餓加載

前面Spring Cloud為每個服務(wù)提供者的Rebbion 負載均衡器 維護了一個子應(yīng)用上下文,通過代碼也分析出來了,這個上下文默認是懶加載。只有在第一次請求時,對應(yīng)的上下文才會被加載,因此,首次請求往往會比較慢,從Spring Cloud Dalston開始,我們可以配置饑餓加載。

例如:

ribbon:eager-load:enable:trueclients:finace-training-stock,finace-training-student

在啟動的時候就會加載 finace-training-stock和finace-training-student的Ribbon Client對應(yīng)的子應(yīng)用上下文,從而提高第一次的訪問速度。

5 參考文獻

[1] Ribbon的GitHub : https://github.com/Netflix/ribbon

[2] 周立. Spring Cloud與Docker微服務(wù)架構(gòu)實戰(zhàn)

[3] Spring Cloud中文網(wǎng).https://www.springcloud.cc/spring-cloud-dalston.html#spring-cloud-ribbon

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容