(五)SpringCloud-Ribbon负载均衡

1、Spring Cloud Ribbon简介

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/


2、Ribbon提供了七种负载均衡策略

名称解释
RoundRobinRule轮训策略
RandomRule随机策略
BestAvailableRule过滤出故障服务器后,选择一个并发量最小的
WeightedResponseTimeRule针对响应时间加权轮询
AvailabilityFilteringRule可用过滤策略,先过滤出故障的或并发请求大于阈值的一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个
ZoneAvoidanceRule从最佳区域实例集合中选择一个最优性能的服务实例
RetryRule选择一个Server,如果失败,重新选择一个Server重试

3、微服务架构下如何使用Ribbon

3.1 使用Eureka作为注册中心时使用Ribbon

3.1.1 引入ribbon依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

3.1.2 往容器中注入RestTemplate

使用@LoadBalanced注解赋予RestTemplate负载均衡的能力

@Bean
@LoadBalanced
public RestTemplate createRestTemplate(){
    return new RestTemplate();
}

3.1.3 配置RibbonClient

例如:注入负载均衡规则,默认是轮询,更多配置以及配置实现类参见本文档的 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)}))或者将这个类不与启动类放在同一个包下。

3.1.4 为服务指定ribbon配置

// 启动类为某个服务指定负载均衡规则
@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);
    }
}

3.1.5 使用RestTemplate调用服务

 // 通过在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);
}

3.1.6 Demo示例

gitee地址中的cloud-eureka-consumer-order80和cloud-eureka-provider-payment8001-3模块


3.2 在没有Eureka的情况下使用Ribbon

官网教程:
在这里插入图片描述


3.2.1 调用方式一:使用Ribbon的Api

这地方需要注意的是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);
}

3.2.2 调用方式二:使用带负载均衡的RestTemplate

// 通过在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);
}

3.2.3 通过listOfServers配置的服务器列表

禁用 Eureka 后(ribbon.eureka.enabled=false,也可以不引入Eureka的包)手动配置PROVIDER-PAYMENT-8001-3服务的地址

# 通过配置的方式为PROVIDER-PAYMENT-8001-3服务配置负载均衡的规则
PROVIDER-PAYMENT-8001-3:
  ribbon:
    listOfServers: localhost:8001,localhost:8002

3.2.4 完整demo

gitee地址中的cloud-ribbon-consumer-order80模块


4、如何使用RestTemplate

4.1 GET请求

4.1.1 getForObject

getForObject有以下3种重载方法

  • public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables)
    该方法提供了三个参数,其中url为请求的地址,responseType为请求响应体body的包装类型,urlVariables为url中的参数绑定

  • public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
    该方法与上面的相似,只是用map来封装参数。

  • public <T> T getForObject(URI url, Class<T> responseType)
    使用统一资源标识符来封装请求地址和参数

