一、Ribbon概述
Spring Cloud Ribbon是 Netflix 公司开源的一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现,负载均衡的行为在客户端发生。
通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括 Feign,它也是基于Ribbon实现的工具。
文章为了更贴切 Ribbon 主题,所以使用 RestTemplate 充当网络调用工具
RestTemplate 是 Spring Web 下提供访问第三方 RESTFul Http 接口的网络框架
Eureka的jar包中包含Ribbon。
实现:负载均衡+RestTemplate 调用
- Ribbon工作时有两步
- 第一步先选择 EurekaServer,优先选择统一区域负载较少的 server
- 第二部再根据用户指定的策略,从server取到的服务注册列表中选择一个地址。其中 Riibon 提供了多种策略(轮询,随机,根据响应时间加权)。
二、RestTemplate的使用
1、配置类ApplicationContextConfig
RestTemplate
默认是没有负载均衡的,所以需要添加 @LoadBalanced
@Configuration
public class ApplicationContextConfig {
@Bean
//赋予RestTemplate负载均衡的能力,实现轮询
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
2、getFor***()
区别:
getForObject()
返回对象为响应体中的数据转化成的对象,可以理解为json串(推荐使用)getForEntity()
返回ResponseEntity
对象,包含响应中的一些重要信息,包括响应头、状态码、响应体等。
@RestController
@Slf4j
public class OrderController {
//服务名,用于查找对应的服务集群
public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
//服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。
@Resource
private RestTemplate restTemplate;
//此处最后要加/,
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") Long id){
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
}
@GetMapping("/consumer/payment/getEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id){
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
//通过响应状态码判断是否成功访问
if (entity.getStatusCode().is2xxSuccessful()){
log.info("200");
return entity.getBody();
}else{
return new CommonResult<>(444,"操作失败");
}
}
}
3、@LoadBalanced注意事项
注意:加了@LoadBalanced
后,
不能直接访问地址,需要把地址改成你所调用的ur在eureka上注册的application.name
加了注解 @LoadBalanced 之后,我们的restTemplate 会走这个类RibbonLoadBalancerClient,serverid必须是我们访问的服务名称 ,当我们直接输入ip的时候获取的server是null,就会抛出异常
因为ribbon的作用是负载均衡,那么你直接使用ip地址,那么就无法起到负载均衡的作用,因为每次都是调用同一个服务,当你使用的是服务名称的时候,他会根据自己的算法去选择具有该服务名称的服务。
No instances available for localhost
三、核心组件IRule
IRule:根据特定算法从服务列表中选取一个要访问的服务,即改变RestTemplate的负载均衡规则
1、IRule默认自带的负载规则
(1)RoundRobinRule
轮询策略:Ribbon 默认采用的策略。若经过一轮轮询没有找到可用的provider,其最多轮询 10 轮(代码中写死的,不能修改)。若还未找到,则返回 null。
(2)RandomRule
随机策略:从所有可用的 provider 中随机选择一个。
(3)RetryRule
重试策略:先按照 RoundRobinRule 策略获取 server,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。
重试策略有个子规则,默认是轮询。先通过子规则得到服务,如果服务不可用,开启一个定时任务,在这个定时任务生效之前不断循环选取服务,直到选择一个合适的服务。
(4)BestAvailableRule
最可用策略:选择并发量最小的 provider,即连接的消费者数量最少的provider。其会遍历服务列表中的每一个server,选择当前连接数量minimalConcurrentConnections 最小的server。
(5)AvailabilityFilteringRule
可用性敏感策略,过滤掉性能差的服务
可用过滤算法:该算法规则是过滤掉处于熔断状态的 server 与已经超过连接极限的server,对剩余 server 采用轮询策略。
过滤掉那些一直连接失败的且被标记为 circuit tripped 的后端 Server,并过滤掉那些高并发的后端 Server 或者使用一个 AvailabilityPredicate 来包含过滤 Server 的逻辑。其实就是检查 Status 里记录的各个 Server 的运行状态。
(6)ZoneAvoidanceRule
区域敏感性策略,综合考虑区域可用性和服务可用性,默认规则
使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 来判断是否选择某个 Server,前一个判断判定一个 Zone 的运行性能是否可用,剔除不可用的 Zone(的所有 Server),AvailabilityPredicate 用于过滤掉连接数过多的 Server。
(7)WeightedResponseTimeRule
权重轮询策略,响应时间越短,越容易被选上
根据响应时间分配一个 Weight(权重),响应时间越长,Weight 越小,被选中的可能性越低。
(8)ResponseTimeWeightedRule
作用同 WeightedResponseTimeRule,ResponseTimeWeightedRule 后来改名为 WeightedResponseTimeRule。
(9)ClientConfigEnabledRoundRobinRule
客户端可配轮询,实际上就是轮询策略
2、自定义规则
优先同与调用者同 IP 的服务,如果没有那就随机
/**
* nacos dev环境微服务调度规则
* 优先同IP
* debugger.local-ip 请配置为本机在nacos上的那个IP
*/
public class DevNacosDebugRule extends AbstractLoadBalancerRule {
protected final Logger logger = LoggerFactory.getLogger(this.getClass());
//@Value("${debugger.local-ip:}")
private String localIp;
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
/**
* 重写choose方法
*/
@SneakyThrows
@Override
public Server choose(Object key) {
//获取负载均衡器
BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) getLoadBalancer();
//调用服务的名字
String invokedServerName = baseLoadBalancer.getName();
//获取namingServer(包含nacos注册发现相关api)
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
//获取被调用的服务的所有实例
List<Instance> invokedAllInstanceList = namingService.getAllInstances(invokedServerName);
if (invokedAllInstanceList.isEmpty()) {
logger.warn("no instance in service {}", invokedServerName);
return null;
}
String localIp = this.getLocalhostIp();
for (Instance instance : invokedAllInstanceList) {
// 同 ip 优先级最高,匹配到直接返回
if (instance.getIp().equals(localIp)) {
return new NacosServer(instance);
}
}
Random random = new Random();
Instance instance = invokedAllInstanceList.get(random.nextInt(invokedAllInstanceList.size()));
return new NacosServer(instance);
}
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
/**
* 获取本地ip
*/
public String getLocalhostIp() {
if (!StringUtils.isEmpty(localIp)) {
return localIp;
}
try {
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = allNetInterfaces.nextElement();
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress ip = addresses.nextElement();
if (ip instanceof Inet4Address
&& !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
&& !ip.getHostAddress().contains(":")) {
return ip.getHostAddress();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
3、替换负载规则-原生Ribbon
(1)新建配置类
注意:注意:IRule配置类不能放在@ComponentSan 的包及子包下,因为默认的扫描会变成全局负载均衡都按照这样的规则。而@SpringBootApplication
就包含这个注解,所以不能和主启动类放在一个包下。
public class MySelfRule {
@Bean
public IRule myRule(){
return new RandomRule();//定义为随机
}
}
(2)主启动类中添加注解
// 选择要接收的服务和配置类
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
4、替换负载规则-Nacos
(1)全局配置
只需把 IRule
的实现类注入到容器中即可
(2)指定服务
# 针对调用 ribbon-order-service 服务时生效
ribbon-order-service:
ribbon:
NFLoadBalancerRuleClassName: com.rewind.config.MyRule1
# 针对调用 ribbon-stock-service 服务时生效
ribbon-stock-service:
ribbon:
NFLoadBalancerRuleClassName: com.rewind.config.MyRule2
此种方法自定义的规则类中,可使用 @Autowired
注入 bean
四、Ribbon底层原理
更多原理说明:https://juejin.cn/post/6933767343570944008#heading-13
底层原理实现这一块内容,会先说明使用 Ribbon 负载均衡调用远端请求的全过程,然后着重看一下 RandomRule 负载策略底层是如何实现
- 创建 ILoadBalancer 负载均衡客户端,初始化 Ribbon 中所需的 定时器和注册中心上服务实例列表
- 从 ILoadBalancer 中,通过 负载均衡选择出健康服务列表中的一个 Server
- 将服务名(ribbon-produce)替换为 Server 中的 IP + Port,然后生成 HTTP 请求进行调用并返回数据
上面已经说过,ILoadBalancer 是负责负载均衡路由的,内部会使用 IRule 实现类进行负载调用
public interface ILoadBalancer {
public Server chooseServer(Object key);
...
}
chooseServer 流程中调用的就是 IRule 负载策略中的 choose 方法,在方法内部获取一个健康 Server
public Server choose(ILoadBalancer lb, Object key) {
...
Server server = null;
while (server == null) {
...
List<Server> upList = lb.getReachableServers(); // 获取服务列表健康实例
List<Server> allList = lb.getAllServers(); // 获取服务列表全部实例
int serverCount = allList.size(); // 全部实例数量
// 全部实例数量为空,返回 null,相当于错误返回
if (serverCount == 0) {
return null;
}
// 考虑到效率问题,使用多线程 ThreadLocalRandom 获取随机数
int index = chooseRandomInt(serverCount);
// 获取健康实例
server = upList.get(index);
if (server == null) {
// 作者认为出现获取 server 为空,证明服务列表正在调整,但是!这只是暂时的,所以当前释放出了 CPU
Thread.yield();
continue;
}
if (server.isAlive()) { // 服务为健康,返回
return (server);
}
...
}
return server;
}
简单说一下随机策略 choose 中流程
- 获取到全部服务、健康服务列表,判断全部实例数量是否等于 0,是则返回 null,相当于发生了错误
- 从全部服务列表里获取下标索引,然后去 健康实例列表获取 Server
- 如果获取到的 Server 为空会放弃 CPU,然后再来一遍上面的流程,相当于一种重试机制
- 如果获取到的 Server 不健康,设置 Server 等于空,再歇一会,继续走一遍上面的流程
比较简单,有小伙伴可能就问了,如果健康实例小于全部实例怎么办?这种情况下存在两种可能
- 运气比较好,从全部实例数量中随机了比较小的数,刚好健康实例列表有这个数,那么返回 Server
- 运气比较背,从全部实例数量中随机了某个数,健康实例列表数量为空或者小于这个数,直接会下标越界异常
为什么不直接从健康实例中选择实例呢
如果直接从健康实例列表选择,就能规避下标越界异常,为什么作者要先从全部实例中获取 Server 下标?