一、背景介绍
项目中使用到的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如何和其他组件关联的?我们还要知其所以然。