问:什么是 Ribbon?
答:Ribbon 是一个基于 HTTP 和 TCP 的客户端负载均衡工具,它基于 Netflix Ribbon 实现。SpringCloud Ribbon 只是一个工具类框架,不需要独立部署,但它几乎存在于每一个 SpringCloud 构建的微服务和基础设施中。微服务之间的调动、API 网关的请求转发等内容,实际上是通过 Ribbon 来实现的。
问:什么是负载均衡?
答:负载均衡(Load Balance)是一种网络术语,它指的是在现有的网络结构上,提供一种廉价、有效的方法来扩展网络设备和服务器带宽,以增加吞吐量和加强网络数据处理能力,提高网络灵活性和可用性。
而我们做开发的人员来说,负载均衡通常指的是服务端负载均衡。可以分为硬件负载均衡和软件负载均衡。硬件负载均衡是通过在服务器节点安装一些专门用于负载均衡的设备,如 F5等。而软件负载均衡,是通过在服务器安装一些有负载均衡功能的软件来完成请求分发工作,如 Nginx 等。我们做架构的,需要了解软件负载均衡。
我们来体验一下 Ribbon。
二话不说,先启动注册中心,学习的时候我们只需要启动1个注册中心即可,这时候会在控制台看到报错信息,原因是无法找到另一个注册中心(因为我们的 defaultZone 填写的是备注册中心的URL地址),这个没关系。如果你启动2个注册中心,就不会看到报错信息了。
创建一个模块,取名:ribbon-consumer(consumer:消费者),创建的方法见上一篇博客。
在此模块下的 pom.xml 加入 spring-cloud-starter-netflix-ribbon 依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>MyProject</artifactId>
<groupId>com.study</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ribbon-consumer</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
</dependencies>
</project>
这时我们发现,我们的依赖 spring-cloud-starter-netflix-eureka-client 在之前博客(第 6 篇)也有此依赖,spring-cloud-starter-netflix-eureka-client 没有指定依赖的版本号,需要手动指定其版本号。既然都是相同的版本号,那么我们就提出来,放到公共的地方,做统一版本号管理。步骤如下:
1、我们打开 MyProject 这个工程里的 pom.xml 文件(即最外层的 pom 文件),会看到我们加入的所有模块信息和各个依赖的 jar 版本信息,看截图:
2、在这个 pom.xml 文件的 <properties> 节点加入以下代码
<!-- 统一管理 netflix-eureka-client 的版本 -->
<netflix.eureka.client.version>2.1.2.RELEASE</netflix.eureka.client.version>
效果图。当然,这个节点的名字可以自己起,可以叫做阿猫阿狗.version,但是我们的代码要“顾名思义”。
3、修改 ribbon-consumer 和 eureka-client 的 pom.xml 配置文件
ribbon-consumer 的 pom.xml 完整代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>MyProject</artifactId>
<groupId>com.study</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ribbon-consumer</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>${netflix.eureka.client.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
</dependencies>
</project>
当相同的代码在一个项目中出现 2 次及以上,就要考虑抽取出来做成公共的,方便统一管理。否则真的是坑货一枚了。
然后,我们先创建1个package,再创建 SpringBoot 的启动类:RibbonConsumerApplication,注意结构(看上一篇博客)
在 RibbonConsumerApplication 类上加入以下 3 个注解
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
其中,@EnableDiscoveryClient 这个注解让该应用注册为 Eureka 客户端应用,以获得服务发现的能力。
完整的 RibbonConsumerApplication 代码如下:
package com.study;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-19 上午 11:50
*/
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient //让该应用注册为 Eureka 客户端应用,以获得服务发现的能力
public class RibbonConsumerApplication {
@Bean
@LoadBalanced //开启客户端负载均衡功能
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(RibbonConsumerApplication.class,args);
}
}
接下来,我们编写测试类,具体创建步骤如下:
1、先创建一个package包,起名 controller
2、在 controller 包下,创建测试类:RibbonController
3、RibbonController 完整代码如下:
package com.study.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-19 下午 11:04
*/
@RestController //@RestController 注解相当于 @ResponseBody + @Controller 合在一起的作用。
//@Controller //如果使用 @Controller 注解,则需要在方法上增加 @ResponseBody 注解
public class RibbonController {
@Autowired
private RestTemplate restTemplate;
//@ResponseBody
@RequestMapping(value = "/ribbon",method = RequestMethod.GET)
public String ribbonConsumer(){
return restTemplate.getForEntity("http://EUREKA-CLIENT-BIANDAN/say",String.class).getBody();
}
}
说明:
1、在 RibbonController 类上增加 @RestController 注解,@RestController 注解相当于@ResponseBody + @Controller 合在一起的作用。
2、@Controller 注解的工作原理可以简单了解一下:接触过 SpringMVC 的同学都知道,Spring使用 DispatcherServlet 核心类分发客户端请求,它把客户端请求的数据经过业务处理层处理后,封装成一个 Model 对象,然后找到对应的 view(通过@RequestMapping("/xxx")寻找),这就使得该 Controller 能被客户端访问。相当于 Servlet 中的 URL 映射。
3、代码:restTemplate.getForEntity("http://EUREKA-CLIENT-BIANDAN/say",String.class).getBody();
中的第一个参数 URL,其实配置的就是我们之前的服务提供者的服务名称。在微服务架构中,可以通过服务名称找到请求的目的地址。调用的是 say 方法。注意:服务的名称都默认全是大写的英文字母。
接下来,我们为 ribbon-consumer 这个模块创建一个 application.yml 文件,内容如下:
# 这是客户端服务的配置节点
server:
port: 10000
eureka:
instance:
hostname: main.study.com
client:
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:8000/eureka/
# 服务的名字
spring:
application:
name: eureka-ribbon-client
说明:
1、端口号:我们使用 10000,避免端口冲突。这样一来,微服务注册中心使用 8000+ 端口,服务提供者使用 9000+ 端口,消费者使用 10000+ 端口,便于我们学习。
2、注册中心的地址 defaultZone 我们指向 8000 端口,即主注册中心。
3、消费者的服务起名:eureka-ribbon-client
因为要测试 Ribbon 的负载均衡功能,我们给服务提供者创建2个服务的小集群。步骤如下:
1、修改 eureka-client 模块的 EurekaClientApplication 类,增加端口号参数的显示,完整代码如下:
package com.study;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author biandan
* @signature 让天下没有难写的代码
* @create 2019-10-15 下午 11:30
*/
@SpringBootApplication
@EnableEurekaClient //表明此类是一个客户端
@RestController
public class EurekaClientApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaClientApplication.class, args);
}
//从 yml 配置文件里获取客户端的名字
@Value("${spring.application.name}")
private String clientName;
@Value("${server.port}")
private String port;//获取配置文件的端口号
//测试方法:向调用者返回一句签名
@RequestMapping("/say")
public String sayHello() {
return clientName + " 说:让天下没有难写的代码!from port =" + port;
}
}
2、配置的端口号 9000 的服务先启动,然后修改 application.yml 的端口号成 9001,再启动服务,这样就创建了一个小集群。
①先启动端口号是 9000 的服务提供者
②修改端口号成 9001,然后需要修改 IDEA 工具的配置,否则启动 9001 端口的服务会把 9000 关闭下线。
我们看下如何在 IDEA 工具下启动多个 SpringBoot 实例:
OK,IDEA 工具配置完之后,我们修改 application.yml 的端口号 9001,再启动 EurekaClientApplication 实例
这时候,我们进入到注册中心,可以看到有 2 个实例了,这相当于一个服务提供者的小集群了。
主:http://main.study.com:8000/ 备:http://main.study.com:8001/ 都可以看到
还有,我们在浏览器打开以下两个地址,能正常访问,唯一不同的就是端口号:
9000 端口的服务:http://main.study.com:9000/say
9001 端口的服务:http://main.study.com:9001/say
万事俱备只欠东风,我们接下来启动 RibbonConsumerApplication 实例,测试 Ribbon 的负载均衡功能。
在浏览器打开以下地址,调用服务 RibbonController 类的 ribbonConsumer() 方法
http://main.study.com:10000/ribbon
多次刷新页面,我们会看到页面的展示信息不同(调用 9000 和 9001 两个不同端口的服务提供者,基本是轮询展示):
eureka-client-biandan 说:让天下没有难写的代码!from port =9000
eureka-client-biandan 说:让天下没有难写的代码!from port =9001
此时的架构图:
我们来讲解这个架构图原理(划考点了):
1、服务注册
“服务提供者”和“服务消费者”在启动时,发送 Rest 请求将自己注册到 Eureka Server 上,同时携带上自身服务的一些元数据信息(框架的底层已经封装好)。Eureka Server 接收到 Rest 请求后,将元数据存储在一个双层结构Map中,第一层的 key 是服务名(如 EUREKA-CLIENT-BIANDAN,都是大写字母),第二层的 key 是具体服务的实例名。
元数据:一般包括服务名称、实例名称、实例IP、实例端口等用于服务治理的重要信息,以及用于负载均衡的配置信息。
2、服务同步
“服务提供者”和“服务消费者”都注册到2个不同的服务注册中心(主备2个)上,它们的信息分别被 2 个注册中心维护。此时这2个注册中心相互注册服务,当一个注册中心发送注册请求到另一个注册中心上时,它会将该请求转发给集群中相连的其它注册中心。从而实现注册中心之间的服务同步。这样一来,已注册的服务的信息可以在2台注册中心中任一的一台获取得到。
3、服务续约(Renew)
注册完服务之后,“服务提供者”和“服务消费者”会维护一个心跳 Heart Beat 来持续告诉 Eureka Server 服务在线,以防止 Eureka Server “剔除服务”将该服务实例从服务列表中剔除出去。
服务续约有两个重要属性,一个是定义服务续约任务的调用间隔时间(默认30秒),另一个是定义服务失效的时间(默认90秒)。我们可以修改 application.yml 的 eureka 节点,如下代码:
eureka:
instance:
hostname: main.study.com
lease-renewal-interval-in-seconds: 30
lease-expiration-duration-in-seconds: 90
4、服务调用
服务消费者在获取服务清单后,通过服务名(我们例子中 http://EUREKA-CLIENT-BIANDAN/say 正是如此)可以获得具体的服务的实例名和实例的元数据。在 Ribbon 中,默认采用“轮询”的方式调用,从而实现客户端负载均衡。
5、服务下线
在服务实例正常关闭的时候,会触发一个服务下线的 Rest 请求给 Eureka Server,告诉服务注册中心它要下线了。服务端收到请求后,将该服务状态置为下线(DOWN),并把该下线事件传播出去。
6、服务剔除
但有些时候,服务实例不一定会正常下线,比如遇到:内存溢出、网络故障等原因导致服务不能正常工作,而 Eureka Server 并没有收到“服务下线”的请求,为了从服务列表中将这些无法提供服务的实例剔除,Eureka Server 在启动的时候会创建一个定时任务,默认每隔一段时间(默认60秒)将当前清单中超时(默认90秒)没有续约的服务剔除。