/**
 * 对于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<T> 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<T> responseType, Map<String, ?> 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<T> 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;
}

4.1.2 getForEntity

  • public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables)
  • public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables)
  • public <T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType)

这3个方法和4.1.1中的3个方法对应。只是返回的是ResponseEntity
可通过responseEntity获取响应头,响应状态,响应体等信息。
// 状态信息
HttpStatus statusCode = responseEntity.getStatusCode();
int statusCodeValue = responseEntity.getStatusCodeValue();

// 请求头信息
HttpHeaders headers = responseEntity.getHeaders();

// 响应体
T body = responseEntity.getBody();

4.2 POST请求(使用demo参考4.1的Get请求)

关于post请求中第二个参数Object request说明:
在这里插入图片描述
从源码可以看到,如果我们传入的是一个非HttpEntity,则封装为一个HttpEntity。我们在实际调用过程中,可以使用传递一个Object类型的参数,这个参数会被放在请求体body中,这就和前端发送post请求一样。我们就可以调用服务提供者方的post接口。

4.2.1 postForObject

  • public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,Object... uriVariables)
  • public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType,Map<String, ?> uriVariables)
  • public <T> T postForObject(URI url, @Nullable Object request, Class<T> responseType)

4.2.2 postForObject

  • public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,Class<T> responseType, Object... uriVariables)
  • public <T> ResponseEntity<T> postForEntity(String url, @Nullable Object request,Class<T> responseType, Map<String, ?> uriVariables)
  • public <T> ResponseEntity<T> postForEntity(URI url, @Nullable Object request, Class<T> responseType)

4.2.3 postForLocation

  • public URI postForLocation(String url, @Nullable Object request, Object... uriVariables)
  • public URI postForLocation(String url, @Nullable Object request, Map<String, ?> uriVariables)
  • public URI postForLocation(URI url, @Nullable Object request)
    该方法实现了以POST请求提交资源,并返回新的资源的URI。这样我们就可以将返回的URI作为参数去调用public <T> ResponseEntity<T> postForObject/postForEntity(URI url, @Nullable Object request, Class<T> responseType)public <T> T getForObject/getForEntity/(URI url, Class<T> responseType) 方法。这实现了重定向的功能。

5、@LoadBalanced 注解作用原理简单讲解

为什么在 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);
			}
		}
	});
}

8.1、负载均衡

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);
}

在这里插入图片描述

8.2、地址替换

InterceptingClientHttpRequest类中的request.getURI()实际调用ServiceRequestWrapper类的getURI()方法,从instance中获取实例的IP地址。
在这里插入图片描述

ServiceRequestWrapperpublic URI getURI() {
	URI uri = this.loadBalancer.reconstructURI(this.instance, getRequest().getURI());
	return uri;
}

6、Ribbon的相关配置

锚点
Spring Cloud Netflix默认为Ribbon提供了以下的beans:

Bean类型Bean名称类名称功能
IClientConfigribbonClientConfigDefaultClientConfigImplribbon相关配置
IRuleribbonRuleZoneAvoidanceRule根据区域和可用性过滤服务器的规则
IPingribbonPingDummyPingIPing在Ribbon 框架中,负责检查服务实例是否存活(UP),DummyPing是IPing实现,永远返回true
ServerListribbonServerListConfigurationBasedServerList加载服务列表。ConfigurationBasedServerList则是从配置文件加载服务列表
ServerListFilterribbonServerListFilterZonePreferenceServerListFilter过滤服务列表,过滤掉所有和客户端环境里的配置的zone的不同的服务,如果和客户端相同的zone不存在,才不进行过滤
ILoadBalancerribbonLoadBalancerZoneAwareLoadBalancer负载均衡器
ServerListUpdaterribbonServerListUpdaterPollingServerListUpdater动态服务器列表更新器更新的策略

注:当Ribbon与Eureka一起使用时

  • 则DiscoveryEnabledNIWSServerList作为ribbonServerList的实现类
  • NIWSDiscoveryPing则是作为IPing接口的实现,该接口会委托给Eureka用于确定服务器是否启动

6.1 IRule

功能:根据特定算法从服务列表选取一个服务进行访问。

RoundRobinRule:轮询规则,默认规则
AvailabilityFilteringRule:负载均衡器规则,过滤掉1。由于多次访问故障而处于断路状态的服务,2。并发的连接数量超过阈值的服务。然后对剩余的服务列表按照RoundRobinRule策略进行访问。
WeightedResponseTimeRule:根据平均响应时间计算所有服务的权重,响应时间越快,服务权重越重,优先被选中
RetryRule:按照RoundRobinRule的策略获取服务。如果获取服务失败。在指定时间内进行重试,获取可用的服务
BestAvailableRule:此负载均衡器会过滤由于多次访问故障而处于断路状态的服务,然后选择一个并发量最小的服务
RandomRule:随意获取一个服务

6.2 IPing

功能:在后台运行的一个组件,用于检测服务器列表是否运行正常

NIWSDiscoveryPing:不执行真正的ping,如果DiscoveryClient认为是在线,则程序认为本次心跳成功,服务正常运行
PingUrl:此组件会使用HttpClient调用一个服务,如果调用成功,则认为本次心跳成功,表示服务正常运行
NoOpPing:永远返回true,表示服务永远正常
DummyPing:默认实现,默认返回true,表示服务永远正常

6.3 ServerList

功能:存储服务列表。分为静态和动态,如果为动态,后台有一个县城会定时刷新和过滤服务列表

ConfigurationBasedServerList:从配置文件中获取所有服务列表。(静态)
DiscoveryEnabledNIWSServerList:从EurekaClient中获取服务列表。(动态)
DomainExtractingServerList:代理类,根据ServerList的值实现具体的逻辑

6.4 ServerListFilter

该接口允许过滤配置或动态获取的具有所需特性的服务器列表

ZoneAffinityServerListFilter:过滤掉所有的不和客户端在相同zone的服务,如果和客户端相同的zone不存在,才不过滤不同zone服务
ZonePreferenceServerListFilter:ZoneAffinityServerListFilter的子类。和ZoneAffinityServerListFilter相似,但是比较的zone是发布环境里面的zone。过滤掉所有和客户端环境里的配置的zone的不同的服务,如果和客户端相同的zone不存在,才不进行过滤。
ServerListSubsetFilter:ZoneAffinityServerListFilter的子类。此过滤器确保客户端仅看到由ServerList实现返回的整个服务器的固定子集。它还可以定期用新服务器替代可用性差的子集中的服务器。

6.5 ServerListUpdater

功能:被DynamicServerListLoadBalancer用于动态的更新服务列表

PollingServerListUpdater:默认的实现策略,此对象会启动一个定时线程池,定时执行更新策略
EurekaNotificationServerListUpdater:当收到缓存刷新的通知梦回更新服务列表。

6.6 IClientConfig

功能:定义配置信息,用来初始化ribbon客户端和负载均衡器

DefaultClientConfigImpl:IClientConfig的默认实现,配置文件里的部分值为ribbon。

6.7 ILoadBalancer

功能:定义软件负载平衡器操作的接口。动态更新一组服务列表及根据指定算法从现有服务器列表中选择一个服务

DynamicServerListLoadBalancer:DynamicServerListLoadBalancer组合Rule、IPing、ServerList、ServerListFilter、ServerListUpdater实现类,实现动态更新和过滤更新服务列表
ZoneAwareLoadBalancer:这是DynamicServerListLoadBalancer的子类,主要加入zone的因素。统计每个zone的平均请求的情况,保证从所有zone选取对当前客户端服务最好的服务组列表

7、如何通过设置Properties配置文件自定义Ribbon客户端

支持的属性:

配置项对应的配置接口
.ribbon.NFLoadBalancerClassNameILoadBalancer
.ribbon.NFLoadBalancerRuleClassNameIRule
.ribbon.NFLoadBalancerPingClassNameIPing
.ribbon.NIWSServerListClassNameServerList
.ribbon.NIWSServerListFilterClassNameServerListFilter

例如为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的功能也是开启的


8、Ribbon的其他配置

8.1 超时时间

Ribbon 中有两种和时间相关的设置,分别是请求连接的超时时间和请求处理的超时时间,设置规则如下:


ribbon:	
  ConnectTimeout: 2000 # 请求连接的超时时间
  ReadTimeout: 5000 # 请求处理的超时时间

# 也可以为每个Ribbon客户端设置不同的超时时间, 通过服务名称进行指定:
PROVIDER-PAYMENT-8001-3: 
  ribbon:
    ReadTimeout: 5000
    ConnectTimeout: 2000

8.2 并发参数

ribbon:
	MaxTotalConnections: 500 # 最大连接数
	MaxConnectionsPerHost: 500 # 每个host最大连接数
	
PROVIDER-PAYMENT-8001-3: 
  ribbon:
    MaxTotalConnections: 500
    MaxConnectionsPerHost: 500

9、使用RetryRule配置重试

最简单的方法就是利用 Ribbon 自带的重试策略进行负载均衡和重试,此时只需要指定某个服务的负载策略为重试策略即可:

PROVIDER-PAYMENT-8001-3:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule

或者是针对所有客户端生效:
ribbon:
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RetryRule

当然也可是改用配置类的方式

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值