47-Ribbon
1. 负载均衡
1.1 概述
Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端-负载均衡的工具。
简单地说,Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon 客户端组件提供一系列完善的配置项如连接超时、重试等。
Ribbon 目前也进入维护模式,未来的替换方案是 Spring Cloud Starter Loadbalancer。
- 【进程内 LB】将 LB 逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器;
- 【集中式 LB】在服务的消费方和提供方之间使用独立的 LB 设备(可能是硬件,如 F5;也可能是软件,如 Nginx),由该设备负责把访问请求通过某种策略转发至服务的提供方。
Ribbon 就属于进程内 LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址(在调用微服务接口的时候,会在注册中心上获取注册信息服务列表之后缓存到 JVM 本地,从而在本地实现 RPC 远程服务调用技术)。
1.2 说明
Ribbon 在工作时分为两步:
- 先选择 EurekaServer,它优先选择在同一个区域内负载较少的 Server;
- 再根据用户指定的策略,在从 Server 取到的服务注册列表中选择一个地址;
Eureka 已经依赖了 Ribbon:
RestTemplate 说明:
如果需要使用 ip:port
就不需要加 @LoadBalanced
注解。但如果需要使用 application.name
访问,那就需要在 restTemplate 配置加上 @LoadBalanced
,因为 @LoadBalanced
这个注解是负载均衡的注解,而负载均衡就是通过访问服务名而实现的。如果你加上这个注解之后使用 restTemplate,那么它就默认你的 "localhost" 是一个模块的服务名称("ip:port"),而不是本机的 IP。总而言之,如果需要使用 ip:port
,就不需要加 @LoadBalanced
;如果需要使用应用名访问,那就需要在 restTemplate 配置加上 @LoadBalanced
。
2. 负载均衡策略
2.1 策略类型
Ribbon 内置了多种负载均衡策略,内部负责复杂均衡的顶级接口为 com.netflix.loadbalancer.IRule ,类树如下:
根据特定算法从服务列表中选取一个要访问的服务:
负载均衡策略 | 描述 |
---|---|
RoundRobinRule 轮询策略 | 默认超过 10 次获取到的 server 都不可用,会返回一个空的 server。 |
RandomRule 随机策略 | 如果随机到的 server 为 null 或者不可用的话,会 while 不停的循环选取。 |
RetryRule 重试策略 | 一定时限内循环重试。默认继承 RoundRobinRule,也支持自定义注入,RetryRule 会在每次选取之后,对选举的 server 进行判断,是否为 null、是否 alive,并且在 500ms 内会不停的选取判断。而 RoundRobinRule 失效的策略是超过 10 次,RandomRule 是没有失效时间的概念,只要 serverList 没都挂。 |
BestAvailableRule 最小连接数策略 | 遍历 serverList,选取出可用的且连接数最小的一个 server。该算法里面有一个 LoadBalancerStats 的成员变量,会存储所有 server 的运行状况和连接数。如果选取到的 server 为 null,那么会调用 RoundRobinRule 重新选取。 |
AvailabilityFilteringRule 可用过滤策略 | 扩展了轮询策略,会先通过默认的轮询选取一个 server,再去判断该 server 是否超时可用,当前连接数是否超限,都成功再返回。 |
ZoneAvoidanceRule 区域权衡策略(默认策略) | 扩展了轮询策略,继承了 2 个过滤器: ZoneAvoidancePredicate 和 AvailabilityPredicate,除了过滤超时和链接数过多的 server,还会过滤掉不符合要求的 zone 区域里面的所有节点,AWS-ZONE 在一个区域/机房内的服务实例中轮询。 |
2.2 测试用例
修改 demo-project 的负载均衡策略:
# 针对的被调用方微服务名称,不加就是全局生效
cloud-service-resume:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
当调用指定微服务时使用自定义的 Rule:
- 自定义类放置在主启动类所在包同级的包下(不能放在 @ComponentScan 所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的 Ribbon 客户端所共享,达不到特殊化定制的目的了);
@Configuration public class MyRibbonConfig { @Bean public IRule myRule(){ return new RandomRule(); } }
- 修改主启动类
@EnableEurekaClient @SpringBootApplication @RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MyRibbonConfig.class) public class OrderMain8080 { public static void main(String[] args) { SpringApplication.run(OrderMain8080.class, args); } }
3. 源码分析
Ribbon 给 RestTemplate 添加了一个拦截器。
当我们访问 http://cloud-service-resume/resume/openstate/
的时候,Ribbon 应该根据服务名 cloud-service-resume 获取到该服务的实例列表并按照一定的负载均衡策略从实例列表中获取一个实例 Server,并最终通过 RestTemplate 进行请求访问。
涉及到底层的一些组件/类的描述:
图中核心是负载均衡管理器 LoadBalancer(总的协调者,相当于大脑,为了做事情,协调四肢),围绕它周围的有 IRule、IPing 等。
- IRule:是在选择实例的时候的负载均衡策略对象;
- IPing:是用来向服务发起心跳检测的,通过心跳检测来判断该服务是否可用;
- ServerListFilter:根据一些规则过滤传入的服务实例列表;
- ServerListUpdater:定义了一系列的对服务列表的更新操作。
3.1 @LoadBalanced
我们在 RestTemplate 实例上添加了一个 @LoadBalanced
就可以实现负载均衡,很神奇,我们接下来分析这个注解背后的操作(负载均衡过程)查看 @LoadBalanced 注解,那这个注解是在哪里被识别到的呢?
LoadBalancerClient(实现类 RibbonLoadBalancerClient)
到这里,我们明白,添加了注解的 RestTemplate 对象会被添加一个拦截器 LoadBalancerInterceptor,该拦截器就是后续拦截请求进行负载处理的。所以,下一步重点我们该分析拦截器 LoadBalancerInterceptor#intercept()
方法。
3.2 LoadBalancerInterceptor
那么 RibbonLoadBalancerClient 对象是在哪里注入的?回到最初的自动配置类 RibbonAutoConfiguration 中。
核心方法:RibbonLoadBalancerClient.execute()
a. 获取 LoadBaLancer 对象
b. 获取 Server 实例
【serverList 的注入】在进行负载 chooseServer 的时候,LoadBaLancer 负载均衡器中已经有了 serverList,那么这个 serverList 是什么时候被注入到 LoadBalancer 中的,它的一个机制大概是怎样的?
c. 执行调用
3.3 轮询算法
RoundRobinRule 源码如下:
/*
*
* Copyright 2013 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.netflix.loadbalancer;
import com.netflix.client.config.IClientConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* The most well known and basic load balancing strategy, i.e. Round Robin Rule.
*
* @author stonse
* @author Nikos Michalakis <nikos@netflix.com>
*
*/
public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);
public RoundRobinRule() {
nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
setLoadBalancer(lb);
}
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
int nextServerIndex = incrementAndGetModulo(serverCount);
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
/**
* Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
*
* @param modulo The modulo to bound the value of the counter.
* @return The next value.
*/
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
手写一个呢~
- 8001/8002 controller 新增方法:
@GetMapping("/lb") public String testLoadBalance() { return port; }
- 去掉
@LoadBalanced
注解@Configuration public class ApplicationContextConfig { /** * 如果需要使用ip和端口,就不需要加 @LoadBalanced * 如果需要使用应用名访问,那就需要加上 @LoadBalanced * @return */ @Bean // @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } }
- 8080 服务新增接口 MyLoadBalance
public interface MyLoadBalance { /** * 返回下一个要调用的 ServiceInstance * @param serviceInstances 收集服务器总共有多少台能够提供服务的机器,并放到list里面 * @return */ ServiceInstance getNextServer(List<ServiceInstance> serviceInstances); }
- 8080 服务新增接口实现
@Component public class MyLoadBalanceImpl implements MyLoadBalance { private AtomicInteger nextServerIndex = new AtomicInteger(0); private int getNextServerIndex(int serverCount) { int current, next; do { current = this.nextServerIndex.get(); next = (current+1) % serverCount; } while (this.nextServerIndex.compareAndSet(current, next)); return this.nextServerIndex.get(); } @Override public ServiceInstance getNextServer(List<ServiceInstance> serviceInstances) { return serviceInstances.get(getNextServerIndex(serviceInstances.size())); } }
- 8080 服务 controller 新增测试方法
@Resource private MyLoadBalance myLoadBalance; @Resource private DiscoveryClient discoveryClient; @GetMapping("/payment/lb") public CommonResult<String> testLoadBalance() { List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE"); if (instances == null || instances.isEmpty()) { return new CommonResult(HttpStatus.SERVICE_UNAVAILABLE.value(), "无可用服务", null); } ServiceInstance nextServer = myLoadBalance.getNextServer(instances); log.info("LB -> {}", nextServer.getUri()); return new CommonResult(HttpStatus.OK.value(), "负载均衡", restTemplate.getForObject(nextServer.getUri()+"/payment/lb", String.class)); }