Ribbon是Netflix公司開源的一個(gè)負(fù)載均衡的項(xiàng)目(https://github.com/Netflix/ribbon),它是一個(gè)基于HTTP、TCP的客戶端負(fù)載均衡器。
服務(wù)端負(fù)載均衡
負(fù)載均衡是微服務(wù)架構(gòu)中必須使用的技術(shù),通過負(fù)載均衡來實(shí)現(xiàn)系統(tǒng)的高可用、集群擴(kuò)容等功能。負(fù)載均衡可通過硬件設(shè)備及軟件來實(shí)現(xiàn),硬件比如:F5、Array等,軟件比如:LVS、Nginx等。

用戶請求先到達(dá)負(fù)載均衡器(也相當(dāng)于一個(gè)服務(wù)),負(fù)載均衡器根據(jù)負(fù)載均衡算法將請求轉(zhuǎn)發(fā)到微服務(wù)。負(fù)載均衡算法有:輪訓(xùn)、隨機(jī)、加權(quán)輪訓(xùn)、加權(quán)隨機(jī)、地址哈希等方法,負(fù)載均衡器維護(hù)一份服務(wù)列表,根據(jù)負(fù)載均衡算法將請求轉(zhuǎn)發(fā)到相應(yīng)的微服務(wù)上,所以負(fù)載均衡可以為微服務(wù)集群分擔(dān)請求,降低系統(tǒng)的壓力
客戶端負(fù)載均衡
上圖是服務(wù)端負(fù)載均衡,客戶端負(fù)載均衡與服務(wù)端負(fù)載均衡的區(qū)別在于客戶端要維護(hù)一份服務(wù)列表,Ribbon從Eureka Server獲取服務(wù)列表,Ribbon根據(jù)負(fù)載均衡算法直接請求到具體的微服務(wù),中間省去了負(fù)載均衡服務(wù)。
Ribbon負(fù)載均衡的流程圖:

在消費(fèi)微服務(wù)中使用Ribbon實(shí)現(xiàn)負(fù)載均衡,Ribbon先從Eureka Server 或 Nacos Server中獲取服務(wù)列表。
Ribbon根據(jù)負(fù)載均衡的算法去調(diào)用微服務(wù)。
Ribbon測試
Spring Cloud引入Ribbon配合 restTemplate 實(shí)現(xiàn)客戶端負(fù)載均衡。Java中遠(yuǎn)程調(diào)用的技術(shù)有很多,如:webservice、socket、rmi、Apache HttpClient、OkHttp等。
- 在客戶端添加Ribbon依賴
注意:由于我們之前整合Nacos時(shí)引入了spring-cloud-starter-alibaba-nacos-discovery這個(gè)依賴包,而這個(gè)包默認(rèn)已經(jīng)幫我們繼承了Ribbon,所有這里可以不用單獨(dú)引入Ribbon依賴包。
- 配置Ribbon參數(shù)
ribbon:
MaxAutoRetries: 2 #最大重試次數(shù),當(dāng)Eureka中可以找到服務(wù),但是服務(wù)連不上時(shí)將會(huì)重試
MaxAutoRetriesNextServer: 3 #切換實(shí)例的重試次數(shù)
OkToRetryOnAllOperations: false #對所有操作請求都進(jìn)行重試,如果是get則可以,如果是post,put等操作沒有實(shí)現(xiàn)冪等的情況下是很危險(xiǎn)的,所以設(shè)置為false
ConnectTimeout: 5000 #請求連接的超時(shí)時(shí)間
ReadTimeout: 6000 #請求處理的超時(shí)時(shí)間
- 負(fù)載均衡測試
啟動(dòng)兩個(gè)服務(wù),端口需要不一致
定義RestTemplate,使用@LoadBalanced注解
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
測試代碼
@Slf4j
@RestController
public class TestController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/test")
public String test() {
String result = restTemplate.getForObject("http://alibaba-nacos-discovery-server/hello?name=wolf", String.class);
return "Return : " + result;
}
}
可以看到,在定義RestTemplate時(shí)候,增加了@LoadBalanced注解,而在真正調(diào)用服務(wù)接口的時(shí)候,原來host部分是通過手工拼接ip和端口形式。而這里直接采用服務(wù)名來寫請求路徑即可。在真正調(diào)用的時(shí)候,Spring Cloud會(huì)將請求攔截下來,然后通過Ribbon從Nacos Server獲取服務(wù)列表,并通過負(fù)載均衡器選出節(jié)點(diǎn),并替換服務(wù)名部分為具體的ip和端口,交給RestTemplate去請求,從而實(shí)現(xiàn)基于服務(wù)名的負(fù)載均衡調(diào)用。
Ribbon饑餓加載
默認(rèn)情況下Ribbon是懶加載的。當(dāng)服務(wù)起動(dòng)好之后,第一次請求是非常慢的,第二次之后就快很多。
解決方式:開啟饑餓加載
ribbon:
eager-load:
enabled: true #開啟饑餓加載
clients: server-1,server-2,server-3 #為哪些服務(wù)的名稱開啟饑餓加載,多個(gè)用逗號分隔
Ribbon組件
| 接口 | 作用 | 默認(rèn)值 |
|---|---|---|
| IclientConfig | 讀取配置 | DefaultClientConfigImpl |
| IRule | 負(fù)載均衡規(guī)則,選擇實(shí)例 | ZoneAvoidanceRule |
| IPing | 篩選掉ping不通的實(shí)例 | DummyPing(該類什么不干,認(rèn)為每個(gè)實(shí)例都可用,都能ping通) |
| ServerList<Server> | 交給Ribbon的實(shí)例列表 | Ribbon:ConfigurationBasedServerList Spring Cloud Alibaba:NacosServerList |
| ServerListFilter<Server> | 過濾掉不符合條件的實(shí)例 | ZonePreferenceServerListFilter |
| ILoadBalancer | Ribbon的入口 | ZoneAwareLoadBalancer |
| ServerListUpdater | 更新交給Ribbon的List的策略 | PollingServerListUpdater |
這里的每一項(xiàng)都可以自定義:
IclientConfig Ribbon支持非常靈活的配置就是有該組件提供的
IRule 為Ribbon提供規(guī)則,從而選擇實(shí)例、該組件是最核心組件
舉例:
- 代碼方式:
@Configuration
public class RibbonRuleConfig {
@Bean
public IRule ribbonRulr() {
return new RandomRule();
}
@Bean
public IPing iPing(){
return new PingUrl();
}
}
- 配置屬性方式:
<clientName>:
ribbon:
NFLoadBalancerClassName: #ILoadBalancer該接口實(shí)現(xiàn)類
NFLoadBalancerRuleClassName: #IRule該接口實(shí)現(xiàn)類
NFLoadBalancerPingClassName: #Iping該接口實(shí)現(xiàn)類
NIWSServerListClassName: #ServerList該接口實(shí)現(xiàn)類
NIWSServerListFilterClassName: #ServiceListFilter該接口實(shí)現(xiàn)類
在這些屬性中定義的類優(yōu)先于使用@RibbonClient(configuration=RibbonConfig.class)Spring 定義的bean 以及由Spring Cloud Netflix提供的默認(rèn)值。描述:配置文件中定義ribbon優(yōu)先代碼定義
Ribbon負(fù)載均衡的八種算法,其中ResponseTimeWeightedRule已廢除
| 規(guī)則名稱 | 特點(diǎn) |
|---|---|
| AvailabilityFilteringRule | 過濾掉一直連接失敗的被標(biāo)記為circuit tripped(電路跳閘)的后端Service,并過濾掉那些高并發(fā)的后端Server或者使用一個(gè)AvailabilityPredicate來包含過濾Server的邏輯,其實(shí)就是檢查status的記錄的各個(gè)Server的運(yùn)行狀態(tài) |
| BestAvailableRule | 選擇一個(gè)最小的并發(fā)請求的Server,逐個(gè)考察Server,如果Server被tripped了,則跳過 |
| RandomRule | 隨機(jī)選擇一個(gè)Server |
| ResponseTimeWeightedRule | 已廢棄,作用同WeightedResponseTimeRule |
| RetryRule | 對選定的負(fù)責(zé)均衡策略機(jī)上充值機(jī)制,在一個(gè)配置時(shí)間段內(nèi)當(dāng)選擇Server不成功,則一直嘗試使用subRule的方式選擇一個(gè)可用的Server |
| RoundRobinRule | 輪詢選擇,輪詢index,選擇index對應(yīng)位置Server |
| WeightedResponseTimeRule | 根據(jù)相應(yīng)時(shí)間加權(quán),相應(yīng)時(shí)間越長,權(quán)重越小,被選中的可能性越低 |
| ZoneAvoidanceRule |
(默認(rèn)是這個(gè))負(fù)責(zé)判斷Server所Zone的性能和Server的可用性選擇Server,在沒有Zone的環(huán)境下,類似于輪詢(RoundRobinRule) |
實(shí)現(xiàn)負(fù)載均衡<細(xì)粒度>配置-隨機(jī)
- 方式一:代碼方式
首先定義RestTemplate,并且添加注解@LoadBalanced,這樣RestTemplate就實(shí)現(xiàn)了負(fù)載均衡
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
//template.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));//解決中文亂碼
return new RestTemplate();
}
在SpringBootApplication主類下添加配置類。該類主要作用于為哪個(gè)服務(wù)做負(fù)載均衡。默認(rèn)的是輪訓(xùn)
@Configuration
@RibbonClient(name = "${服務(wù)名稱}", configuration = GoodsRibbonRuleConfig.class)//configuration: 指向負(fù)載均衡規(guī)則的配置類
public class GoodsRibbonConfig {
}
添加Ribbon的配置類,注意該類必須配置在@SpringBootApplication主類以外的包下。不然的話所有的服務(wù)都會(huì)按照這個(gè)規(guī)則來實(shí)現(xiàn)。會(huì)被所有的RibbonClient共享。主要是主類的主上下文和Ribbon的子上下文起沖突了。父子上下文不能重疊
@Configuration
public class GoodsRibbonRuleConfig {
@Bean
public IRule ribbonRulr() {
return new RandomRule();
}
}
- 方式二:配置屬性方式
server-1: # 服務(wù)名稱 Service-ID
ribbon:
# 屬性配置方式【推薦】
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 配置文件配置負(fù)載均衡算法-我這里使用的是自定義的Ribbon的負(fù)載均衡算法,默認(rèn)
優(yōu)先級:配置(不會(huì)影響其他服務(wù))>(大于) 硬編碼(類得寫在SpringBoot啟動(dòng)類包外,不然會(huì)影響其他服務(wù))
總結(jié):
| 配置方式 | 優(yōu)點(diǎn) | 缺點(diǎn) |
|---|---|---|
| 代碼配置 | 基于代碼,更加靈活 | 有坑(父子上下文) 線上修改得重新打包,發(fā)布 |
| 屬性配置 | 易上手 | 配置更加直觀 線上修改無需重新打包,發(fā)布 優(yōu)先級更高 極端場景下沒有配置配置方式靈活 |
實(shí)現(xiàn)負(fù)載均衡<全局>配置-隨機(jī)
- 方式一:Ribbon的配置類定義在主類下
讓ComponentScan上下文重疊(強(qiáng)烈不建議使用) - 方式二:
@Configuration
@RibbonClients(defaultConfiguration = GoodsRibbonRuleConfig.class)//Ribbon負(fù)載均衡全局粒度配置(所有服務(wù)都按照這個(gè)配置)
public class RibbonConfig {
}
擴(kuò)展Ribbon-支持Nacos權(quán)重
默認(rèn)情況下Ribbon是不支持Nacos的權(quán)重負(fù)載均衡選擇的,這里我們自己擴(kuò)展一個(gè)Rule,然Ribbon支持Nacos的權(quán)重規(guī)則。
- 創(chuàng)建NacosWeightedRule類
package com.thtf.contentcenter.configuration;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties;
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer;
@Slf4j
public class NacosWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
//讀取配置文件,并初始化
}
@Override
public Server choose(Object o) {
try {
BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
//想要請求的微服務(wù)名稱
String name = loadBalancer.getName();
//實(shí)現(xiàn)負(fù)載均衡算法
//拿到服務(wù)發(fā)現(xiàn)相關(guān)API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
//nacos client自動(dòng)通過基于權(quán)重的負(fù)載均衡算法,給我們選擇一個(gè)實(shí)例。
Instance instance = namingService.selectOneHealthyInstance(name);
log.info("選擇的實(shí)例是:port = {}, instance = {}", instance.getPort(), instance);
return new NacosServer(instance);
} catch (NacosException e) {
log.error("選擇實(shí)例異常:{}", e.getMessage(), e);
return null;
}
}
}
- 創(chuàng)建RibbonConfiguration類
package com.thtf.contentcenter.ribbonconfiguration;
import com.istimeless.contentcenter.configuration.NacosWeightedRule;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RibbonConfiguration {
@Bean
public IRule ribbonRule() {
return new NacosWeightedRule();
}
}
特別注意:RibbonConfiguration要建在啟動(dòng)類掃描不到的地方,如圖所示:

