🌈往期回顾
第一期:Spring Cloud Alibaba前景如何?
第二期:Spring Cloud Alibaba Nacos服务治理
第三期:Spring Cloud Alibaba Sentinel - 分布式系统的流量防卫兵
本篇文章主要讲述微服务实现负载均衡的两种比较常见方式:服务端负载均衡、客户端负载均衡,以一个典型的例子来说明Ribbon的作用,现在有两个微服务:用户中心、内容中心,假设现在部署了三个用户中心实例,内容中心相对用户中心来说就是一个客户端client,内容中心需要调用用户中心,我们可以在内容中心通过自定义负载均衡规则调用用户中心实例,使用Ribbon+Nacos手写负载均衡算法实现客户端的负载均衡。
服务端负载均衡
像以往的单体架构,一般可以部署多个实例,通过负载实现反向代理,由Nginx负载均衡算法实现服务端均衡,如下图所示:
客户端负载均衡
客户端负载均衡实现思想其实很简单,我们只需要在获取实例列表instances中,随机获取列表的实例instance,而不是获取第一个,这样也就能达到随机负载的目的,假设现在有三个用户中心实例,内容中心已经可以通过DiscoveryClient获取到用户中心信息,现在内容中心实现一个负载均衡规则计算将请求通过RestTemplate请求到某一个实例,这种就是客户端侧负载均衡:
手写客户端负载均衡策略
手写一个随机负载均衡算法,选择其中一个用户实例:
@Override
public PostDTO findById(Integer id) {
//获取帖子详情
Post post = postMapper.selectById(id);
//发布人id
Integer userId = post.getUserId();
//实现客户端负载均衡的第一步:先拿到所有用户实例的请求目标地址targetUrls
List<String> targetUrls = instances.stream().map(instance -> instance.getUri().toString() + "/user/{id}").collect(Collectors.toList());
int i = ThreadLocalRandom.current().nextInt(targetUrls.size());
String targetURL = targetUrls.get(i);
log.info("请求目标地址:{}", targetURL);
UserDTO userDTO = restTemplate.getForObject(targetURL, UserDTO.class, userId);
//消息装配
PostDTO postDTO = new PostDTO();
BeanUtils.copyProperties(post, postDTO);
postDTO.setWxNickname(userDTO.getNickname());
return postDTO;
}
模拟启动两个用户实例:端口分别是8080和8081,内容中心(9081)服务调用用户中心服务,现在将用户中心和内容中心都注册到Nacos中:
用户中心部署启用了两个实例,点击用户中心实例详情:
在内容中心模拟发送请求,观察发现请求随机打到8080或者8081两个端口的实例上,至此我们的客户端负载均衡算法实现完成。
整合Ribbon实现负载均衡
Ribbon介绍
Ribbon是Netflix发布的云中间层服务开源项目,其主要功能是提供客户端实现负载均衡算法。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,Ribbon是一个客户端负载均衡器,我们可以在配置文件中Load Balancer后面的所有机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器,我们也很容易使用Ribbon实现自定义的负载均衡算法。
简单来说Ribbon就是我们客户端的一个负载均衡器,也就是用来简化我们上面写的算法实现负载均衡使用的,而且更强大,更丰富的负载均衡算法。
Ribbon的组成
| Riibon组件 | 组件说明 | 默认值 |
|---|---|---|
| IClientConfig | 用来读取配置 | DefaultClientConfigImpl |
| IRule | 为Ribbon提供负载均衡规则,从而选择实例 | ZoneAvoidanceRule |
| IPing | 筛选掉ping不通的实例 | DymmyPing |
| ServerList | 获取实例列表,交给Ribbon的实例列表 | Ribbon: ConfigurationBasedServerList Spring Cloud Alibaba: NacosServerList |
| ServerListFilter | 过滤掉不符合条件的实例 | ZonePreferenceServerListFilter |
| IloadBalancer | Ribbon负载均衡的入口 | ZoneAwareLoadBalancer |
| ServerListUpdater | 更新交给Ribbon的List策略 | PollingServerListUpdater |
ServerList
想要在内容中心获取用户实例,可以通过discoveryClient.getInstances("user-center")获取:
/***
* @Description: 测试:服务发现,证明内容中心总能找到用户中心
* @Author: Jacklin
* @Date: 2021/12/22
*/
@GetMapping(value = "/getInstances")
private List<ServiceInstance> getInstances() {
// 查询指定服务的所有实例信息
List<ServiceInstance> instances = this.discoveryClient.getInstances("user-center");
if (CollectionUtil.isNotEmpty(instances)) {
instances.stream().forEach(o -> {
System.out.println(o.getUri());
});
}
return instances;
}
Ribbon内置的负载均衡规则
| 负载均衡规则 | 规则说明 |
|---|---|
| AvailabilityFilteringRule | 先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,以及并发连接数超过阈值的服务,剩下的服务,使用轮询策略 |
| BestAvailableRule | 先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 |
| ZoneAvoidanceRule | 复合判断 server 所在区域的性能和 server 的可用性选择服务器 |
| RandomRule | 随机负载均衡 |
| RoundRibbonRule | 轮询,人人有份,一个个来 |
| RetryRule | 先轮询,如果获取失败则在指定时间内重试,重新轮询可用的服务 |
| WeightedResponseTimeRule | 根据平均响应时间计算所有服务的权重,响应越快的服务权重越高,越容易被选中。一开始启动时,统计信息不足的情况下,使用轮询 |
🎉Ribbon的默认负载均衡策略采用的是:轮训负载均衡策略。
细粒度配置自定义
通常实现配置的方式主要有两种:
- Java代码配置
- 配置文件配置属性配置
Java代码配置
/**
* 为restTemplate整合ribbon
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@Override
public PostDTO findById(Integer id) {
//获取帖子详情
Post post = postMapper.selectById(id);
//发布人id
Integer userId = post.getUserId();
//整合ribbon实现负载均衡,ribbon会自动将user-center转化成它在nacos的地址
UserDTO userDTO = this.restTemplate.getForObject("http://user-center/user/{id}", UserDTO.class, userId);
//消息装配
PostDTO postDTO = new PostDTO();
BeanUtils.copyProperties(post, postDTO);
postDTO.setWxNickname(userDTO.getNickname());
return postDTO;
}
通过properties自定义Ribbon客户端、指定负载均衡算法
从版本1.2.0开始,Spring Cloud Netflix现在支持通过将属性设置为与 Ribbon文档 兼容来自定义Ribbon客户端。
这使您可以在启动时在不同环境中更改行为。
以下列表显示了受支持的属性>:
<clientName>.ribbon.NFLoadBalancerClassName:应实施ILoadBalancer<clientName>.ribbon.NFLoadBalancerRuleClassName:应实施IRule<clientName>.ribbon.NFLoadBalancerPingClassName:应实施IPing<clientName>.ribbon.NIWSServerListClassName:应实施ServerList<clientName>.ribbon.NIWSServerListFilterClassName:应实施ServerListFilter
这些属性中定义的类优先于使用@RibbonClient(configuration=MyRibbonConfig.class)定义的beans和Spring Cloud Netflix提供的默认值。
要为名为user-center的服务名称设置IRule,可以设置以下属性:
application.yml
user-center:
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.WeightedResponseTimeRule
整合了Ribbon之后,只需要:this.restTemplate.getForObject("http://user-center/user/{id}", UserDTO.class, userId); Ribbon会user-center转化成它在Nacos上的地址。
Ribbon细粒度配置总结
- 尽量使用属性配置,属性配置方式实现不了的情况下再考虑代码配置
- 再同一个微服务内尽量保持单一性配置,比如同一使用属性配置,不要两种方式混用,增加定位代码的复杂性
指定Ribbon负载均衡算法
package com.jacklin.mamba.contentcenter.post.config;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RoundRobinRule;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* 配置RestTemplate
*
* @Author: jacklin
* @Date: 2021-12-30 22:24
*/
@Configuration
public class RestTemplateConfig {
/**
* 为restTemplate整合ribbon
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
/**
* Ribbon 自带的负载均衡策略有如下几个:
* 1.AvailabilityFilteringRule: 先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,以及并发连接数超过阈值的服务,剩下的服务,使用轮询策略
* 2.BestAvailableRule: 先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
* 3.ZoneAvoidanceRule: 复合判断 server 所在区域的性能和 server 的可用性选择服务器
* 4.RandomRule: 随机负载均衡
* 5.RoundRibbonRule:轮询,人人有份,一个个来
* 6.RetryRule:先轮询,如果获取失败则在指定时间内重试,重新轮询可用的服务
* 7.WeightedResponseTimeRule:根据平均响应时间计算所有服务的权重,响应越快的服务权重越高,越容易被选中。一开始启动时,统计信息不足的情况下,使用轮询
*
*/
@Bean
public IRule myCustomRule() {
//轮询策略负载均衡算法,人人有份,一个个来
return new RoundRobinRule();
}
}
由于配置使用RoundRobinRule(轮询策略),所以通过RestTemplate连续请求6次,请求到达连个用户实例的顺序是:先8080->8081->8080->8081->8080->8081,轮着来,这样就实现了负载均衡轮询策略啦~
Ribbon核心源码分析
IRule - 实现负载均衡规提供的接口
/**
* 为 LoadBalancer 定义"规则"的接口,可以将规则视为负载平衡的策略,负载平衡策略包括循环、 基于响应时间等。
*
*/
public interface IRule{
public Server choose(Object key);
public void setLoadBalancer(ILoadBalancer lb);
public ILoadBalancer getLoadBalancer();
}
ILoadBalancer - 负载均衡器
/**
* 负载均衡器操作的接口。一个典型的负载均衡器至少需要一组服务器来进行负载均衡,
* 一种将特定服务器标记为不循环的方法,以及一个从现有服务器列表中选择服务器的调用。
*/
public interface ILoadBalancer {
/**
* 用于为负载均衡器添加具体的服务器,newServers指的是要添加的新服务器
*/
public void addServers(List<Server> newServers);
/**
* 从负载均衡器中选择一个服务器,根据key获取返回对应的服务器
*/
public Server chooseServer(Object key);
/**
* 由负载均衡器的客户端调用通知该服务器已关闭
*/
public void markServerDown(Server server);
@Deprecated
public List<Server> getServerList(boolean availableOnly);
/**
* 获取返回已启动且可以访问的服务器
*/
public List<Server> getReachableServers();
/**
* 获取所有已知的服务器,可访问和不可访问
*/
public List<Server> getAllServers();
}
AbstractLoadBalancerRule
AbstractLoadBalancerRule(抽象负载均衡规则)是Ribbon为设置和获取负载均衡器已经为我们提供了默认的实现类了,可以通过继承AbstractLoadBalancerRule来自定义我们的负载均衡规则算法,比如,随机负载均衡规则RandomRule也是通过继承AbstractLoadBalancerRule的方式去实现:
通过分析RandomRule代码可知,核心的随机算法逻辑:
- Ribbon负载均衡器首先通过getAllServers()方法获取索引的服务器。
- 通过随机算法从获取到的服务器列表中,随机抽取一个。
protected int chooseRandomInt(int serverCount) {
return ThreadLocalRandom.current().nextInt(serverCount);
}
int index = chooseRandomInt(serverCount);
server = upList.get(index);
到此,Spring Cloud Alibaba Ribbon介绍已经介绍,有兴趣的话可以进一步阅读它的源码,学习一下别人优秀的设计思想。