Ribbon是什么
Spring cloud Ribbon是基于Netflix Ribbon实现的一套客户端服务调用以及负载均衡的工具。
Ribbon + Eureka + RestTemplate
Eureka已经集成了Ribbon
Ribbon在工作时分成三步:
- 优先选择在同一个区域内负载较少的Eureka Server(当Eureka Server是集群的时候)
- 从选择的Eureka Server中获取服务注册列表(服务发现),并将信息缓存起来
- 根据用户配置的负载均衡策略,从第二步获取到的服务注册列表中选择一个地址,利用RestTemplate远程调用服务
Ribbon的负载均衡策略
Ribbon和Nginx都可以用来实现负载均衡的功能,那么它们有什么区别呢
Nginx是集中式的负载均衡,即服务器负载均衡,客户端所有请求都会交给Nginx,然后由它实现请求转发,比如浏览器发过来的各种请求都会经过Nginx这第一道大门,Nginx解析请求地址,然后根据一定的映射规则将请求转发
Ribbon是进程内的负载均衡,即本地负载均衡(浏览器先请求该服务,被Nginx转发到该服务,该服务才能继续运行它的逻辑),根据它的负载均衡策略来决定调用同一个服务下的哪个服务接口
即浏览器的请求→Nginx→某微服务→根据负载均衡策略去调用另一个微服务
Ribbon自带的负载均衡策略
所有策略都要继承AbstractLoadBalancerRule抽象类,实现choose方法
常见几种策略的解释
策略 | 说明 |
---|---|
RoundRobinRule | 轮询 |
RandomRule | 随机 |
RetryRule | 先按照轮询策略获取服务,如果获取失败则在指定时间内会进行重试,获取可用的服务 |
WeightedResponseTimeRule | 对轮询策略的扩展,响应速度越快的实例选择权重越大,越容易被选择 |
BestAvailableRule | 会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务 |
AvailablilityFilteringRule | 先过滤掉故障实例,再选择并发较小的实例 |
ZoneAvoidanceRule | 默认规则,复合判断server所在区域的性能和server的可用性选择服务器 |
使用默认的轮询策略
eureka客户端
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
......
</dependencies>
application.yaml
server:
port: 80
spring:
application:
name: cloud-ribbon-order #注册到eureka服务端时使用的微服务名称
eureka:
client:
register-with-eureka: true #将本应用注册到eureka服务端
fetch-registry: true #从eureka服务端获取服务注册列表
service-url:
defaultZone: http://eureka7001.com:7001/eureka #eureka服务端地址
启动类
@SpringBootApplication
@EnableEurekaClient //开启eureka客户端
@EnableDiscoveryClient //开启服务发现功能
public class ApplicationContextMain80 {
public static void main(String[] args) {
SpringApplication.run(ApplicationContextMain80.class,args);
}
}
配置类
@Configuration
public class CustomizeConfiguration {
//配置远程调用工具
@Bean
@LoadBalanced //开启负载均衡功能(默认用的轮询策略)
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
@RestController
public class OrderController {
@Autowired
RestTemplate restTemplate;
public static final String url = "http://CLOUD-RIBBON-PAYMENT"; //将调用的微服务的地址,CLOUD-RIBBON-PAYMENT是将访问的微服务名称
@GetMapping("/ribbon/order")
public String getPaymentById() {
//远程调用
return restTemplate.getForObject(url + "/ribbon/payment", String.class, (Object) null);
}
}
替换负载均衡策略
以替换成随机策略为例,有两种方法:配置文件,配置类
配置文件
application.xml增加如下负载均衡策略,可以实现对特定微服务的负载均衡配置,比如配置支付服务使用的负载均衡策略为随机策略,那么当需要访问支付服务时就会使用随机策略,而访问其他服务则依然使用默认的策略
CLOUD-RIBBON-PAYMENT: #要实现特定负载均衡配置的微服务名称
ribbon:
NFLoadBalancerRuleClassName: com.fl.demo.lb.MyIRule #配置的特定负载均衡策略
配置类
分两步:1.写一个配置类 2.在启动类上加@RibbonClient注解
写配置类,注意,该配置类不能放在启动类所在的包或其子包下,否则将失去针对某一微服务的特定配置策略功能,所有微服务都会受到策略修改的影响
@Configuration
public class RandomRuleConfiguration {
//配置一个随机策略
@Bean
public MyIRule randomRule(){
return new RandomRule();
}
}
在启动类上加@RibbonClient注解,其中name表示要修改负载均衡策略的微服务名称,configuration填写的是上面定义的配置类,这样就将特定的微服务和特定的策略绑定,调用其他微服务时依然使用的是默认的负载均衡策略
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@RibbonClient(name = "CLOUD-RIBBON-PAYMENT", configuration = RandomRuleConfiguration.class)
public class ApplicationContextMain80 {
public static void main(String[] args) {
SpringApplication.run(ApplicationContextMain80.class,args);
}
}
自定义负载均衡策略
我们先来看看自带的轮询负载均衡策略是怎么写的
//这里继承了AbstractLoadBalancerRule抽象类
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() {
//初始化访问次数为0
this.nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
this.setLoadBalancer(lb);
}
//业务逻辑方法
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
} else {
Server server = null;//选择的实例
int count = 0;//获取实例失败后的重试次数
while(true) {
//如果当前还没有获取到实例并且重试次数没有超过10次,则尝试获取实例
if (server == null && count++ < 10) {
//获取所调用微服务的所有可用实例
List<Server> reachableServers = lb.getReachableServers();
//获取所调用微服务的所有实例
List<Server> allServers = lb.getAllServers();
//所有可用实例个数
int upCount = reachableServers.size();
//所有实例个数
int serverCount = allServers.size();
//如果可用实例和所有实例个数均不为0,则尝试获取实例
if (upCount != 0 && serverCount != 0) {
//根据某一算法获取一个下标,参数为所有实例的个数
int nextServerIndex = this.incrementAndGetModulo(serverCount);
//根据下标从所有实例中获取实例
server = (Server)allServers.get(nextServerIndex);
//若获取的实例为空(可能是高并发引起),则当前线程释放cpu,稍等一会再尝试获取
if (server == null) {
Thread.yield();
} else {
//若获取到的实例可用并且已经做好了服务的准备,则将实例返回
if (server.isAlive() && server.isReadyToServe()) {
return server;
}
//获取到的实例不可用或是实例没有准备好服务,则将实例置为null,重新尝试获取实例
server = null;
}
continue;
}
//服务注册列表中没有可用实例,则直接返回null
log.warn("No up servers available from load balancer: " + lb);
return null;
}
//如果重试了10次依然获取不到可用的实例,则日志记录报错信息
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
}
}
//获取下标的算法
//算法:(当前访问次数+1)% 服务实例的数量得到一个下标,实现轮询的效果
private int incrementAndGetModulo(int modulo) {
int current;//当前访问次数
int next;//下标
do {
current = this.nextServerCyclicCounter.get();//获取当前访问次数
next = (current + 1) % modulo;//计算得到下标
} while(!this.nextServerCyclicCounter.compareAndSet(current, next));//cas尝试把访问次数更新为下标
return next;
}
//需要实现的方法
public Server choose(Object key) {
return this.choose(this.getLoadBalancer(), key);
}
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
总结一下几点:
- 需要继承AbstractLoadBalancerRule抽象类,并实现
public Server choose(Object key)
和public void initWithNiwsConfig(IClientConfig clientConfig)
方法,后者先用空实现就行 public Server choose(ILoadBalancer lb, Object key)
和private int incrementAndGetModulo(int modulo)
方法承担了主要的业务逻辑,前者是获取实例,包括空判断,重试等,后者是获取下标的计算逻辑,前者调用了后者public Server choose(Object key)
方法调用了public Server choose(ILoadBalancer lb, Object key)
方法
依据这三点我们可以模仿着自定义一个策略:将轮询的一次换一个实例替换成三次换一个实例,即每个实例需要访问三次后才能轮到下一个实例
public class MyIRule extends AbstractLoadBalancerRule{
AtomicInteger count = new AtomicInteger(0);//访问次数
//该方法保持空实现就行
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
public MyIRule(){}
public MyIRule(ILoadBalancer iLoadBalancer){
setLoadBalancer(iLoadBalancer);
}
//需要实现的方法
@Override
public Server choose(Object o) {
return this.choose(this.getLoadBalancer(), key);
}
//简单地根据下标获取实例,不考虑重试以及实例是否可用的问题
public Server choose(ILoadBalancer lb, Object key){
if(loadBalancer == null)return null;
List<Server> allServers = loadBalancer.getAllServers();
if(allServers == null || allServers.size() == 0)return null;
return allServers.get(incrementAndGetModulo(allServers.size()));
}
//计算获得下标
//算法:假设现在有两个实例,则sum=2*3=6,那么(访问次数+1)%sum的结果是0~5轮询,我们规定,结果为0~2的就访问第一个实例,3~5访问第二个实例,为了满足这个需求,我们只需要将该结果除以3即可获得下标
private int incrementAndGetModulo(int modulo){
int current;
int next;
int sum = 3 * modulo;
int result;
do{
current = count.get();
next = (current + 1) % sum;
result = next / 3;
}while(!count.compareAndSet(current, next));
return result;
}
}
接下来我们需要把自定义的策略配置给相应的微服务使之生效,如何配置请参考上面提到的“替换负载均衡策略”