目录
博主整理的SpringCloud系列目录:>>戳这里<<
一、Ribbon 介绍
负载均衡是分布式架构的重点,负载均衡机制决定着整个服务集群的性能与稳定。根据前面章节的介绍可知, Eureka 服务实例可以进行集群部署,每个实例都均衡处理服务请求,那么这些请求是如何被分摊到各个服务实例中的呢?下面将讲解 Netflix 的负载均衡项目 Ribbon。
1、Ribbon 简介
Ribbon 是 Netflix 下的负载均衡项目,它在集群中为各个客户端的通信提供了支持,它主要实现中间层应用程序的负载均衡。
Ribbon 提供以下特性:
- 负载均衡器,可支持插拔式的负载均衡规则。
- 对多种协议提供支持,例如 HTTP、CP、UDP。
- 集成了负载均衡功能的客户端。
同为 Netflix 项目, Ribbon 可以与 Eureka 整合使用, Ribbon 同样被集成到 Spring Cloud中,作为 spring-cloud-netflix 项目中的子模块 Spring Cloud 将 Ribbon API 进行了封装,使用者可以使用封装后的 API 来实现负载均衡,也可以直接使用 Ribbon 的原生 API。
2、Ribbon 子模块
Ribbon 主要有以下三大子模块。
-
ribbon-core
该模块为 Ribbon 项目的核心,主要包括负载均衡器接口定义、 客户端接口定义、内置的负载均衡实现等 API。 -
ribbon-eureka
为 Eureka 客户端提供的负载均衡实现类 -
ribbon-httpclient
对 Apache 的 HttpClient 进行封装,该模块提供了含有负载均衡功能的 REST 客户端。
3、负载均衡器组件
Ribbon 的负载均衡器主要与集群中的各个服务器进行通信,负载均衡器需要提供以下基础功能:
- 维护服务器的 IP、DNS 名称等信息。
- 根据特定的逻辑在服务器列表中循环。
为了实现负载均衡的基础功能, Ribbon 的负载均衡器有以下三大子模块。
-
Rule:一个逻辑组件,这些逻辑将会决定从服务器列表中返回哪个服务器实例。
-
Ping:该组件主要使用定时器来确保服务器网络可以连接。
-
ServerList:服务器列表,可以通过静态的配置确定负载的服务器,也可以动态指定服务器列表。 如果动态指定服务器列表,则会有后台的线程来刷新该列表。
二、第一个 Ribbon 程序
我们先编写一个Ribbon程序。这里我们单独使用 Ribbon 框架,关于整合 Spring Cloud 的内容,将在下篇讲述。
下面,我们将以一个简单的 Hello World 程序来展示 Ribbon API 的使用。本例的程序结构如下:
可以看到,Ribbon 负载均衡是在客户端实现的。
1、编写服务器
为了能查看负载均衡效果,先编写一个简单的 REST 服务,通过添加JVM参数 -Dserver.port=8081
和 -Dserver.port=8082
来指定不同的端口,让服务可以启动多个实例。
本例的请求服务器,仅仅是一个基于 Spring Boot 的 Web 应用,如果读者熟悉建立过程,可跳过部分创建过程,我们的最终目的是发布两个 REST 服务。
新建名为 first-ribbon-server
的 Spring Boot 项目。具体依赖如下:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.swotxu</groupId>
<artifactId>first-ribbon-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>first-ribbon-server</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 此依赖为了方便使用,非必须的 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后编写控制器,提供一个 REST 服务。
package com.swotxu.firstribbonserver.web;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* @Date: 2020/7/9 20:43
* @Author: swotXu
*/
@RestController
public class MyController {
@RequestMapping(value = "/user/{userId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public UserInfo findUser(@PathVariable("userId") Integer userId, HttpServletRequest request){
UserInfo userInfo = new UserInfo(userId, "swotxu", 18, request.getRequestURL().toString());
return userInfo;
}
@RequestMapping(value = "/", method = RequestMethod.GET)
public String findUser(){
return "hello";
}
}
// ---------------- 实体类 ----------------------
package com.swotxu.firstribbonserver.web;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Date: 2020/6/25 23:51
* @Author: swotXu
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo {
private Integer id;
private String username;
private Integer age;
private String message;
}
在控制器中,发布了两个 REST 服务。其中,调用地址为 /user/userId
的服务后,会返回一个 UserInfo 实例的 JSON 字符串,为了看到请求的 URL ,为 UserInfo 的 message 属性设置了请求的 URL。
2、编写请求客户端
新建名称为 first-ribbon-client
的 Maven 项目。加入以下依赖:
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.swotxu</groupId>
<artifactId>first-ribbon-client</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<ribbon.version>2.3.0</ribbon.version>
</properties>
<dependencies>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
<version>${ribbon.version}</version>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-httpclient</artifactId>
<version>${ribbon.version}</version>
</dependency>
<!-- 需手动引入以下包,否则会导致编码时会找不到 com.netflix.client.ClientFactory -->
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-loadbalancer</artifactId>
<version>${ribbon.version}</version>
</dependency>
<!-- 需手动引入以下包,否则会导致编码时会找不到 com.netflix.client.IClient -->
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
<version>${ribbon.version}</version>
</dependency>
<!-- 需手动引入以下包,否则会导致编码时会找不到 com.netflix.config.ConfigurationManage -->
<dependency>
<groupId>com.netflix.archaius</groupId>
<artifactId>archaius-core</artifactId>
<version>0.7.6</version>
</dependency>
<!-- 需手动引入以下包,否则会导致编码时会找不到 org.apache.commons.configuration.AbstractConfiguration -->
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>1.10</version>
</dependency>
<!-- 防止运行时 NoClassDefFoundError: com/google/common/reflect/TypeToken-->
<dependency>
<groupId>com.ecwid</groupId>
<artifactId>ecwid-mailchimp</artifactId>
<version>2.0.1.0</version>
</dependency>
</dependencies>
</project>
接下来,使用 Ribbon 的客户端发送请求,代码如下:
package com.swotxu.firstribbonclient;
import com.netflix.client.ClientFactory;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpResponse;
import com.netflix.config.ConfigurationManager;
import com.netflix.niws.client.http.RestClient;
/**
* @Date: 2020/7/9 21:02
* @Author: swotXu
*/
public class TestRestClient {
public static void main(String[] args) throws Exception {
// 通过配置文件导入配置信息
// ConfigurationManager.loadPropertiesFromResources("ribbon.properties");
// 通过编码方式,设置请求的服务器
ConfigurationManager.getConfigInstance()
.setProperty("my-client.ribbon.listOfServers", "localhost:8081,localhost:8082");
// 获取 REST 请求客户端
RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
// 创建请求实例
HttpRequest request = HttpRequest.newBuilder().uri("/user/1").build();
// 发送 8 次请求
for (int i = 0; i < 8; i++) {
HttpResponse response = client.executeWithLoadBalancer(request);
String entity = response.getEntity(String.class);
System.out.println(entity);
}
}
}
上述代码中,使用 ConfigurationManager 类来配置请求的服务器列表,为 localhost:8081
与 localhost:8082
,再使用 RestClient 对象,向 /user/1
地址发送 8 次请求。
3、测试
先启动两次服务器类 FirstRibbonServerApplication
,端口分别设置为 8081 和 8082。
待两个服务器启动成功后,运行客户端,会看到控制台中如下输出:
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8082/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8081/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8082/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8081/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8082/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8081/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8082/user/1"}
{"id":1,"username":"swotxu","age":18,"message":"http://localhost:8081/user/1"}
Process finished with exit code 0
根据输出结果可知,RestClient 轮流向 8081 与 8082 端口发送请求,可见在 RestClient 中已经帮我们实现了负载均衡的功能。
4、Ribbon 配置
在编写客户端时,使用了 ConfigurationManager 来设置配置项,除了在代码中指定配置项外,还可以将配置放到 .properties 文件中。ConfigurationManager 的 loadPropertiesFromResources 方法可以指定 .properties 文件的位置,配置格式如下:
<client>.<nameSpace>.<property> = <value>
其中<client>
为客户的名称,声明该配置属于哪一个客户端,在使用 ClientFactory 时可传入客户端的名称,即可返回对应的“请求客户端”实例。<nameSpace>
为该配置的命名空间,默认为 ribbon,<property>
为属性名,<value>
为属性值。如果想对全部客户端生效,可以将客户端名称去掉,直接以<nameSpace>.<property>
的格式进行配置。示例如下:
my-client.ribbon.listOfServers = localhost:8081,localhost:8082
三、Ribbon 的负载均衡机制
Ribbon 提供了几个负载均衡的组件,其目的就是让请求转给合适的服务器处理。因此,如何选择合适的服务器便成为负载均衡机制的核心。
1、负载均衡器
Ribbon 的负载均衡器接口定义了服务器的操作,主要是用于进行服务器选择。
在上面例子中,客户端使用了 RestClient 类,在发送请求时,会使用负载均衡器(ILoadBalancer)接口,根据特定的逻辑来选择服务器。服务器列表可使用 listOfServers 进行配置,也可使用动态更新机制。
下面,我们在请求客户端中新建 ChoseServerTest
,内容如下:
package com.swotxu.firstribbonclient;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.ArrayList;
import java.util.List;
/**
* @Date: 2020/7/12 15:43
* @Author: swotXu
*/
public class ChoseServerTest {
public static void main(String[] args) {
// 创建负载均衡器
ILoadBalancer balancer = new BaseLoadBalancer();
//添加服务器列表
List<Server> servers = new ArrayList<Server>();
servers.add(new Server("localhost", 8081));
servers.add(new Server("localhost", 8082));
balancer.addServers(servers);
// 进行6次服务器选择
for (int i = 0; i < 6; i++) {
Server server = balancer.chooseServer(null);
System.out.println(server);
}
}
}
代码中使用了 BaseLoadBalancer 这个负载均衡器,将两个服务器对象加入负载均衡器中,再调用 6 次 chooseServer 方法,可看到输出结果如下:
localhost:8082
localhost:8081
localhost:8082
localhost:8081
localhost:8082
localhost:8081
根据结果可知,最终选择的服务器与前面的案例一致,可以判定本例与前面的案例选择服务器的逻辑是一致的,在默认情况下, 会使用 RoundRobinRule 的规则逻辑。
查看源码证实了我们的猜想:
2、自定义负载规则
根据刚刚的案例可知,选择哪个服务器进行请求处理,由 ILoadBalancer 接口的 chooseServer 方法决定。而其方法内部,则是委托给 IRule 接口的 choose 方法来决定选择哪个服务器对象的。
我们可以自定义 IRule 的实现类如下:
package com.swotxu.firstribbonclient.custom;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;
import java.util.List;
/**
* @Date: 2020/7/12 16:36
* @Author: swotXu
*/
public class MyRule implements IRule {
ILoadBalancer iLoadBalancer;
public MyRule() {
}
public MyRule(ILoadBalancer iLoadBalancer) {
this.iLoadBalancer = iLoadBalancer;
}
public Server choose(Object key) {
List<Server> servers = iLoadBalancer.getAllServers();
return servers.get(0);
}
public void setLoadBalancer(ILoadBalancer iLoadBalancer) {
this.iLoadBalancer = iLoadBalancer;
}
public ILoadBalancer getLoadBalancer() {
return iLoadBalancer;
}
}
在自定义规则类中,实现的 choose 方法调用了 ILoadBalancer 的 getAllServers 方法,返回全部服务器,为了简单起见,本例只返回第一个服务器。
为了能在负载均衡器中使用自定义的规则,需要修改选择服务器的代码,新建 TestMyRule 类,如下:
package com.swotxu.firstribbonclient.custom;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import java.util.ArrayList;
import java.util.List;
/**
* @Date: 2020/7/12 15:43
* @Author: swotXu
*/
public class TestMyRule {
public static void main(String[] args) {
// 创建负载均衡器
BaseLoadBalancer balancer = new BaseLoadBalancer();
// 设置自定义的负载规则
balancer.setRule(new MyRule(balancer));
//添加服务器列表
List<Server> servers = new ArrayList<Server>();
servers.add(new Server("localhost", 8081));
servers.add(new Server("localhost", 8082));
balancer.addServers(servers);
// 进行6次服务器选择
for (int i = 0; i < 6; i++) {
Server server = balancer.chooseServer(null);
System.out.println(server);
}
}
}
运行后可看到 6 次所得的服务器均为 localhost:8081
。以上,我们是使用硬编码的方式来设置负载策略,当然,我们也可以使用配置来完成这些工作。
新建 TestMyRuleConfig 类,如下:
package com.swotxu.firstribbonclient.custom;
import com.netflix.client.ClientFactory;
import com.netflix.client.http.HttpRequest;
import com.netflix.client.http.HttpResponse;
import com.netflix.config.ConfigurationManager;
import com.netflix.niws.client.http.RestClient;
/**
* @Date: 2020/7/12 18:03
* @Author: swotXu
*/
public class TestMyRuleConfig {
public static void main(String[] args) throws Exception {
// 通过配置文件导入配置信息
// ConfigurationManager.loadPropertiesFromResources("ribbon.properties");
// 通过编码方式,设置请求的服务器
ConfigurationManager.getConfigInstance()
.setProperty("my-client.ribbon.listOfServers", "localhost:8081,localhost:8082");
// 配置负载规则处理类
ConfigurationManager.getConfigInstance()
.setProperty("my-client.ribbon.NFLoadBalancerRuleClassName", MyRule.class.getName());
// 获取 REST 请求客户端
RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
// 创建请求实例
HttpRequest request = HttpRequest.newBuilder().uri("/user/1").build();
// 发送 6 次请求
for (int i = 0; i < 6; i++) {
HttpResponse response = client.executeWithLoadBalancer(request);
String entity = response.getEntity(String.class);
System.out.println(entity);
}
}
}
我们先启动两个服务器,端口分别为 8081
和 8082
,再运行代码,可以看到输出了 6 次 {"id":1,"username":"swotxu","age":18,"message":"http://localhost:8081/user/1"}
至此,自定义规则演示就完成了,本例的负载规则较为简单,目的是让读者了解负载均衡的原理。下面我们介绍下 Ribbon 自带的负载策略。
3、Ribbon 自带的负载规则
Ribbon 提供了若干个内置的负载规则,使用者完全可以直接使用,主要有以下内置的负载规则。
-
RoundRobinRule 轮询策略
系统默认的规则,通过简单地轮询服务列表来选择服务器,其他规则在很多情况下仍然使用 RoundRobinRule。 -
AvailabilityFilteringRule 可用过滤策略
此规则会过滤掉无法连接的服务器、并发数过高的服务器,在剩下的服务器中选择。a. 无法连接的服务器
在默认情况下,如果 3 次连接失败,该服务器将会被置为“短路”状态,该状态将持续 30 秒;如果再次连接失败,“短路”状态将以几何级数增加。可通过修改配置niws.loadbalancer.<clientName>.connectionFailureCountThreshold
属性,来设置连接失败次数。b. 并发数过高的服务器
如果连接到该服务器的并发数过高,也会被这个规则忽略,可以通过修改<clientName> .ribbon.ActiveConnectionsLimit
属性来设置最高并发数。 -
WeightedResponseTimeRule 权重策略
为每个服务器赋予一个权重值,服务器的响应时间越长,该权重值就越少,这个规则会随机选择服务器,权重值有可能会决定服务器的选择。 -
ZoneAvoidanceRule 区域策略
该规则以区域、可用服务器为基础进行服务器选择。使用 Zone 对服务器进行分类,可以理解为机架或者机房。 -
BestAvailableRule 与策略二类似
忽略“短路”的服务器,并选择并发数较低的服务器。 -
RandomRule 随机策略
-
RetryRule 重试策略
含有重试的选择逻辑,如果使用 RoundRobinRule 选择的服务器无法连接,则将会重新选择服务器。
以上提供的负载规则基本可以满足大部分的需求,如果有更为复杂的要求,建议实现自定义负载规则。
如我工作中,遇到一个需求,需要自定义一个IP_Hash的负载策略,具体如何实现,可以看我另外一个博客:>>自定义IP_Hash的Rule策略<<
四、Ribbon 的 Ping 机制
1、服务 Ping 机制
在负载均衡器中,提供了 Ping 机制,每隔一段时间,会去 Ping 服务器,判断服务器是否存活。 该工作由 IPing 接口的实现类负责,如果单独使用 Ribbon,在默认情况下,不会激活 Ping 机制,默认的实现类为 DummyPing。
下面我们使用另一个 IPing 的实现类 PingUrl
package com.swotxu.firstribbonclient.customping;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.PingUrl;
import com.netflix.loadbalancer.Server;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @Date: 2020/8/2 16:32
* @Author: swotXu
*/
public class TestPingUrl {
public static void main(String[] args) throws InterruptedException {
// 创建负载均衡器
BaseLoadBalancer balancer = new BaseLoadBalancer();
//添加服务器列表
List<Server> servers = new ArrayList<Server>();
servers.add(new Server("localhost", 8081));
// 一个不存在的端口
servers.add(new Server("localhost", 8088));
balancer.addServers(servers);
// 设置 IPing 的实现类 PingUrl
balancer.setPing(new PingUrl());
// 设置 Ping 时间间隔为 2s
balancer.setPingInterval(2);
TimeUnit.SECONDS.sleep(6);
// 进行6次服务器选择
for (Server server : balancer.getAllServers()) {
System.out.printf("%s 状态: %b %n", server.getHostPort(), server.isAlive());
}
}
}
我们使用代码的方法来设置负载均衡器使用 PingUrl ,设置了每隔 2 秒就向两个服务器发起请求,PingUrl 实际使用的是 HttpClient 。在运行前先以 8081 端口启动前面介绍的服务器。最终控制台运行结果如下:
localhost:8081 状态: true
localhost:8088 状态: false
注意代码中的以下两个配置。
- my-client.ribbon.NFLoadBalancerPingClassName :配置 IPing 的实现类。
- my-client.ribbon.NFLoadBalancerPinglnterval :配置 IPing 操作的时间间隔。
2、自定义 Ping
通过前面章节的案例可知,实现自定义 Ping 较为简单,先实现 IPing 接口,然后再通过配置来设定具体的 Ping 实现类。
package com.swotxu.firstribbonclient.customping;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.Server;
/**
* @Date: 2020/9/19 18:38
* @Author: swotXu
*/
public class MyPing implements IPing {
public boolean isAlive(Server server) {
System.out.printf("这是自定义Ping实现类:%s \n", server.getHostPort());
return true;
}
}
要使用自定义的 Ping 类,通过修改 <client>.<nameSpace>.NFLoadBalancerPingClassName 配置即可。
五、其他配置
- NFLoadBalancerClassName:指定负载均衡器的实现类,可利用该配置实现自己的负载均衡器。
- NIWSServerListClassName:服务器列表处理类,用来维护服务器列表,Ribbon 已经实现动态服务器列表。
- NIWSServerListFilterClassName :用于处理服务器列表拦截。
六、项目下载
1、项目完整结构图
2、源码下载
码云Gitee仓库地址:https://gitee.com/swotxu/Spring-Cloud-Study.git
>>戳这里<<
项目路径:Spring-Cloud-Study/02/ribbon01
为了能演示 Ribbon 与 Spring 的完美结合,下篇,我们将会以本案例为基础,讲解如何在Spring使用Ribbon。
下篇:Spring Cloud 入门到进阶 - 02 Ribbon 负载均衡 (下)
别忘了点赞关注收藏~