1.概述
在SpringCloud中,负载均衡是基于组件Netflix Ribbon来实现的。Ribbon客户端组件提供了一系列完善的配置(超时、重试等),它会基于用户配置的策略(或默策略),按照策略去访问Load Balancer (简称LB)后所注册的服务,保证请求按照策略分发到不同的机器上,实现负载均衡。本文将详细分析SpringCloud中Ribbon的实现原理,以及实现负载均衡策略的原理。
2.原理分析
2.1 负载均衡概念
负载均衡(Load balancing)指的是将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。上述解释来自于百度,应用于微服务架构中,就是当一个服务拥有多个实例时,调用方来访问这些服务,能够均匀请求到这些实例上,达到资源使用最优化、吞吐量最大化、响应时间最小化的目的,同时还需要避免服务过载。
2.2 负载均衡对微服务的重要性
在微服务中,若没有负载均衡,可能会导致请求不均匀分布(一部分实例会收到大量请求而造成瘫痪,另一部分却接收到个位数请求),结果可能会导致一部分实例先宕机,进而请求又会不断涌向正常的实例,最后造成所有实例瘫痪。可以说负载均衡策略对微服务健康运行有着至关重要的作用,在没有充足的资源前提下,这种方式能够有效保证资源的充分应用和服务的健康运行。
2.3 SpringCloud中Ribbon使用
2.3.1 引入依赖
<!--添加ribbon的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
springCloud中大部分服务发现都引入了ribbon,比如feign组件、eurker-server、nacos-discovery等,若已经引入这些包,则不需要额外再引入上述依赖。
2.3.1 添加@LoadBalanced注解
@Configuration
public class RestConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
@LoadBalanced注解则是ribbon负载均衡的具体实现,通过添加该注解,默认会以轮询方式获取所请求服务,保证负载均衡。
2.3.2 使用RestTemplate远程调用
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
public R findUserByUserId(Integer id) {
String url = "http://userService/findOrderByUserId/"+id;
R result = restTemplate.getForObject(url,R.class);
return result;
}
}
2.4 Ribbon的实现原理
2.4.1 Ribbon工作原理
在上述案例中,以订单服务根据userId查询用户信息为例。订单服务(orderService)发起查询请求:http://userService/getUserInfo,Ribbon负载均衡器会拦截该http请求,获取userService在注册中心所注册的服务列表,然后根据配置的策略(轮询、随机、哈希等),从服务列表中筛选一个userService发送请求,接收返回结果。
2.4.2 Ribbon负载均衡策略
Ribbon内置的核心负载均衡策略类图如下所示:
各策略的详细解释如下:
RoundRobinRule:轮询策略(Ribbon默认策略),若获取的server不可用、或者超过10次未能获取到server,则会返回一个空的server;
RandomRule:随机策略,随机选取一个server返回,若随机到的server为null或者不可用,则会不停地循环选取;
RetryRule:重试策略,一定时间内循环重试。默认使用RoundRobinRule策略,也可以传入指定策略,它会在每次选举之后,判断server是否为空、是否alive,在500ms内不停获取server;
BestAvailableRule:最小连接数策略,遍历serverList,选取可用的且连接数最小的一个server;如果server为空,则使用轮询策略重新获取;
AvailabilityFilteringRule:可用过滤策略,该策略是在轮询策略上的扩展,对所选取的server进行了超时判断、连接数超限判断等,符合条件才会返回;
ZoneAvoidanceRule:区域权衡策略,该策略是在轮询策略上的扩展,过滤了超时和连接数较多的server,还会过滤掉不符合要求的 zone 区域⾥⾯的所有节点,始终保证在⼀个区域/机房内的服务实例进行轮询;
WeightedResponseTimeRule:平均响应时间策略,该策略是在轮询策略上的扩展,它计算每个服务的平均相应时间,时间越少权重越大,若刚开始时信息不足,则使用轮询策略,待统计信息足够,会使用该策略。
2.5 Ribbon在SpringCloud中源码解读
Ribbon在SpringCloud核心代码的加载顺序如下:
LoadBalancerAutoConfiguration配置类中,给添加了@LoadBalanced注解的RestTemplate类,增加了一个拦截器LoadBalancerInterceptor,该拦截器拦截到请求后,会调用RibbonLoadBalancerClient中getLoadBalancer()方法,获取可用server,该方法调用IRule的choose()方法,获取策略,根据策略选择server。
2.5.1 LoadBalancerInterceptor
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
private LoadBalancerClient loadBalancer;
private LoadBalancerRequestFactory requestFactory;
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer, LoadBalancerRequestFactory requestFactory) {
this.loadBalancer = loadBalancer;
this.requestFactory = requestFactory;
}
public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
// for backwards compatibility
this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
}
@Override
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
//获取请求路径uri
final URI originalUri = request.getURI();
//获取服务名称
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
//此处真正执行的方法是:RibbonLoadBalancerClient.execute()方法
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
}
上述代码中的核心方法就是intercept()方法,debug案例如下,获取服务名称之后,会携带服务名称去查询并获取可用server。
2.5.2 RibbonLoadBalancerClient
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
//获取服务列表
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
//进行负载均衡操作,获取server
Server server = getServer(loadBalancer);
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);
}
protected Server getServer(ILoadBalancer loadBalancer) {
if (loadBalancer == null) {
return null;
}
//默认采用轮询策略获取server
return loadBalancer.chooseServer("default"); // TODO: better handling of key
}
// BaseLoadBalancer类中使用chooseServer()方法
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
//根据传入的服务id,从负载均衡器中为指定的服务选择一个服务实例
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
//根据传入的服务id,指定的负载均衡器中的服务实例执行请求
@Override
public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
Server server = null;
if(serviceInstance instanceof RibbonServer) {
server = ((RibbonServer)serviceInstance).getServer();
}
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonLoadBalancerContext context = this.clientFactory
.getLoadBalancerContext(serviceId);
RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);
try {
T returnVal = request.apply(serviceInstance);
statsRecorder.recordStats(returnVal);
return returnVal;
}
// catch IOException and rethrow so RestTemplate behaves correctly
catch (IOException ex) {
statsRecorder.recordStats(ex);
throw ex;
}
catch (Exception ex) {
statsRecorder.recordStats(ex);
ReflectionUtils.rethrowRuntimeException(ex);
}
return null;
}
2.6 Ribbon饥饿加载机制
Ribbon默认是采用懒加载,第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
饥饿加载则会在项目启动时创建,降低第一次访问的耗时通过下面配置开启饥饿加载:
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: userservice # 指定服务名为userservice的服务饥饿加载
由上图可知,配置完饥饿加载后,服务启动后会直接生成LoadBalanceClient。
2.7 自定义负载均衡策略
2.7.1 bean注入指定IRule实现类
@Bean
public IRule rule(){
return new RandomRule();
}
2.7.1 通过配置文件配置
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule # 负载均衡规则
3.小结
1.本文详细介绍了负载均衡的原理、实现流程以及源码分析等;
2.Ribbon饥饿加载机制,只在使用时才创建LoadBalanceClient;
3.配置饥饿加载时,要指定服务名称;
4.可以通过bean注入和配置文件配置的方式来进行负载均衡策略配置。
4.参考文献
1.https://www.bilibili.com/video/BV1LQ4y127n4
2.https://www.cnblogs.com/WarBlog/p/15397657.html
3.https://zhuanlan.zhihu.com/p/88431514