前面的内容学习,已经实现了服务的注册和服务发现。当启动某个服务的时候,可以通过 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

org.springframework.cloud

spring-cloud-starter-netflix-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 create(Payment payment){

return restTemplate.postForObject(PAYMENT_URL + "/payment/create", payment, CommonResult.class);

}

@GetMapping("/consumer/payment/get/{id}")

public CommonResult getPayment(@PathVariable("id") Long id){

return restTemplate.getForObject(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);

}

@GetMapping("/consumer/payment/getForEntity/{id}")

public CommonResult getPaymentForEntity(@PathVariable("id") Long id){

ResponseEntity forEntity = restTemplate.getForEntity(PAYMENT_URL + "/payment/get/" + id, CommonResult.class);

if (forEntity.getStatusCode().is2xxSuccessful()) {

return forEntity.getBody();

} else {

return new CommonResult<>(444, "操作失败");

}

}

@GetMapping("/consumer/payment/createForEntity")

public CommonResult createPaymentForEntity(Payment payment){

ResponseEntity responseEntity = restTemplate.postForEntity(PAYMENT_URL + "/payment/create", payment, CommonResult.class);

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 instances = discoveryClient.getInstances("CLOUD_PAYMENT_SERVICE");

如: 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 reachableServers = lb.getReachableServers();

// 获取所有服务

List 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) {

}

}

推荐阅读

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: