Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。它有助于控制Http和Tcp客户端的行为。通过SpringCloud的封装,可以让我们轻松的将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用
官网参考地址:
官网:https://www.springcloud.cc/spring-cloud-greenwich.html#spring-cloud-ribbon
中文:http://docs.springcloud.cn/user-guide/ribbon/
名称 | 解释 |
---|---|
RoundRobinRule | 轮训策略 |
RandomRule | 随机策略 |
BestAvailableRule | 过滤出故障服务器后,选择一个并发量最小的 |
WeightedResponseTimeRule | 针对响应时间加权轮询 |
AvailabilityFilteringRule | 可用过滤策略,先过滤出故障的或并发请求大于阈值的一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个 |
ZoneAvoidanceRule | 从最佳区域实例集合中选择一个最优性能的服务实例 |
RetryRule | 选择一个Server,如果失败,重新选择一个Server重试 |
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
使用@LoadBalanced注解赋予RestTemplate负载均衡的能力
@Bean
@LoadBalanced
public RestTemplate createRestTemplate(){
return new RestTemplate();
}
例如:注入负载均衡规则,默认是轮询,更多配置以及配置实现类参见本文档的 Ribbon的相关配置
@Configuration
public class MyRibbonRuleConfig {
// 为指定轮询规则为随机
@Bean
public IRule myRule(){
return new RandomRule();
}
}
注:这个配置类MyRibbonRuleConfig
必须使用@Configuration
注解标识,并且每一个配置都要用@Bean
注解
如果我们将这个配置类放在@ComponentScan
注解的扫描包下,则会对所有的@RibbonClients
客户端生效,但是有时候,我们想要针对不同的客户端,使用不同的配置(例如不同的负载均衡规则),就需要排除该配置类,可以通过@ComponentScan
或@SpringBootApplication
限定扫包范围(@ComponentScan
配置排除:@ComponentScan(excludeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = MyRibbonRuleConfig.class)})
)或者将这个类不与启动类放在同一个包下。
// 启动类为某个服务指定负载均衡规则
@SpringBootApplication
@EnableEurekaClient
// 修改针对PROVIDER-PAYMENT-8001-3服务的轮训规则,多个不同的服务使用不同的规则,则在@RibbonClients中配置多个@RibbonClient
@RibbonClients(value = {@RibbonClient(name="PROVIDER-PAYMENT-8001-3",configuration = MyRibbonRuleConfig.class)})
public class EurekaOrder80Start {
public static void main(String[] args) {
SpringApplication.run(EurekaOrder80Start.class,args);
}
}
// 通过在eureka上注册过的微服务名称调用
public static final String PAYMENT_SRV = "http://PROVIDER-PAYMENT-8001-3/study";
@Resource
private RestTemplate restTemplate;
// 127.0.0.1/study/consumer/payment/create?serial=xiaohong
@GetMapping("/consumer/payment/create")
public CommonResult create(Payment payment)
{
return restTemplate.postForObject(PAYMENT_SRV + "/provider/payment/insertPayment",payment,CommonResult.class);
}
gitee地址中的cloud-eureka-consumer-order80和cloud-eureka-provider-payment8001-3模块
这地方需要注意的是loadBalancer.choose()
方法返回的已经是服务的真实地址了。调用的时候需要使用不带负载均衡的RestTemplate
去调用。
@Autowired
private LoadBalancerClient loadBalancer;
// 没有负载均衡的restTemplate,如果这里也使用负载均衡的restTemplate,那么在调用getForObject的时候,则会将uri中的域名地址作为服务名称,再去查询对应的IP地址。
// 这当然就会找不到啦,因为loadBalancer.choose()返回的已经是服务真实的ip地址了
@Resource(name = "restTemplate")
private RestTemplate restTemplate;
// 127.0.0.1/study/consumer/payment/get2/1
@GetMapping("/consumer/payment/get2/{id}")
public CommonResult getPayment2(@PathVariable Long id) {
ServiceInstance instance = loadBalancer.choose("PROVIDER-PAYMENT-8001-3");
URI uri = UriComponentsBuilder.fromUriString(String.format("http://%s:%s/study/provider/payment/get/{id}", instance.getHost(), instance.getPort()))
.build()
.expand(id) // 替换url中的模板参数
.encode(StandardCharsets.UTF_8) // 设置编码
.toUri();
return restTemplate.getForObject(uri,CommonResult.class);
}
// 通过在eureka上注册过的微服务名称调用
public static final String PAYMENT_SRV = "http://PROVIDER-PAYMENT-8001-3/study";
// 带有负载均衡的RestTemplate
@Resource(name = "restTemplateLoadBalanced")
private RestTemplate restTemplateLoadBalanced;
// 127.0.0.1/study/consumer/payment/get/1
@GetMapping("/consumer/payment/get/{id}")
public CommonResult getPayment(@PathVariable Long id)
{
return restTemplateLoadBalanced.getForObject(PAYMENT_SRV + "/provider/payment/get/"+id, CommonResult.class, id);
}
禁用 Eureka 后(ribbon.eureka.enabled=false
,也可以不引入Eureka的包)手动配置PROVIDER-PAYMENT-8001-3
服务的地址
# 通过配置的方式为PROVIDER-PAYMENT-8001-3服务配置负载均衡的规则
PROVIDER-PAYMENT-8001-3:
ribbon:
listOfServers: localhost:8001,localhost:8002
gitee地址中的cloud-ribbon-consumer-order80模块
getForObject有以下3种重载方法
public
该方法提供了三个参数,其中url为请求的地址,responseType为请求响应体body的包装类型,urlVariables为url中的参数绑定
public
该方法与上面的相似,只是用map来封装参数。
public
使用统一资源标识符来封装请求地址和参数
/**
* 对于getForObject的三种重载方法调用demo
* 127.0.0.1/study/restTemplateTestOrder/payment/getObject_1/1
* @param id
* @return
*/
@GetMapping("/restTemplateTestOrder/payment/getObject_1/{id}")
public CommonResult getPayment(@PathVariable Long id)
{
CommonResult commonResult = new CommonResult();
Map<String,CommonResult> resultMap = new HashMap<>();
// getForObject(String url, Class responseType, Object... uriVariables)
CommonResult result1 = restTemplate.getForObject(PAYMENT_SRV + "/restTemplateTestPayment/payment/get_1/" + id, CommonResult.class, id);
CommonResult result2 = restTemplate.getForObject(PAYMENT_SRV + "/restTemplateTestPayment/payment/{1}/{2}", CommonResult.class, "get_1",id);
CommonResult result3 = restTemplate.getForObject(PAYMENT_SRV + "/restTemplateTestPayment/payment/get_2?id=" + id, CommonResult.class, id);
CommonResult result4 = restTemplate.getForObject(PAYMENT_SRV + "/restTemplateTestPayment/payment/get_2?id={1}&serial={2}", CommonResult.class, id,"小明");
resultMap.put("result1",result1);
resultMap.put("result2",result2);
resultMap.put("result3",result3);
resultMap.put("result4",result4);
// getForObject(String url, Class responseType, Map uriVariables)
Map<String, Object> param = new HashMap<>();
param.put("id",id);
param.put("get_1","get_1");
param.put("serial","小王");
CommonResult result5 = restTemplate.getForObject(PAYMENT_SRV + "/restTemplateTestPayment/payment/{get_1}/{id}", CommonResult.class, param);
CommonResult result6 = restTemplate.getForObject(PAYMENT_SRV + "/restTemplateTestPayment/payment/get_2?id={id}&serial={serial}", CommonResult.class, param);
resultMap.put("result5",result5);
resultMap.put("result6",result6);
// getForObject(URI url, Class responseType)
MultiValueMap<String,String> mapParam = new LinkedMultiValueMap();
mapParam.add("serial","小王1");
mapParam.add("serial","小王2");
URI uri = UriComponentsBuilder.fromUriString(PAYMENT_SRV + "/restTemplateTestPayment/payment/get_2?id={id}")
.queryParams(mapParam) // 设置查询参数
.build()
.expand(id) // 替换url中的模板参数
.encode(StandardCharsets.UTF_8) // 设置编码
.toUri();
CommonResult result7 = restTemplate.getForObject(uri, CommonResult.class);
resultMap.put("result7",result7);
commonResult.setData(resultMap);
return commonResult;
}
public ResponseEntity getForEntity(String url, Class responseType, Object... uriVariables)
public ResponseEntity getForEntity(String url, Class responseType, Map uriVariables)
public ResponseEntity getForEntity(URI url, Class responseType)
这3个方法和4.1.1中的3个方法对应。只是返回的是ResponseEntity
。
可通过responseEntity获取响应头,响应状态,响应体等信息。
// 状态信息
HttpStatus statusCode = responseEntity.getStatusCode();
int statusCodeValue = responseEntity.getStatusCodeValue();
// 请求头信息
HttpHeaders headers = responseEntity.getHeaders();
// 响应体
T body = responseEntity.getBody();
关于post请求中第二个参数Object request
说明:
从源码可以看到,如果我们传入的是一个非HttpEntity,则封装为一个HttpEntity。我们在实际调用过程中,可以使用传递一个Object类型的参数,这个参数会被放在请求体body中,这就和前端发送post请求一样。我们就可以调用服务提供者方的post接口。
public T postForObject(String url, @Nullable Object request, Class responseType,Object... uriVariables)
public T postForObject(String url, @Nullable Object request, Class responseType,Map uriVariables)
public T postForObject(URI url, @Nullable Object request, Class responseType)
public ResponseEntity postForEntity(String url, @Nullable Object request,Class responseType, Object... uriVariables)
public ResponseEntity postForEntity(String url, @Nullable Object request,Class responseType, Map uriVariables)
public ResponseEntity postForEntity(URI url, @Nullable Object request, Class responseType)
public URI postForLocation(String url, @Nullable Object request, Object... uriVariables)
public URI postForLocation(String url, @Nullable Object request, Map uriVariables)
public URI postForLocation(URI url, @Nullable Object request)
public ResponseEntity postForObject/postForEntity(URI url, @Nullable Object request, Class responseType)
或 public T getForObject/getForEntity/(URI url, Class responseType)
方法。这实现了重定向的功能。为什么在 RestTemplate 上加了一个 @LoadBalanced 之后,RestTemplate 就能够跟 Eureka整合,让我们不但可以使用服务名称去调用接口,还能够自动通过注册中心的实例集群实现负载均衡,应该归功于 Spring Cloud 给大量的底层适配工作,将这些复杂都封装好了,用起来才会那么简单。
主要的逻辑就是@LoadBalanced修饰的 RestTemplate 会注册一个拦截器,在请求之前在拦截器中对请求的地址进行替换,或者根据具体的负载策略选择服务地址,然后再去调用真实的服务地址,这就是 @LoadBalanced 的原理
LoadBalancerAutoConfiguration类中:
public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
// 实现RestTemplateCustomizer中customize方法的对象,实现逻辑是为restTemplate添加LoadBalancerInterceptor拦截器
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
@Bean
public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
return () -> restTemplateCustomizers.ifAvailable(customizers -> {
for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
for (RestTemplateCustomizer customizer : customizers) {
// 调用RestTemplateCustomizer中customize方法,为restTemplate添加LoadBalancerInterceptor拦截器
customizer.customize(restTemplate);
}
}
});
}
RibbonLoadBalancerClient类:
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
throws IOException {
// 获取负载均衡器,里面包含serviceId下存活的实例
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
// 从负载均衡器中根据轮询规则获取一个实例的地址
Server server = getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server,
isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
InterceptingClientHttpRequest类中的request.getURI()实际调用ServiceRequestWrapper类的getURI()方法,从instance中获取实例的IP地址。
ServiceRequestWrapper类
public URI getURI() {
URI uri = this.loadBalancer.reconstructURI(this.instance, getRequest().getURI());
return uri;
}
锚点
Spring Cloud Netflix默认为Ribbon提供了以下的beans:
Bean类型 | Bean名称 | 类名称 | 功能 |
---|---|---|---|
IClientConfig | ribbonClientConfig | DefaultClientConfigImpl | ribbon相关配置 |
IRule | ribbonRule | ZoneAvoidanceRule | 根据区域和可用性过滤服务器的规则 |
IPing | ribbonPing | DummyPing | IPing在Ribbon 框架中,负责检查服务实例是否存活(UP),DummyPing是IPing实现,永远返回true |
ServerList | ribbonServerList | ConfigurationBasedServerList | 加载服务列表。ConfigurationBasedServerList则是从配置文件加载服务列表 |
ServerListFilter | ribbonServerListFilter | ZonePreferenceServerListFilter | 过滤服务列表,过滤掉所有和客户端环境里的配置的zone的不同的服务,如果和客户端相同的zone不存在,才不进行过滤 |
ILoadBalancer | ribbonLoadBalancer | ZoneAwareLoadBalancer | 负载均衡器 |
ServerListUpdater | ribbonServerListUpdater | PollingServerListUpdater | 动态服务器列表更新器更新的策略 |
注:当Ribbon与Eureka一起使用时
功能:根据特定算法从服务列表选取一个服务进行访问。
RoundRobinRule
:轮询规则,默认规则
AvailabilityFilteringRule
:负载均衡器规则,过滤掉1。由于多次访问故障而处于断路状态的服务,2。并发的连接数量超过阈值的服务。然后对剩余的服务列表按照RoundRobinRule策略进行访问。
WeightedResponseTimeRule
:根据平均响应时间计算所有服务的权重,响应时间越快,服务权重越重,优先被选中
RetryRule
:按照RoundRobinRule的策略获取服务。如果获取服务失败。在指定时间内进行重试,获取可用的服务
BestAvailableRule
:此负载均衡器会过滤由于多次访问故障而处于断路状态的服务,然后选择一个并发量最小的服务
RandomRule
:随意获取一个服务
功能:在后台运行的一个组件,用于检测服务器列表是否运行正常
NIWSDiscoveryPing
:不执行真正的ping,如果DiscoveryClient认为是在线,则程序认为本次心跳成功,服务正常运行
PingUrl
:此组件会使用HttpClient调用一个服务,如果调用成功,则认为本次心跳成功,表示服务正常运行
NoOpPing
:永远返回true,表示服务永远正常
DummyPing
:默认实现,默认返回true,表示服务永远正常
功能:存储服务列表。分为静态和动态,如果为动态,后台有一个县城会定时刷新和过滤服务列表
ConfigurationBasedServerList
:从配置文件中获取所有服务列表。(静态)
DiscoveryEnabledNIWSServerList
:从EurekaClient中获取服务列表。(动态)
DomainExtractingServerList
:代理类,根据ServerList的值实现具体的逻辑
该接口允许过滤配置或动态获取的具有所需特性的服务器列表
ZoneAffinityServerListFilter
:过滤掉所有的不和客户端在相同zone的服务,如果和客户端相同的zone不存在,才不过滤不同zone服务
ZonePreferenceServerListFilter
:ZoneAffinityServerListFilter的子类。和ZoneAffinityServerListFilter相似,但是比较的zone是发布环境里面的zone。过滤掉所有和客户端环境里的配置的zone的不同的服务,如果和客户端相同的zone不存在,才不进行过滤。
ServerListSubsetFilter
:ZoneAffinityServerListFilter的子类。此过滤器确保客户端仅看到由ServerList实现返回的整个服务器的固定子集。它还可以定期用新服务器替代可用性差的子集中的服务器。
功能:被DynamicServerListLoadBalancer用于动态的更新服务列表
PollingServerListUpdater
:默认的实现策略,此对象会启动一个定时线程池,定时执行更新策略
EurekaNotificationServerListUpdater
:当收到缓存刷新的通知梦回更新服务列表。
功能:定义配置信息,用来初始化ribbon客户端和负载均衡器
DefaultClientConfigImpl
:IClientConfig的默认实现,配置文件里的部分值为ribbon。
功能:定义软件负载平衡器操作的接口。动态更新一组服务列表及根据指定算法从现有服务器列表中选择一个服务
DynamicServerListLoadBalancer
:DynamicServerListLoadBalancer组合Rule、IPing、ServerList、ServerListFilter、ServerListUpdater实现类,实现动态更新和过滤更新服务列表
ZoneAwareLoadBalancer
:这是DynamicServerListLoadBalancer的子类,主要加入zone的因素。统计每个zone的平均请求的情况,保证从所有zone选取对当前客户端服务最好的服务组列表
支持的属性:
配置项 | 对应的配置接口 |
---|---|
.ribbon.NFLoadBalancerClassName | ILoadBalancer |
.ribbon.NFLoadBalancerRuleClassName | IRule |
.ribbon.NFLoadBalancerPingClassName | IPing |
.ribbon.NIWSServerListClassName | ServerList |
.ribbon.NIWSServerListFilterClassName | ServerListFilter |
例如为PROVIDER-PAYMENT-8001-3
服务配置负载均衡规则:
PROVIDER-PAYMENT-8001-3:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
这里有个问题:
官网描述的优先级是:配置文件是高于@RibbonClient(configuration=MyRibbonConfig.class)
定义的beans和默认值,但是经过我试验,优先级是@RibbonClient(configuration=MyRibbonConfig.class)
>配置文件>默认值。
ps: 这个地方可能是这样,如果你在@RibbonClient
注解的属性或者是配置文件配置了和默认值一样的配置,那么就等同于你没有配置。未在ribbon验证,因为我再使用feign的时候也遇到类似的问题。
情况如下:
decode404默认值是false。
当在@FeignClient
注解配置decode404=false
,配置文件decode404=true
时,decode404的功能是开启的。
当在@FeignClient
注解配置decode404=true
,配置文件decode404=false
时,decode404的功能也是开启的。
Ribbon 中有两种和时间相关的设置,分别是请求连接的超时时间和请求处理的超时时间,设置规则如下:
ribbon:
ConnectTimeout: 2000 # 请求连接的超时时间
ReadTimeout: 5000 # 请求处理的超时时间
# 也可以为每个Ribbon客户端设置不同的超时时间, 通过服务名称进行指定:
PROVIDER-PAYMENT-8001-3:
ribbon:
ReadTimeout: 5000
ConnectTimeout: 2000
ribbon:
MaxTotalConnections: 500 # 最大连接数
MaxConnectionsPerHost: 500 # 每个host最大连接数
PROVIDER-PAYMENT-8001-3:
ribbon:
MaxTotalConnections: 500
MaxConnectionsPerHost: 500
最简单的方法就是利用 Ribbon 自带的重试策略进行负载均衡和重试,此时只需要指定某个服务的负载策略为重试策略即可:
PROVIDER-PAYMENT-8001-3:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule
或者是针对所有客户端生效:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule
当然也可是改用配置类的方式