前面的内容学习,已经实现了服务的注册和服务发现。当启动某个服务的时候,可以通过 HTTP 的形式将信息注册到注册中心,并且可以通过 SpringCloud 提供的工具获取注册中心的服务列表。但是服务之间的调用还存在很多的问题,如何更加方便的调用微服务,多个微服务的提供者如何选择,如何负载均衡等。
一、Ribbon 概述
1.1 什么是 Ribbon
Ribbon 是 Netflix 发布的一个负载均衡器,有助于控制 HTTP 和 TCP 客户端行为。在 SpringCloud 中,Eureka 一般配合 Ribbon 进行使用,Ribbon 提供了客户端负载均衡的功能,Ribbon 利用从 Eureka 中读取到的服务信息,在调用服务节点提供的服务时,会合理的进行负载。
在 SpringCloud 中可以将注册中心和 Ribbon 配合使用,Ribbon 自动地从注册中心获取服务提供者的列表信息,并基于内置的负载均衡算法,请求服务。
1.2 Ribbon 的主要作用
(1)服务调用 基于 Ribbon 实现服务调用,是通过拉取到的所有服务列表组成(服务名-请求路径的)映射关系。借助 RestTemplate 最终进行调用。
(2)负载均衡 当多个服务提供者时,Ribbon 可以根据负载均衡的算法(如简单轮询、随机连接等)自动地选择需要调用的服务地址。
二、服务调用 Ribbon 高级
2.1 负载均衡概述
2.1.1 什么是负载均衡
在搭建网站时,如果单节点的 web 服务性能和可靠性都无法达到要求;或者是在使用外网服务时,经常担心被人攻破,一不小心就会有打开外网端口的情况,通常这个时候加入负载均衡就能有效解决服务问题。
LB 负载均衡是(Load Balance)一种基础的网络服务,其原理是通过运行在前面的负载均衡服务,按照指定的负载均衡算法,将流量分配到后端服务集群上,从而为系统提供并行扩展的能力。简单的说就是将用户的请求平摊的分配到多个服务上,从而达到系统的 HA(高可用)。常见的负载均衡有软件 Nginx,LVS,硬件 F5 等。
负载均衡的应用场景包括流量包、转发规则以及后端服务,由于该服务有内外网隔离、健康检查等功能,能够有效提供系统的的安全性和可用性。
2.1.2 客户端负载均衡和服务端负载均衡
服务端负载均衡: 先发送请求到负载均衡服务器或者软件,然后通过负载均衡算法,在多个服务器之间选择一个进行访问;即在服务端在进行负载均衡算法分配。
客户端负载均衡: 客户端会有一个服务器地址列表,在发送请求前通过负载均衡算法选择一个服务器,然后进行访问,这是客户端负载均衡;即在客户端就进行负载均衡算法分配。
Nginx 是服务端负载均衡,客户端所有请求都会交给 Nginx,然后由 Nginx 实现转发请求,即负载均衡是由服务端实现的。 Ribbon 客户端负载均衡,在调用微服务接口的时候,会在注册中心上获取到注册信息服务列表之后,缓存到 JVM 本地,从而在本地实现 RPC 远程服务调用技术。
集中式LB:即在服务的消费方和提供方之间使用独立的 LB 设施(可以是硬件,如 F5,也可以是软件,如 Nginx),由该设施负载把访问请求通过某种策略转发至服务的提供方。
进程内LB:将 LB 逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。Ribbon 就属于进程内 LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。
7.2 基于 Ribbon 实现负载均衡
7.2.1 架构说明
Ribbon 在工作时分为两步: 第一步先选择 EurekaServer,它优先选择在同一个区域内负载较少的 server。 第二步再根据用户指定的策略,再从 server 取到的服务注册列表中选择一个地址。其中 Ribbon 提供了多种策略,比如轮询、随机和根据响应时间加权等。
Ribbon 其实就是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,和 Eureka 结合只是其中的一个实例。
7.2.2 pom.xml 说明
在之前的样例中,其实是有用到 ribbon 的负载均衡的,但是当时并没有引入 spring-cloud-starter-ribbon
这是由于在 spring-cloud-starter-netflix-eureka-client 中引入了 Ribbon:
7.2.3 RestTemplate 的使用
官网连接:https://docs.spring.io/spring-framework/docs/5.2.2.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html getForObject 方法:返回对象为响应体重数据转化成的对象,基本上可以理解为 JSON。 getForEntity 方法:返回对象为 ResponseEntity 对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等。
7.2.4 代码示例:
修改子模块 cloud-consumer-order80 中 OrderController 代码:
@RestController
@Slf4j
public class OrderController {
public static final String PAYMENT_URL = "http://CLOUD-PAYMENT-SERVICE";
// public static final String PAYMENT_URL = "http://localhost:8001";
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/payment/create")
public CommonResult
return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);
}
@GetMapping("/consumer/payment/get/{id}")
public CommonResult
return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);
}
@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult
ResponseEntity
if (forEntity.getStatusCode().is2xxSuccessful()) {
return forEntity.getBody();
} else {
return new CommonResult<>(444, "操作失败");
}
}
@GetMapping("/consumer/payment/createForEntity")
public CommonResult
ResponseEntity
CommonResult body = responseEntity.getBody();
return body;
}
}
测试 getPaymentForEntity: 使用 postman 测试 createPaymentForEntity:
7.2.5 负载均衡策略
Ribbon 内置了多种负载均衡策略,内部负责负载均衡的顶级接口为 com.netflix.loadbalancer.IRule,实现方式如下:
com.netflix.loadbalancer.RoundRobinRule:以轮询的方式进行负载均衡com.netflix.loadbalancer.RandomRule:随机策略com.netflix.loadbalancer.RetryRule:重试策略。先按照 RoundRobinRule 的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务。com.netflix.loadbalancer.WeightedResponseTimeRule:权重策略。会计算每个服务的权重,越高的被调用的可能性越大com.netflix.loadbalancer.BestAvailableRule:最佳策略。遍历所有的服务实例,过滤掉故障实例,并返回请求数最小的实例。com.netflix.loadbalancer.AvailabilityFilteringRule:可用过滤策略。过滤掉故障和请求数超过阈值的服务实例,再从剩下的实例中轮询调用。com.netflix.loadbalancer.ZoneAvoidanceRule:默认规则,复核判断 server 所在区域的性能和 server 的可用性选择服务器
7.2.6 修改负载均衡策略
有两种方式: 1、在服务消费者的 application.yml 配置文件中修改负载均衡策略: 2、创建 MySelfRule 配置类
7.2.6.1 修改 application.yml
# 修改 ribbon 的负载均衡策略
CLOUD-PAYMENT-SERVICE: # 需要调用的微服务名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
整体 application.yml 配置:
server:
port: 80
spring:
application:
name: cloud-order-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
instance:
instance-id: order80
prefer-ip-address: true
# 修改 ribbon 的负载均衡策略
CLOUD-PAYMENT-SERVICE: # 需要调用的微服务名称
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
7.2.6.2 创建 MySelfRule 配置类
1、注意配置细节 官方文档明确给出了警告: 这个自定义配置类不能放在 @ComponentScan 所扫描的当前包下以及子包下,否则我们自定义的这个配置类就会被所有的 Ribbon 客户端所共享,达不到特殊定制的目的了。 2、新建package com.atguigu.myrule,并创建 MySelfRule 配置类
@Configuration
public class MySelfRule {
@Bean
public IRule myRule(){
return new RandomRule();
}
}
3、在主动类上添加 @RibbonClient 注解
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE")
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
三、Ribbon 负载均衡算法
3.1 原理
负载均衡算法:rest 接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启动后 rest 接口计数从 1 开始。
List
如: List [0] instances = 127.0.0.1:8002 List [1] instances = 127.0.0.1:8001
8001 + 8002 组合成为集群,共计 2 台机器,集群总数为 2,按照轮询算法原理: 当总请求数为 1 时:1 % 2 = 1,对应下标位置为 1,则获得服务地址为 127.0.0.1:8001 当总请求数为 2 时:2 % 2 = 0,对应下标位置为 0,则获得服务地址为 127.0.0.1:8002 当总请求数为 3 时:3 % 2 = 1,对应下标位置为 1,则获得服务地址为 127.0.0.1:8001 当总请求数为 4 时:4 % 2 = 0,对应下标位置为 0,则获得服务地址为 127.0.0.1:8002 以此类推…
3.2 源码分析
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
// 获取所有服务
List
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) {
}
}
推荐阅读
发表评论