1. 负载均衡
负载均衡是分布式架构的重点,负载均衡机制决定着整个服务集群的性能与稳定。Eureka 服务实例可以进行集群部署,每个实例都均衡处理服务请求,那么这些请求是如何被分摊到各个服务实例中的呢~~~~
2. Ribbon 简介
Ribbon 是 Netflix 下的负载均衡项目,它在集群中为各个客户端的通信提供了支持,它主要实现中间层应用程序的负载均衡。Ribbon 提供了以下特性:
同时 Ribbon 可以与 Eureka 整合使用,Ribbon 同样被集成到 Spring Cloud 中,作为 spring-cloud-netflix 项目中的子模块。Spring Cloud 将 Ribbon 的 API 进行了封装,使用者可以使用封装后的 API 来实现负载均衡,也可以直接使用 Ribbon 的原生 API。
3. 负载均衡器组件
Ribbon 的负载均衡器主要与集群中的各个服务器进行通信,负载均衡器需要提供以下基础功能:
为了实现负载均衡的基础功能,Ribbon 的负载均衡器有以下三大子模块:
4. 实践案例
(1). 创建Maven项目:crazyspringcloud-firstribbon-server,并添加web依赖:
4.0.0
crazyspringcloud-firstribbon-server
crazyspringcloud-firstribbon-server
com.sztxtech.creazyspringcloud
crazyspringcloud-parent
1.0-SNAPSHOT
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-web
(2). 创建启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FirstRibbonServerApp {
public static void main(String[] args) {
SpringApplication.run(FirstRibbonServerApp.class, args);
}
}
(3). 创建控制器类,添加 REST 服务:
import com.sztxtech.creazyspringcloud.entity.Person;
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;
@RestController
public class RibbonController {
@RequestMapping(value = "/person/{personId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public Person findPersonById(@PathVariable("personId") Integer personId, HttpServletRequest request){
Person person = new Person();
person.setId(personId);
person.setName("项振海");
person.setAge(28);
person.setMessage(request.getRequestURL().toString());
return person;
}
@RequestMapping(value = "/", method = RequestMethod.GET)
public String hello(){
return "hello";
}
}
(4). 创建Maven项目:crazyspringcloud-firstribbon-client,并添加相关依赖:
4.0.0
crazyspringcloud-firstribbon-client
crazyspringcloud-firstribbon-client
com.sztxtech.creazyspringcloud
crazyspringcloud-parent
1.0-SNAPSHOT
UTF-8
UTF-8
1.8
com.netflix.ribbon
ribbon
com.netflix.ribbon
ribbon-httpclient
org.springframework.cloud
spring-cloud-starter-ribbon
org.springframework.boot
spring-boot-starter-web
(5). 创建测试类:
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;
public class FirstRibbonClientApp {
public static void main(String[] args) throws Exception {
// 设置请求的服务器
ConfigurationManager.getConfigInstance().setProperty(
"my-client.ribbon.listOfServers",
"localhost:8761,localhost:8762");
// 获取 REST 请求客户端
RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
// 创建请求实例
HttpRequest httpRequest = HttpRequest.newBuilder().uri("/person/1156532").build();
// 发送100次请求到服务器中
for (int i=0; i<100; i++){
HttpResponse response = client.executeWithLoadBalancer(httpRequest);
String result = response.getEntity(String.class);
System.out.println("======>>" + result);
}
}
}
(6). 复制 crazyspringcloud-firstribbon-server 项目,修改名称以及相关冲突内容;并分别设置两个server项目的服务端口号为8761、8762;依次启动两个server服务,确保两个都正常运行后运行 crazyspringcloud-firstribbon-client 的测试类:FirstRibbonClientApp,可查看控制台输出的结果;这里为方便查看,将输出结果通过excel 处理后得到如下数据(100条数据,这里只截取前面的15条):
info | port | suffix |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8762 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8761 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8762 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8761 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8762 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8761 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8762 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8761 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8762 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8761 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8762 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8761 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8762 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8761 | /person/1156532"} |
======>>{"id":1156532,"name":"项振海","age":28,"message":"http://localhost: | 8762 | /person/1156532"} |
分析输出数据中的端口号可知,RestClient 轮流向 8761 和 8762 端口发送请求,可见在 RestClient 中已经帮我们实现了负载均衡的功能。
5. Ribbon 的配置
在编写客户端时,使用了 ConfigurationManager 来设置配置项,除了在代码中指定配置项外,还可以将配置放到 .properties 文件中。ConfigurationManager 的 loadPropertiesFromResources 方法可指定 properties 文件的位置,配置格式如下:
..=
其中:
如果相对全部客户端生效,可以将客户端名称去掉,直接以
my-client.ribbon.listOfServers=localhost:8761,localhost:8762
Ribbon 的配置同样可以在 Spring Cloud 的配置文件(即 application.tml)中使用。
6. Ribbon 的负载均衡机制
Ribbon 提供了几个负载均衡的组件,其目的就是让请求转给盒实的服务器来处理。因此,如何选择盒实的服务器就成为负载均衡机制的核心。
(1) 负载均衡器
Ribbon 的负载均衡器接口定义了服务器的操作,主要是用于进行服务器选择;上面的案例中,客户端使用了 RestClient 类,在发送请求时,会使用负载均衡器(ILoadBalancer)接口,根据特定的逻辑来选择服务器,服务器列表可使用listOfServers进行配置,也可以使用动态更新机制。例如如下代码,使用负载均衡器来选择服务器:
public class FirstRibbonClientApp {
public static void main(String[] args) throws Exception {
// 创建负载均衡器
ILoadBalancer lb = new BaseLoadBalancer();
// 添加服务器
List servers = new ArrayList();
servers.add(new Server("localhost", 8761));
servers.add(new Server("localhost", 8762));
lb.addServers(servers);
// 进行10次服务器选择
for(int i=0; i<100; i++){
Server server = lb.chooseServer(null);
System.out.println(server);
}
}
}
代码中使用了 BaseLoadBalancer 这个负载均衡我,将两个服务器对象加入负载均衡器中,调用10次 chooseServer() 方法,可以看到如下所示的输出结果;从结果来看,和上面实践案例中选择服务器的逻辑是一致的;在默认情况下。会使用 RoundRobinRule 的规则逻辑。
"D:\Program Files\Java\jdk1.8.0_112\bin\java.exe" "-javaagent:D:\Program ......
15:16:59.761 [main] WARN com.netflix.config.sources.URLConfigurationSource - No URLs will be polled as dynamic configuration sources.
15:16:59.764 [main] INFO com.netflix.config.sources.URLConfigurationSource - To enable URLs as dynamic configuration sources, define System property archaius.configurationSource.additionalUrls or make config.properties available on classpath.
15:16:59.775 [main] INFO com.netflix.config.DynamicPropertyFactory - DynamicPropertyFactory is initialized with configuration sources: com.netflix.config.ConcurrentCompositeConfiguration@73035e27
15:16:59.910 [main] DEBUG com.netflix.loadbalancer.BaseLoadBalancer - LoadBalancer [default]: clearing server list (SET op)
15:16:59.912 [main] DEBUG com.netflix.loadbalancer.BaseLoadBalancer - LoadBalancer [default]: addServer [localhost:8761]
15:16:59.912 [main] DEBUG com.netflix.loadbalancer.BaseLoadBalancer - LoadBalancer [default]: addServer [localhost:8762]
localhost:8762
localhost:8761
localhost:8762
localhost:8761
localhost:8762
localhost:8761
localhost:8762
localhost:8761
localhost:8762
localhost:8761
Process finished with exit code 0
(2) 自定义负载规则
至此可知,选择哪个服务器进行请求处理,是由 ILoadBalancer 接口的 chooseServer() 方法来决定的。而在 BaseLoadBalancer 类中,则使用 IRule 接口的 choose() 方法来决定选择哪一个服务器对象;如果想自定义负载均衡规则,可以编写一个 IRule 接口的实现类来定义自己所需要的规则。例如:
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;
import java.util.List;
/**
* 自定义负载均衡规则
*/
public class MyRule implements IRule {
// 负载均衡器接口
ILoadBalancer iLoadBalancer;
@Override
public void setLoadBalancer(ILoadBalancer lb) {
this.iLoadBalancer = lb;
}
@Override
public ILoadBalancer getLoadBalancer() {
return this.iLoadBalancer;
}
/** 带参构造器 */
public MyRule(ILoadBalancer iLoadBalancer) {
this.iLoadBalancer = iLoadBalancer;
}
/** 无参构造器 */
public MyRule() {
}
/**
* 实现 IRule 接口中的 choose() 方法,决定选择哪一个服务器对象;
* @param key
* @return
*/
@Override
public Server choose(Object key) {
// 获取全部的服务器
List servers = iLoadBalancer.getAllServers();
// 只返回第一个Server对象
return servers.get(0);
}
}
在自定义规则类中,实现的 choose() 方法调用了 ILoadBalancer 的 getAllServers() 方法,返回全部的服务器;为了简单起见,这里只返回第一个服务器。为了能在负载均衡器中使用自定义的规则,需要修改选择服务器的代码;创建测试类代码如下:
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import com.sztxtech.creazyspringcloud.ribbonrule.MyRule;
import java.util.ArrayList;
import java.util.List;
/**
* 测试自定义负载均衡规则
*/
public class TestMyRule {
public static void main(String[] args){
// 创建负载均衡器
BaseLoadBalancer blb = new BaseLoadBalancer();
// 设置自定义的负载均衡规则
blb.setRule(new MyRule());
// 添加服务器
List servers = new ArrayList();
servers.add(new Server("localhost", 8761));
servers.add(new Server("localhost", 8762));
servers.add(new Server("localhost", 8763));
servers.add(new Server("localhost", 8764));
blb.addServers(servers);
// 进行10次服务器选择
for (int i=0; i<10; i++){
Server server = blb.chooseServer(null);
System.out.println("第一次:" + server);
}
}
}
运行以上测试代码,运行结果如下,请求10次所得到的服务器都是 localhost:8760。
以上是使用编码的方式来设置负载均衡规则的,下面的第二种方式是使用配置的方式来实现对负载均衡规则进行设置的案例:
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;
import com.sztxtech.creazyspringcloud.ribbonrule.MyRule;
/**
* 测试自定义负载均衡规则
*/
public class TestMyRule {
public static void main(String[] args) throws Exception{
//===第一种方式====================================================================================
// // 创建负载均衡器
// BaseLoadBalancer blb = new BaseLoadBalancer();
//
// // 设置自定义的负载均衡规则
// blb.setRule(new MyRule());
//
// // 添加服务器
// List servers = new ArrayList();
// servers.add(new Server("localhost", 8761));
// servers.add(new Server("localhost", 8762));
// servers.add(new Server("localhost", 8763));
// servers.add(new Server("localhost", 8764));
// blb.addServers(servers);
//
// // 进行10次服务器选择
// for (int i=0; i<10; i++){
// Server server = blb.chooseServer(null);
// System.out.println("第"+ i + "次:" + server);
// }
//
//===第二种方式====================================================================================
// 设置请求的服务器
ConfigurationManager.getConfigInstance().setProperty("my-client.ribbon.listOfServers","localhost:8761,localhost:8762,localhost:8763");
// 配置规则处理类
ConfigurationManager.getConfigInstance().setProperty("my-client.ribbon.NFLoadBalancerRuleClassName", MyRule.class.getName());
// 获取REST请求客户端
RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
// 获取请求实例
HttpRequest request = HttpRequest.newBuilder().uri("/person/1156532").build();
// 进行10次服务器选择
for (int i=0; i<10; i++){
HttpResponse response = client.executeWithLoadBalancer(request);
String result = response.getEntity(String.class);
System.out.println("第"+ i + "次:" + result);
}
}
}
这种方式与前面的相比,只是加入了 my-client.ribbon.NFLoadBalancerRuleName 属性,设置了自定义规则处理类为 MyRule,这个配置项同样可以在配置文件中使用,包括 Spring Colud 的配置文件(application.yml等)。依次启动端口号为8761和8762的服务,确认正常启动后,运行TestMyRule;看到控制台输出的10次输入结果如下,输出的10次全都8761端口处理的,所以可以确定自定义的规则生效了:
第0次:{"id":1156532,"name":"项振海","age":28,"message":"http://localhost:8761/person/1156532"} |
第1次:{"id":1156532,"name":"项振海","age":28,"message":"http://localhost:8761/person/1156532"} |
第2次:{"id":1156532,"name":"项振海","age":28,"message":"http://localhost:8761/person/1156532"} |
第3次:{"id":1156532,"name":"项振海","age":28,"message":"http://localhost:8761/person/1156532"} |
第4次:{"id":1156532,"name":"项振海","age":28,"message":"http://localhost:8761/person/1156532"} |
第5次:{"id":1156532,"name":"项振海","age":28,"message":"http://localhost:8761/person/1156532"} |
第6次:{"id":1156532,"name":"项振海","age":28,"message":"http://localhost:8761/person/1156532"} |
第7次:{"id":1156532,"name":"项振海","age":28,"message":"http://localhost:8761/person/1156532"} |
第8次:{"id":1156532,"name":"项振海","age":28,"message":"http://localhost:8761/person/1156532"} |
第9次:{"id":1156532,"name":"项振海","age":28,"message":"http://localhost:8761/person/1156532"} |
在实际环境中,若要实现自定义的负载均衡规则,可能还需要结合各种因素,例如考虑具体业务的发生事件、服务器性能等。实现中可能还涉及使用计算器、数据库等技术。
(3) Ribbon 自带的负载均衡规则
Ribbon 提供了若干个内置的负载均衡规则,使用者完全可以直接使用,主要有以下内置的负载规则:
① RoundRobinRule:系统默认的规则,通过简单的轮询服务列表来选择服务器,其他规则在很多情况下仍然使用 RoundRobinRule;
② AvailabilityFilteringRule:该规则会忽略以下服务器:
③ WeightedResponseTimeRule:为每个服务器赋予一个权重值,服务器的响应时间越长,该权重值就越少;这个规则会随机选择服务器,权重值也可能会决定服务器的选择;
④ ZoneAvoidanceRule:该规则以区域、可用服务器为基础进行服务器选择。使用 Zone 对服务器进行分类,可以理解为机架或机房;
⑤ BestAvailableRule:忽略 “短路” 的服务器,并选择并发数较低的服务器;
⑥ RandomRule:随机选择可用的服务器;
⑦ RetryRule:含有重试的选择逻辑,如果使用 RoundRobinRule 选择的服务器无法连接,那么将会重新选择服务器;
以上提供的负载均衡规则基本可以满足大部分的需求,如果有更为复杂的要求,建议实现自定义的负载规则。
(4) Ping 机制
在负载均衡器中,提供了 Ping 机制,每隔一段时间会去 Ping 服务器,判断服务器是否存活。该工作由 IPing 接口的实现类负责,如果单独使用 Ribbon,在默认情况下,不会激活 Ping 机制,默认的实现类为 DummyPing。如下所示代码使用了另外一个 IPing 实现类 PingUrl:
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.PingUrl;
import com.netflix.loadbalancer.Server;
import java.util.ArrayList;
import java.util.List;
public class TestPingUrl {
public static void main(String[] args) throws Exception {
// 创建负载均衡器
BaseLoadBalancer blb = new BaseLoadBalancer();
// 添加服务器
List servers = new ArrayList<>();
// 8761端口连接正常
servers.add(new Server("localhost",8761));
servers.add(new Server("localhost",8762));
// 一个不存在的端口
servers.add(new Server("localhost", 8888));
blb.addServers(servers);
// 设置 IPing 实现类
blb.setPing(new PingUrl());
// 设置 Ping 时间间隔为2秒
blb.setPingInterval(2);
Thread.sleep(6000);
for (Server server : blb.getAllServers()) {
System.out.println("端口:" + server.getHostPort() + " || 状态:" + server.isAlive());
}
}
}
说明:以上案例使用代码的方法来设置负载均衡器使用 PingUrl,设置了每隔2秒就向服务器发起请求,PingUrl 实际使用的是 HttpClient。在以上案例中,实际上会请求 http://localhost:8761、http://localhost:8762、http://localhost:8888 三个地址;
依次启动端口号为8761和8762的两个服务,确保两个服务正常启动后,运行以上测试类,可在控制台看到测试结果如下:
可以看到测试结果:8761、8762 端口的服务器状态是正常的;而8888端口的服务器是无法连接的。
除了在代码中配置使用 IPing 类之外,还可以在配置中设置 IPing 实现类:
package com.sztxtech.creazyspringcloud;
import com.netflix.client.ClientFactory;
import com.netflix.config.ConfigurationManager;
import com.netflix.loadbalancer.PingUrl;
import com.netflix.loadbalancer.Server;
import com.netflix.niws.client.http.RestClient;
import java.util.List;
public class TestPingUrl {
public static void main(String[] args) throws Exception {
//===第一种方式====================================================================================
// // 创建负载均衡器
// BaseLoadBalancer blb = new BaseLoadBalancer();
// // 添加服务器
// List servers = new ArrayList<>();
// // 8761端口连接正常
// servers.add(new Server("localhost",8761));
// servers.add(new Server("localhost",8762));
// // 一个不存在的端口
// servers.add(new Server("localhost", 8888));
// blb.addServers(servers);
// // 设置 IPing 实现类
// blb.setPing(new PingUrl());
// // 设置 Ping 时间间隔为2秒
// blb.setPingInterval(2);
//
// Thread.sleep(6000);
//
// for (Server server : blb.getAllServers()) {
// System.out.println("端口:" + server.getHostPort() + " || 状态:" + server.isAlive());
// }
//===第二种方式====================================================================================
// 设置请求的服务器
ConfigurationManager.getConfigInstance().setProperty(
"my-client.ribbon.listOfServers",
"localhost:8761,localhost:8762,localhost:8888");
// 配置 Ping 处理类
ConfigurationManager.getConfigInstance().setProperty(
"my-client.ribbon.NFLoadBalancerPingClassName", // 配置 IPing 的实现类
PingUrl.class.getName());
// 配置 Ping 时间间隔
ConfigurationManager.getConfigInstance().setProperty(
"my-client.ribbon.NFLoadBalancerPingInterval", 2); // 配置 Ping 操作的时间间隔
// 获取REST请求客户端
RestClient client = (RestClient) ClientFactory.getNamedClient("my-client");
Thread.sleep(6000);
// 获取全部服务器
List servers = client.getLoadBalancer().getAllServers();
System.out.println("获取到服务器总数为:" + servers.size());
// 输出状态
for (Server server : servers) {
System.out.println("端口:" + server.getHostPort() + " || 状态:" + server.isAlive());
}
}
}
说明:需要注意以上代码中的以下两项配置,同样可以使用在配置文件中:
依次启动端口号为8761和8762的两个服务,确保两个服务正常启动后,运行以上测试类,可在控制台看到测试结果如下:
(5) 自定义 Ping
通过前面的案例可知,实现自定义 Ping 需要先实现 IPing 接口,然后再通过配置来设定具体的 Ping 实现类;示例如下:
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.Server;
/**
* 自定义 Ping
*/
public class MyPing implements IPing {
@Override
public boolean isAlive(Server server) {
System.out.println("这是自定义 Ping 实现类:" + server.getHostPort());
return true;
}
}
(6) 其他配置
Ribbon 负载均衡器的负载规则以及 Ping 可以通过配置来实现逻辑的改变。除了这两部分外,还可以使用以下配置来改变负载均衡器的其他行为:
7. Ribbon 在Spring Cloud中的使用
Spring Cloud 集成了 Ribbon,结合 Eureka ,可以实现客户端的负载均衡;下面以 RestTemplate 为基础,测试 Eureka 中的 Ribbon 配置;
(1) 分别创建两个 Eureka 服务器项目:crazyspringcloud-cloudribbon-server_one 和 crazyspringcloud-cloudribbon-server_two ,端口分别设置为 8761 和 8762;
(2) 创建 Eureka 服务提供者项目:crazyspringcloud-cloudribbon-provider,端口号为:9001;该项目注意进行以下工作;在控制器中发布一个 REST 服务,地址为:/person/{personId},请求后返回 Person 实例,其中 Person 的 message 为 HTTP 请求的 URL;