文章目錄
2.1 Ribbon實現(xiàn)REST請求負(fù)載均衡邏輯
2.2 LoadBalancerInterceptor 的實現(xiàn)
2.3 LoadBalancerClient的實現(xiàn)
2.4ILoadBalancer負(fù)載均衡器的實現(xiàn)(結(jié)合Eureka)
3 Ribbon實戰(zhàn):為服務(wù)消費者整合Ribbon
Ribbon 是Netflix 發(fā)布的基于HTTP和TCP的客戶端負(fù)載均衡器凯楔,將我們服務(wù)間的REST模板請求進行攔截封裝嘹裂,轉(zhuǎn)發(fā)到合適的服務(wù)提供者實例上。
Spring Cloud 中昆雀,Ribbon與Eureka 配合使用時焚廊,Ribbon可自動通過Eureka Client獲取服務(wù)提供者的地址列表,并基于某種負(fù)載均衡算法,請求其中一個服務(wù)提供實例妖碉。
Ribbon + Eureka 負(fù)載均衡架構(gòu)如下圖所示:
Rebbion 負(fù)載均衡器組件介紹:
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按照某種負(fù)載均衡策略選擇要使用的服務(wù)實例豪诲。
通過上面的組件架構(gòu)流程我們可以為服務(wù)的每個請求選擇一個可用的服務(wù)DiscoveryEnabledServer。
Spring Cloud為每個服務(wù)提供者的Rebbion 負(fù)載均衡器 維護了一個子應(yīng)用上下文挂绰,我們可以為不同服務(wù)提供者配置不同的均衡策略屎篱,當(dāng)然也可以配置全局的均衡策略(詳見:配置項)。
前面介紹了Ribbon是將我們服務(wù)間的REST請求通過封裝轉(zhuǎn)發(fā)葵蒂,來實現(xiàn)負(fù)載均衡芳室,那么具體是怎么實現(xiàn)的?
2.1 Ribbon實現(xiàn)REST請求負(fù)載均衡邏輯
當(dāng)服務(wù)啟動時刹勃,會為我們應(yīng)用中每個有@LoadBalanced 的RestTemplate實例堪侯,注入一個Ribbon負(fù)載均衡器的攔截器(LoadBalancerInterceptor ),當(dāng)服務(wù)在向某個服務(wù)提供者發(fā)起首次請求時荔仁,會初始化該服務(wù)提供者負(fù)載均衡器伍宦,這個加載過程也可以通過配置在服務(wù)啟動時被加載完成(詳見:《饑餓加載》章節(jié))
2.2 LoadBalancerInterceptor 的實現(xiàn)
LoadBalancerClient:負(fù)載均衡器客戶端,負(fù)載均衡入口乏梁,下一章節(jié)將詳解
LoadBalancerRequestFactory:負(fù)載均衡的請求創(chuàng)建工廠
在集成了Ribbon負(fù)載均衡之后不可能在使用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負(fù)載均衡之后不可能在使用IP:PORT 這樣的方式去發(fā)起請求,而是將IP:PORT換成了每個服務(wù)提供者的ServerName落萎,但是最終還是轉(zhuǎn)成IP:PORT發(fā)起REST請求亥啦,reconstructURI實現(xiàn)了從RibbonServer服務(wù)實例向URI的轉(zhuǎn)化。
2.4ILoadBalancer負(fù)載均衡器的實現(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ù)(默認(rèn)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ù)實例。
隨機策略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ù)(默認(rèn)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時形入,默認(rèn)IRule)
使用ZoneAvoidancePredicate過濾掉不可用的zone下的所有Server實例,再使用AvailabilityFiltering過濾掉過濾掉那些高并發(fā)的的后端server(active connections 超過配置的閾值)缝左,在輪詢選擇一個服務(wù)實例亿遂。
3 Ribbon實戰(zhàn):為服務(wù)消費者整合Ribbon
學(xué)生查詢已下單股票列表時,需要去股票服務(wù)中獲取股票詳情渺杉,補全股票信息蛇数。
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();}}
全局默認(rèn)配置
Spring Cloud Ribbon自動化配置了默認(rèn)接口配置:
IClientConfig:Ribbon的客戶端配置浦徊,默認(rèn)采用DefaultClientConfigImpl。
IRule:Ribbon的負(fù)載均衡策略天梧,默認(rèn)采用ZoneAvoidanceRule辑畦,該策略能夠在多區(qū)域環(huán)境下選出最佳區(qū)域的實例進行訪問。
IPing:Ribbon的實例檢查策略腿倚,默認(rèn)采用DummyPing實現(xiàn)纯出,檢查實例狀態(tài)為UP,則返回true敷燎。
ServerList:服務(wù)實例清單的維護機制暂筝,默認(rèn)采用 ConfigurationBasedServerList實現(xiàn)。
ServerListFilter:服務(wù)實例清單過濾機制硬贯,默認(rèn)采用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的負(fù)載均衡策略改為隨機策略饵骨。
?
例如:
ribbon:NFLoadBalancerRuleClassName:com.netflix.loadbalancer.RandomRule
將所有的Ribbon Client的負(fù)載均衡策略改為隨機策略。
使用java代碼自定義Ribbon配置
? 由于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ù)用來開啟重試機制传货,它默認(rèn)是關(guān)閉的
hystrix.command.default.execution.isolation.thread.timeoutInMillseconds:斷路器的超時時間需要大于Ribbon的超時時間,不然不會觸發(fā)重試宏娄。
.ribbon.ConnectTimeout:請求連接的超時時間问裕。
.ribbon.ReadTimeout:請求處理的超時時間。
.ribbon.OkToRetryOnAllOperations:對所有操作請求都進行重試孵坚,默認(rèn)只對GET請求重試粮宛。
.ribbon.MaxAutoRetriesNextServer:切換實例的重試次數(shù)。
.ribbon.MaxAutoRetries:對當(dāng)前實例的重試次數(shù)卖宠。
具體可參考:
org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration.RetryAutoConfiguration
前面Spring Cloud為每個服務(wù)提供者的Rebbion 負(fù)載均衡器 維護了一個子應(yīng)用上下文巍杈,通過代碼也分析出來了,這個上下文默認(rèn)是懶加載扛伍。只有在第一次請求時筷畦,對應(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)用上下文鼎文,從而提高第一次的訪問速度渔肩。
[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