- 創(chuàng)建UserCenterRibbonConfiguration類,實(shí)現(xiàn)全局配置
package com.thtf.contentcenter.configuration;
import com.istimeless.ribbonconfiguration.RibbonConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;
@Configuration
@RibbonClients(defaultConfiguration = RibbonConfiguration.class)
public class UserCenterRibbonConfiguration {
}
擴(kuò)展Ribbon-同集群優(yōu)先
在Nacos上,支持集群配置。集群是指對指定微服務(wù)的一種虛擬分類。集群還是比較有用的,例如:
- 為了容災(zāi),把指定微服務(wù)同時(shí)部署在兩個(gè)機(jī)房(例如同城多活,其中1個(gè)機(jī)房崩潰了,另一個(gè)機(jī)房還能頂上,異地多活防止自然災(zāi)害)
- 調(diào)用時(shí),可優(yōu)先調(diào)用同機(jī)房的實(shí)例,如果同機(jī)房沒有實(shí)例,再跨機(jī)房調(diào)用。
雖然Spring Cloud Alibaba支持集群配置,例如:
spring:
cloud:
nacos:
discovery:
# 北京機(jī)房集群
cluster-name: BJ
但在調(diào)用時(shí),服務(wù)消費(fèi)者并不會(huì)優(yōu)先調(diào)用同集群的實(shí)例。
本節(jié)來探討如何擴(kuò)展Ribbon,從而實(shí)現(xiàn)同集群優(yōu)先調(diào)用的效果,并且還能支持Nacos權(quán)重配置。關(guān)于權(quán)重配置,前面已經(jīng)實(shí)現(xiàn)了,在前面的基礎(chǔ)上實(shí)現(xiàn)同集群優(yōu)先策略。
/**
* 支持優(yōu)先調(diào)用同集群實(shí)例的ribbon負(fù)載均衡規(guī)則.
*
* @author itmuch.com
*/
@Slf4j
public class NacosRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public Server choose(Object key) {
try {
String clusterName = this.nacosDiscoveryProperties.getClusterName();
DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
String name = loadBalancer.getName();
NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();
// 1. 找到指定服務(wù)的所有實(shí)例 A
List<Instance> instances = namingService.selectInstances(name, true);
if (CollectionUtils.isEmpty(instances)) {
return null;
}
List<Instance> instancesToChoose = instances;
if (StringUtils.isNotBlank(clusterName)) {
// 2. 過濾出相同集群下的所有實(shí)例 B
List<Instance> sameClusterInstances = instances.stream()
.filter(instance -> Objects.equals(clusterName, instance.getClusterName()))
.collect(Collectors.toList());
// 3. 如果B為空,就用A
if (!CollectionUtils.isEmpty(sameClusterInstances)) {
instancesToChoose = sameClusterInstances;
} else {
log.warn("發(fā)生跨集群的調(diào)用,name = {}, clusterName = {}, instance = {}", name, clusterName, instances);
}
}
// 4. 基于權(quán)重的負(fù)載均衡算法,返回一個(gè)實(shí)例
Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);
return new NacosServer(instance);
} catch (Exception e) {
log.warn("NacosRule發(fā)生異常", e);
return null;
}
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
}
負(fù)載均衡算法:
// Balancer來自于com.alibaba.nacos.client.naming.core.Balancer,也就是Nacos Client自帶的基于權(quán)重的負(fù)載均衡算法。
public class ExtendBalancer extends Balancer {
/**
* 根據(jù)權(quán)重,隨機(jī)選擇實(shí)例
*
* @param instances 實(shí)例列表
* @return 選擇的實(shí)例
*/
public static Instance getHostByRandomWeight2(List<Instance> instances) {
return getHostByRandomWeight(instances);
}
}
配置:
microservice-provider-user:
ribbon:
NFLoadBalancerRuleClassName: com.itmuch.cloud.study.ribbon.NacosClusterAwareWeightedRule
這樣,服務(wù)在調(diào)用microservice-provider-user 這個(gè)服務(wù)時(shí),就會(huì)優(yōu)先選擇相同集群下的實(shí)例。
擴(kuò)展Ribbon-基于元數(shù)據(jù)的版本控制
至此,已經(jīng)實(shí)現(xiàn)了
- 優(yōu)先調(diào)用同集群下的實(shí)例
- 實(shí)現(xiàn)基于權(quán)重配置的負(fù)載均衡
但實(shí)際項(xiàng)目,我們可能還會(huì)有這樣的需求:
一個(gè)微服務(wù)在線上可能多版本共存,例如: - 服務(wù)提供者有兩個(gè)版本:v1、v2
- 服務(wù)消費(fèi)者也有兩個(gè)版本:v1、v2
v1/v2是不兼容的。服務(wù)消費(fèi)者v1只能調(diào)用服務(wù)提供者v1;消費(fèi)者v2只能調(diào)用提供者v2。如何實(shí)現(xiàn)呢?
下面圍繞該場景,實(shí)現(xiàn)微服務(wù)之間的版本控制。
元數(shù)據(jù)
元數(shù)據(jù)就是一堆的描述信息,以map存儲(chǔ)。舉個(gè)例子:
spring:
cloud:
nacos:
metadata:
# 自己這個(gè)實(shí)例的版本
version: v1
# 允許調(diào)用的提供者版本
target-version: v1
需求分析
我們需要實(shí)現(xiàn)的有兩點(diǎn):
- 優(yōu)先選擇同集群下,符合metadata的實(shí)例
- 如果同集群加沒有符合metadata的實(shí)例,就選擇所有集群下,符合metadata的實(shí)例
代碼實(shí)現(xiàn)
@Slf4j
public class NacosFinalRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public Server choose(Object key) {
// 負(fù)載均衡規(guī)則:優(yōu)先選擇同集群下,符合metadata的實(shí)例
// 如果沒有,就選擇所有集群下,符合metadata的實(shí)例
// 1. 查詢所有實(shí)例 A
// 2. 篩選元數(shù)據(jù)匹配的實(shí)例 B
// 3. 篩選出同cluster下元數(shù)據(jù)匹配的實(shí)例 C
// 4. 如果C為空,就用B
// 5. 隨機(jī)選擇實(shí)例
try {
String clusterName = this.nacosDiscoveryProperties.getClusterName();
String targetVersion = this.nacosDiscoveryProperties.getMetadata().get("target-version");
DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
String name = loadBalancer.getName();
NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();
// 所有實(shí)例
List<Instance> instances = namingService.selectInstances(name, true);
List<Instance> metadataMatchInstances = instances;
// 如果配置了版本映射,那么只調(diào)用元數(shù)據(jù)匹配的實(shí)例
if (StringUtils.isNotBlank(targetVersion)) {
metadataMatchInstances = instances.stream()
.filter(instance -> Objects.equals(targetVersion, instance.getMetadata().get("version")))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(metadataMatchInstances)) {
log.warn("未找到元數(shù)據(jù)匹配的目標(biāo)實(shí)例!請檢查配置。targetVersion = {}, instance = {}", targetVersion, instances);
return null;
}
}
List<Instance> clusterMetadataMatchInstances = metadataMatchInstances;
// 如果配置了集群名稱,需篩選同集群下元數(shù)據(jù)匹配的實(shí)例
if (StringUtils.isNotBlank(clusterName)) {
clusterMetadataMatchInstances = metadataMatchInstances.stream()
.filter(instance -> Objects.equals(clusterName, instance.getClusterName()))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(clusterMetadataMatchInstances)) {
clusterMetadataMatchInstances = metadataMatchInstances;
log.warn("發(fā)生跨集群調(diào)用。clusterName = {}, targetVersion = {}, clusterMetadataMatchInstances = {}", clusterName, targetVersion, clusterMetadataMatchInstances);
}
}
Instance instance = ExtendBalancer.getHostByRandomWeight2(clusterMetadataMatchInstances);
return new NacosServer(instance);
} catch (Exception e) {
log.warn("發(fā)生異常", e);
return null;
}
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
}
** 負(fù)載均衡算法:**
public class ExtendBalancer extends Balancer {
/**
* 根據(jù)權(quán)重,隨機(jī)選擇實(shí)例
*
* @param instances 實(shí)例列表
* @return 選擇的實(shí)例
*/
public static Instance getHostByRandomWeight2(List<Instance> instances) {
return getHostByRandomWeight(instances);
}
}
思考
截止到這里,我們已經(jīng)對Ribbon的基本使用已經(jīng)如何自定義Ribbon負(fù)載均衡規(guī)則做了詳細(xì)說明,但細(xì)心的人會(huì)發(fā)現(xiàn),我使用上面 RestTemplate 地址拼接方式調(diào)用服務(wù)接口會(huì)存在以下幾點(diǎn)不足:
- 代碼可讀性差
- 復(fù)雜的url接口地址難以維護(hù)
- 編碼體驗(yàn)不統(tǒng)一
帶著這些不足,我們引入下一章要講解的另一種服務(wù)調(diào)用方式:Feign