一、背景介绍
项目中使用到的SpringCloud Alibaba这一套微服务架构中服务注册与发现Nacos兼容了Feign,而Feign默认集成了Ribbon,当Nacos下使用Feign默认实现了负载均衡的效果。即使是默认集成了,也要追根溯源。
二、过程
负载均衡是什么?
将请求分摊到多个服务器上去执行
为什么要负载均衡?
分担压力,当开发的应用同时被成千上万,甚至更多用户同时访问的时候,并发问题就出现了,如果所有的请求都使用同一台机器,可能这个机器无法承受同时的高并发,这时候就可以将大量请求分发给不同的机器,应用的处理性能(吞吐量、网络处理能力)就需要得到提高
故障转移,实现高可用。如果某台服务器坏了(机器停机、进程异常等),其他服务器可以提供相同服务,顶替上岗
安全防护。可以实现过滤,如黑白名单处理等
一个请求是如何到达服务后端的?
①、Nginx:通过在前端接收到来自客户端的请求,将请求分发到后端服务器,实现负载均衡分发HTTP请求
假设我现在在浏览器访问【**https://internetbar.tech/root/user-service/api/login**】这个地址,通过HTTP请求到达【internetbar.tech】这个服务器的【Nginx】,Nginx接收到请求后根据请求的路径,在默认配置文件【nginx.conf】中找到对应的配置。 Nginx的配置文件中会有对GateWay的代理配置。
在http字段中配置的upstream api模块中定义了需要反向代理的服务器地址及端口,将要进行负载均衡的时候就会从这两个服务器进行选择。根据请求路径中的/root/进行匹配发现配置了proxy_pass模块,结合配置的负载均衡策略—8080端口的权重大于6688的权重,Nginx优先将请求转发到8080这个端口对应的服务器地址,在将请求转发给8080这个服务之前,Nginx还可以进行一些预处理操作,比方说请求的重定向、添加请求头等等
②、GateWay:路由转发、负载均衡
此时,8080端口的Gateway接收到通过Nginx代理的请求,GateWay与Nacos集成,并且GateWay配置的各个微服务的信息都通过Nacos做了服务配置。
请求到达GateWay服务的过滤器后,根据配置文件中预先定义的路由规则:
发现请求的Url中包含/user-service/,满足配置的断言规则,GateWay确定请求要转发到的目标服务是【internetbar-provider-user】。GateWay定时从Nacos注册中心拉取服务列表(Nacos中记录了服务名、ip地址、端口号等服务实例信息)动态地获取可用的服务实例,拉取到列表后发现此时【internetbar-provider-user】服务有多个实例。
③、Ribbon:GateWay与Ribbon来实现对服务的负载均衡
在转发请求时,GateWay会利用Ribbon进行负载均衡,利用Ribbon提供的负载均衡算法(默认是使用轮询算法,可以自定义策略)选择一个健康的服务实例来处理请求
根据负载均衡发生的位置不同分为两类
①、服务端负载均衡:发生在服务提供者一方,如nginx负载均衡
客户端发送请求到达Nginx,Nginx通过负载均衡策略选择其中的一个服务地址进行访问,此时Nginx作为了服务端
②、客户端负载均衡:发生在服务请求一方
在图中GateWay作为了客户端,通过集成ribbon根据负载均衡策略,在发送请求前通过负载均衡策略选择目标服务器,再将请求分发到不同的服务端。此时发送请求的GateWay作为客户端
Ribbon是什么?
是基于Netflix Ribbon实现的一套客户端负载均衡的工具。是Spring Cloud的一个组件,通过提供使用提供的注解我们就能自动化的实现负载均衡
Ribbon的作用是什么?
实现负载均衡和服务调用
如何使用Ribbon?
添加@LoadBalance注解
package cn.itcast.order;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
/**
* @Description: 将RestTemplate注册到容器中
* @Author: denglimei
* @Date: 2023/3/13 11:28
* @return: org.springframework.web.client.RestTemplate
**/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
此时我们通过浏览器进行四次访问,我们看看结果:
说明调用成功,此时我们看看idea中的四次访问分别是怎么样的一个顺序呢?
根据每次对userservice这个服务的访问来看,是采用了轮询的策略。内部到底如何进行负载均衡的?策略有哪些?是否默认采用用轮询,我们来看看源码如何说的!
Ribbon负载均衡的策略由哪些?
策略名 | 策略描述 | 实现说明 |
BestAvailableRule | 选择一个最小的并发请求的server | 逐个考察Server,如果Server被tripped了,则忽略,在选择其中ActiveRequestsCount最小的server |
AvailabilityFilteringRule | 过滤掉因为连接失败被标记为circuit tripped的后端server,和高并发的后端server(active connections超过配置的阈值) | 使用一个AvailabilityPredicate来包含过滤server的逻辑,检查status里记录的各个server的运行状 态 |
WeightedResponseTimeRule | 根据相应时间分配一个weight,相应时间越长,weight越小,被选中的可能性越低 | 一个后台线程定期的从status里面读取评价响应时间,为每个server计算一个weight。Weight的计算也比较简单,responsetime减去每个server自己平均的responsetime是server的权重。当刚开始运行,没有形成statas时,使用RoundRobinRule策略选择server。 |
RetryRule | 对选定的负载均衡策略进行重试机制 | 在一个配置时间段内当选择server不成功,则一直尝试使用subRule的方式选择一个可用的server |
RoundRobinRule | 轮询方式轮询选择server | 轮询index,选择index对应位置的server |
RandomRule | 随机选择一个server | 在index上随机,选择index对应位置的server |
ZoneAvoidanceRule | 复合判断server所在区域的性能和server的可用性选择server | 使用ZoneAvoidancePredicate和AvailabilityPredicate来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用于过滤掉连接数过多的Server。 |
Ribbon自定义负载均衡策略
方式一:代码方式,定义随机策略
package cn.itcast.order;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
/**
* @Description: 将RestTemplate注册到容器中
* @Author: denglimei
* @Date: 2023/3/13 11:28
* @return: org.springframework.web.client.RestTemplate
**/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
/**
* @Description: 修改负载均衡策略方式
* @Author: denglimei
* @Date: 2023/3/13 19:56
* @return: com.netflix.loadbalancer.IRule
**/
@Bean
public IRule randomRule() {
return new RandomRule();
}
}
方式二:配置文件方式
userservice:
ribbon:
MFLoadBalancerRuleClassName: com.netlix.loadbalancer.RandomRule #负载均衡规则
不同:
代码方式:只要是order访问任何其他服务都采用随即方式
配置文件方式:只针对order这个服务
优缺点:
代码方式:配置灵活,但修改时需要重新打包发布
配置方式:直观,方便,无需重新打包发布,但是无法做全局配置
通过浏览器访问order服务,五次调用效果如下:
其中1-4次分别调用了userservice的8081这个端口;第5次调用了userservice的8082这个端口
Ribbon实现原理
1、获取请求信息(url、服务名称……)
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null,
"Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName,
this.requestFactory.createRequest(request, body, execution));
}
实现负载均衡的方法—execute
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
return this.execute(serviceId, (LoadBalancerRequest)request, (Object)null);
}
对外暴露的接口,也是实现负载均衡的关键,具体实现逻辑如下:
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
//拉取服务列表
ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
//选择服务
Server server = this.getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
} else {
RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
}
}
this.getLoadBalancer(serviceId)方法内部实现逻辑:
2、根据服务名称拉取服务列表
this.getServer(loadBalancer, hint)方法内部实现逻辑
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
}
3、选择服务
public Server chooseServer(Object key) {
if (ENABLED.get() && this.getLoadBalancerStats().getAvailableZones().size() > 1) {
Server server = null;
try {
LoadBalancerStats lbStats = this.getLoadBalancerStats();
Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
logger.debug("Zone snapshots: {}", zoneSnapshot);
if (this.triggeringLoad == null) {
this.triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2D);
}
if (this.triggeringBlackoutPercentage == null) {
this.triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty("ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999D);
}
Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, this.triggeringLoad.get(), this.triggeringBlackoutPercentage.get());
logger.debug("Available zones: {}", availableZones);
if (availableZones != null && availableZones.size() < zoneSnapshot.keySet().size()) {
String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
logger.debug("Zone chosen: {}", zone);
if (zone != null) {
BaseLoadBalancer zoneLoadBalancer = this.getLoadBalancer(zone);
server = zoneLoadBalancer.chooseServer(key);
}
}
} catch (Exception var8) {
logger.error("Error choosing server using zone aware logic for load balancer={}", this.name, var8);
}
if (server != null) {
return server;
} else {
logger.debug("Zone avoidance logic is not invoked.");
return super.chooseServer(key);
}
} else {
logger.debug("Zone aware logic disabled or there is only one zone");
//选择服务
return super.chooseServer(key);
}
}
4、轮询
super.chooseServer(key)内部具体业务逻辑
负载均衡实现后的返回如下结果:
Ribbon饥饿加载和懒加载
懒加载
默认是采用,即第一次访问时才会去创建LoadBalanceClient去拉取服务列表,第一次请求时间会很长,但后续请求时间会变短
我们在通过访问浏览器看看效果:
饥饿加载
急不可耐,当服务启动时就创建LoadBalanceClient去拉取服务列表,降低第一次访问的耗时
需要在配置文件中进行配置
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: userservice # 指定对userservice这个服务饥饿加载
当服务启动时查看,效果如下:
三、总结
Ribbon帮助我们在去选择对应策略的服务实例,SpringCloud中许多组件都集成了Ribbon,Ribbon提供的内置策略,我们也可以自定义策略,虽然知道SpringCloud的某些组件集成了Ribbon,但是怎么集成的Ribbon?Ribbon如何和其他组件关联的?我们还要知其所以然。