- 前言
- 一、Eureka基础知识
- 二、SpringCloud整合Eureka作为注册中心
- 2.1 Eureka注册中心搭建
- 2.2 服务提供者支付服务搭建
- 2.3 消费者订单服务搭建
- 2.4 Eureka集群原理总结
- 2.5 再谈RestTemplate和Ribbon的关系
- 2.6 服务发现Discovery
- 2.7 Eureka自我保护机制
- 三、总结
在传统的RPC远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册。
一、Eureka基础知识Eureka采用了C/S的设计架构,分为Server端和Client端,Eureka Sever作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到 Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。
在服务注册与发现中,有一个注册中心。当服务器启动的时候,服务提供者会把当前自己服务器的信息如服务地址通讯地址等以别名方式注册到注册中心上。服务消费者以别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用,整个RPC远程调用框架核心设计思想在于:注册中心,注册中心能够管理各个微服务之间的依赖关系,这叫做服务治理。在任何RPC远程框架中,都会有一个注册中心存放服务地址相关信息(接口地址)。
Eureka包含两个组件:Eureka Server和Eureka Client
Eureka Server:
Eureka Server提供服务注册服务,各个微服务节点通过配置启动后在Eureka Server中进行注册,这样Eureka Server中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
Eureka Client:
Eureka Client通过注册中心进行访问可用的微服务,它是一个Java客户端,用于简化与Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)。
因此,如果我们需要进行演示测试,那么我们需要搭建Eureka Server的服务端作为注册中心,But,注册中心作为重中之重,我们如果只有一个节点,那么将可能会导致注册中心挂掉之后服务不可用。Spring Cloud封装了Netflix 公司开发的Eureka模块来实现服务治理,因此新版本的Eureka依赖为如下两个部分:
二、SpringCloud整合Eureka作为注册中心org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.cloud spring-cloud-starter-netflix-eureka-client
在本节中,我们将使用SpringCloud整合Eureka作为注册中心,提供服务注册、服务治理和服务发现功能。
在本次整合案例中,我们将使用新版的(2020.2后的)Eureka进行测试,业务功能为:提供一个Eureka Server注册中心,构建两个服务提供者,均提供支付服务,构建一个服务消费者:提供订单服务。
本次使用父子模块的形式进行搭建,在父pom中提供公用组件版本管理,注意packaging打包方式修改为pom:
UTF-8 1.8 1.8 4.12 1.18.16 8.0.18 1.1.20 1.2.17 2.1.2 org.springframework.boot spring-boot-dependencies 2.2.2.RELEASE pom import org.springframework.cloud spring-cloud-dependencies Hoxton.SR1 pom import mysql mysql-connector-java ${mysql.version} com.alibaba druid ${druid.version} org.mybatis.spring.boot mybatis-spring-boot-starter ${mybatis.spring.boot.version} junit junit ${junit.version} log4j log4j ${log4j.version} org.projectlombok lombok ${lombok.version} true provided
本次使用的MySQL库表为:
DROp TABLE IF EXISTS `payment`; CREATE TABLE `payment` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `serial` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;2.1 Eureka注册中心搭建
org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok org.springframework.boot spring-boot-starter-test test junit junit
properties配置文件:
server.port=7001
# 设置该服务注册中心(eureka服务端)的hostname
eureka.instance.hostname=eureka7001.com
# 由于我们目前创建的应用是一个服务注册中心,而不是普通的应用,默认情况下,这个应用会向注册中心(也是它自己)注册它自己,设置为false表示禁止这种自己向自己注册的默认行为
eureka.client.register-with-eureka=false
#表示不去检索其他的服务,因为服务注册中心本身的职责就是维护服务实例,它不需要去检索其他服务
eureka.client.fetch-registry=false
#设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址
#单机版
#eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
#集群版
eureka.client.service-url.defaultZone=http://eureka7002.com:7002/eureka/
#关闭eureka自我保护机制,保证服务不可用时及时剔除
eureka.server.enable-self-preservation=false
eureka.server.eviction-interval-timer-in-ms=2000
最后在主启动类中加入注解@EnableEurekaServer:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer //开启Eureka注册中心服务端
public class Eureka7001Application {
public static void main(String[] args) {
SpringApplication.run(Eureka7001Application.class,args);
}
}
上面我们就搭建好了一个注册中心7001,搭建7002也很简单,只需要修改properties配置文件中的端口号、注册中心的hostname:eureka.instance.hostname=eureka7002.com和eureka.client.service-url.defaultZone=http://eureka7001.com:7001/eureka/指向7001的Eureka即可。
完成之后我们就搭建好Eureka注册中心集群了,搭建集群规则为,将自己注册进集群中的其他注册中心,比如我们当前两个Eureka注册中心,其结构图如下:
注意,此时在配置文件中我们配置的Eureka实例名称分别为eureka7001.com和eureka7002.com(单机时我们可以直接取名localhost就能找到),但是实际使用时候是不能进行解析的,因此在C盘中找到C:WindowsSystem32driversetc路径下的hosts文件,在末尾添加两个映射:
127.0.0.1 eureka7001.com 127.0.0.1 eureka7002.com
接下来我们启动Eureka注册中心7001和7002进行测试,我们可以随便进一个Eureka Server看看,是否进行了正常注册,以7001为例:http://localhost:7001/
发现7002已经正常注册进来了,说明注册中心集群搭建没问题。
在这块我们将搭建服务名为:cloud-provider-payment8001和cloud-provider-payment8002的两个服务提供者。
搭建的目录结构如下:
cloud-provider-payment8001依赖如下:
org.springframework.cloud spring-cloud-starter-netflix-eureka-client com.dl.springcloud cloud-api-commons ${project.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.mybatis.spring.boot mybatis-spring-boot-starter com.alibaba druid-spring-boot-starter 1.1.10 mysql mysql-connector-java org.springframework.boot spring-boot-starter-jdbc org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true org.springframework.boot spring-boot-starter-test test
在上面依赖中,有一个依赖是我们本地打包的依赖,cloud-api-commons包含了公共的部分代码,数据库对应实体类Payment和统一返回结果类CommonResult:
cloud-api-commons依赖为:
SpringCloud com.dl 1.0-SNAPSHOT 4.0.0 com.dl.springcloud cloud-api-commons org.springframework.boot spring-boot-devtools runtime true org.projectlombok lombok true cn.hutool hutool-all 5.1.0 org.springframework.boot spring-boot-maven-plugin true true
数据库对应实体类Payment:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
private Long id;
private String serial;
}
统一返回结果类CommonResult:
package com.dl.response; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class CommonResult{ private Integer code; private String message; private T data; public CommonResult(Integer code, String message,T data){ this.code=code; this.message=message; this.data=data; } public static CommonResult createBySuccessMsg(String message,T data){ return new CommonResult (200,message,data); } public static CommonResult createByErrorMsg(String message,T data){ return new CommonResult (404,message,data); } }
然后利用idea的maven打包按钮或者使用maven命令打包即可。
maven打包命令为:
mvn -T 4 -DskipTests=true -am -pl cloud-api-commons clean package
-T 4 :多线程打包,4指使用4个线程
-DskipTests :跳过测试
-am:构建指定模块,同时构建指定模块依赖的其他模块;
pl presto-udf:手动选择需要构建的项目,这里选择cloud-api-commons
clean package:这个命令实际上做了很多步骤,总结为:清理-编译-打包
关于maven的打包命令可参见:https://dh-butterfly.blog.csdn.net/article/details/120332563
上面我们已经将支付服务提供者8001的依赖解释得很清楚了,接下来我们开始编写配置文件,配置文件中使用到的有些东西后面用到了再进行解释:
server.port=8001 spring.profiles.active=default #热部署生效,默认已经开启 #spring.devtools.restart.enabled=true #微服务名称,会在Eureka中显示 spring.application.name=cloud-payment-service #数据源和数据库配置 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/cloud?serverTimezone=UTC&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=5120154230 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource #mybatis扫描xml位置 mybatis.mapper-locations=classpath:mapper/*.xml # 所有Entity别名类所在包 mybatis.type-aliases-package=com.dl.entity #eureka # 表示是否将自己注册进Eureka Server中,默认为true。 eureka.client.register-with-eureka=true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 eureka.client.fetch-registry=true #设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址, # 单机版 #eureka.client.service-url.defaultZone=http://localhost:7001/eureka #Eureka集群 eureka.client.service-url.defaultZone=http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ #指定实例id eureka.instance.instance-id=payment8001 #访问信息有IP信息提示,(就是将鼠标指针移至payment8001,payment8002名下,会有IP地址提示) eureka.instance.prefer-ip-address=true
主启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
//@MapperScan({"com.dl.dao"})
public class Payment8001Application {
public static void main(String[] args) {
SpringApplication.run(Payment8001Application.class,args);
}
}
在dao接口中,提供两个方法:
@Mapper
public interface PaymentDao {
int createPayment(Payment payment); //支付订单创建
Payment getPaymentById(Long id); //根据id查询订单
}
对应xml文件为:
id,serial insert into payment(serial) values(#{serial})
接下来就是service和service实现类:
public interface PaymentService {
int createPayment(Payment payment);
Payment getPaymentById(Long id);
}
其实现类为:
import com.dl.dao.PaymentDao;
import com.dl.entity.Payment;
import com.dl.service.PaymentService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class PaymentServiceImpl implements PaymentService {
@Resource
private PaymentDao paymentDao;
@Override
public int createPayment(Payment payment) {
return paymentDao.createPayment(payment);
}
@Override
public Payment getPaymentById(Long id) {
return paymentDao.getPaymentById(id);
}
}
在控制层controller:
import com.dl.entity.Payment;
import com.dl.response.CommonResult;
import com.dl.service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@Slf4j
public class PaymentController {
@Autowired
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@RequestMapping(value = "payment/create",method = RequestMethod.POST)
public CommonResult createPayment(@RequestBody Payment payment){
log.info("serverPort: "+serverPort+"payment="+payment);
int result=paymentService.createPayment(payment);
if(result>=1){
return CommonResult.createBySuccessMsg("插入数据库成功serverPort: "+serverPort,result);
}else{
return CommonResult.createByErrorMsg("数据插入失败",result);
}
}
@RequestMapping(value = "/payment/get",method = RequestMethod.GET)
public CommonResult getPaymentById(@RequestParam(value = "id",defaultValue = "1") Long id){
Payment payment=paymentService.getPaymentById(id);
log.info("serverPort: "+serverPort+"查询id="+id+"查询结果为:"+payment);
if(payment!=null){
return CommonResult.createBySuccessMsg("查询成功,serverPort: "+serverPort,payment);
}else{
return CommonResult.createByErrorMsg("没有对应id的记录:"+id,null);
}
}
}
8001创建好了,那么8002的创建相信也没什么难度,主要就是修改一下properties配置文件中的端口号,以及指定的实例id:eureka.instance.instance-id=payment8002,创建好之后可以进行自测,调用上面两条接口能正常和DB打交道即可。
2.3 消费者订单服务搭建接下来就需要搭建消费者模块,搭建一个cloud-consumer-order80模块,通过调用注册中心中服务提供者(8001和8002)提供的创建支付服务以及根据id查询订单服务完成订单创建以及查询订单功能。
其主要依赖如下,其他与8001一致:
org.springframework.cloud spring-cloud-starter-netflix-eureka-client
订单服务80的配置文件properties
server.port=80 #微服务名称 spring.application.name=cloud-order-service #eureka # 表示是否将自己注册进Eureka Server中,默认为true。 eureka.client.register-with-eureka=true #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡 eureka.client.fetch-registry=true #设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址 eureka.client.service-url.defaultZone=http://localhost:7001/eureka,http://eureka7002.com:7002/eureka/
主启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@SpringBootApplication
@EnableEurekaClient //Eureka客户端是一个Java客户端,用来连接Eureka服务端,与服务端进行交互、负载均衡,服务的故障切换等;
public class Order80Application {
public static void main(String[] args) {
SpringApplication.run(Order80Application.class,args);
}
}
在控制层controller中利用resttemplate进行远程调用:
import com.dl.entity.Payment;
import com.dl.response.CommonResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
@RestController
@RequestMapping("/consumer/")
public class OrderController {
@Autowired
private RestTemplate restTemplate;
// public static final String PAYMENT_URL = "http://localhost:8001"; //单机环境下直接使用eureka注册中心服务
public static final String PAYMENT_URL = "http://cloud-payment-service"; //集群环境下使用服务名称
@PostMapping("payment/create")
public CommonResult createPayment(Payment payment){
return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment,CommonResult.class);
}
@GetMapping("payment/get")
public CommonResult getPayment(@RequestParam("id")Long id){
return restTemplate.getForObject(PAYMENT_URL+"/payment/get?id="+id,CommonResult.class);
}
}
由于我们需要使用RestTemplate进行远程调用,其中RestTemplate是Spring Web模块提供的一个基于Rest规范提供Http请求的工具。因此需要对其进行配置,同时因为这里使用的服务提供者为集群环境,因此赋予RestTemplate负载均衡的能力:
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@LoadBalanced //赋予RestTemplate负载均衡的能力,默认是轮询
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
接下来就是进行测试了:
- 启动Eureka注册中心7001和7002
- 启动支付服务8001和8002
- 启动订单服务80调用接口进行测试
由于是post请求,因此我们使用postman进行测试,测试接口为:http://127.0.0.1/consumer/payment/create,其中80端口可以省略,并且添加参数serial,测试结果如下,多次测试发现8001与8002端口交替出现,验证了使用负载均衡。
查询接口为:http://localhost/consumer/payment/get?id=1
服务注册:将服务信息注册进注册中心,实质上存储为key-value方式,key为服务名,value为调用地址。
服务发现:从注册中心上获取服务信息,利用服务名去注册中心上找相应的服务调用地址。
上面我们搭建的集群服务进行原理说明:
2.5 再谈RestTemplate和Ribbon的关系1、先启动eureka注主册中心。
2、启动服务提供者payment支付服务。
3、支付服务启动后会把自身信息(比服务地址以别名方式注朋进eureka。
4、消费者order服务在需要调用接口时,使用服务别名去注册中心获取实际的RPC远程调用地址。
5、消费者知道调用地址后,底层实际是利用HttpClient技术实现远程调用。
6、消费者获得服务地址后会缓存在本地jvm内存中,默认每间隔30秒更新—次服务调用地址。
RestTemplate是Spring Web模块提供的一个基于Rest规范提供Http请求的工具。而Ribbon是SpringCloud中客户端负载均衡的组件。在微服务架构中,往往通过RestTemplate发送RPC请求,然后通过Ribbon做客户端负载均衡。那么它们是如何配合工作的。在上面,我们通过@LoadBalanced实现了客户端的负载均衡功能,这个注解主要是通过拦截器在RestTemplate工作前进行负载均衡。该拦截器中注入了LoadBalancerClient(负载均衡客户端)的实现。当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会执行LoadBalancerInterceptor类的intercept函数。由于我们在使用RestTemplate时候采用了服务名作为host,所以直接从HttpRequest的URI对象中通过getHost()就可以拿到服务名,然后调用execute函数去根据服务名来选择实例并发起实际的请求。
LoadBalancerClient是一个负载均衡的接口,有不同的负载均衡客户端实现,这里的实现是RibbonLoadBalancerClient
由于我们将 eureka.instance.instance-id设置为一个字符串名字而不是IP地址,因此,在Eureka Server中我们也只能看到字符串名字如:payment8001,因此可以进行简单配置后在Eureka Server中访问信息有IP信息提示,(就是将鼠标指针移至payment8001,payment8002名下,会有IP地址提示),在配置文件中共配置一个属性即可:eureka.instance.prefer-ip-address=true
每个服务想要获取注册中心的其他服务怎么办呢?很简单,利用Eureka的服务发现功能即可,以8001为例,在启动类上加上服务发现注解@EnableDiscoveryClient,然后修改在Controller中添加一条用于服务发现测试的接口即可。
@Autowired private DiscoveryClient discoveryClient; @RequestMapping(value = "/payment/discovery",method = RequestMethod.GET) public CommonResult
启动服务Eureka Server、80订单服务和8001之后调用地址:http://localhost:8001/payment/discovery得到如下结果:
后台也会有相应的输出:
Eureka的保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据,也就是不会注销任何微服务。
如果在Eureka Server的首页看到以下这段红色提示,则说明Eureka进入了保护模式:
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THANTHRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUSTTO BE SAFE
主要原因为:当某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存。主要还是为了Eureka Client可以正常运行,防止与Eureka Server网络不通情况下,Eureka Server不会立刻将Eureka Client服务剔除,属于CAP里面的AP分支。
我们都知道,Eureka采用C/S架构方式,那么Eureka Server如何确认Eureka Client还存活着呢,答案就是利用心跳机制,Client每隔一段时间发送一个心跳包告诉Server自己还存活着,默认情况下,如果Eureka Server在一定时间内没有接收到某个微服务实例的心跳,Eureka Server将会注销该实例(默认90秒)。但是当网络分区故障发生(延时、卡顿、拥挤)时,微服务与Eureka Server之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过“自我保护模式”来解决这个问题——当Eureka Server节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。
如果Eureka Server端在一定时间内(默认90秒)没有收到Eureka Client发送心跳包,便会直接从服务注册列表中剔除该服务,但是在短时间( 90秒中)内丢失了大量的服务实例心跳,这时候Eureka Server会开启自我保护机制,不会剔除该服务(该现象可能出现在如果网络不通,但是Eureka Client为出现宕机,此时如果换做别的注册中心如果一定时间内没有收到心跳会将剔除该服务,这样就出现了严重失误,因为客户端还能正常发送心跳,只是网络延迟问题,而保护机制是为了解决此问题而产生的)。在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。
如果想要禁止自我保护,只需要在配置文件中进行配置即可:
eureka.server.enable-self-preservation=false #禁用自我保护功能 eureka.server.eviction-interval-timer-in-ms=2000 #设置心跳间隔,单位为ms
默认情况下:
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒) eureka.instance.lease-renewal-interval-in-seconds=30 #Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务 eureka.instance.lease-expiration-duration-in-seconds=90
说明情况:
三、总结Eureka 的自我保护模式是有意义的,该模式被激活后,它不会从注册列表中剔除因长时间没收到心跳导致租期过期的服务, 而是等待修复,直到心跳恢复正常之后,它自动退出自我保护模式。这种模式旨在避免因网络分区故障导致服务不可用的问题。例如,两个客户端实例 C1 和 C2 的连通性是良好的,但是由于网络故障,C2 未能及时向 Eureka 发送心跳续约,这时候 Eureka不能简单的将 C2 从注册表中剔除。因为如果剔除了,C1 就无法从 Eureka 服务器中获取 C2 注册的服务,但是这时候 C2 服务是可用的。
本文对Eureka基础知识进行了介绍,并利用SpringCloud整合Eureka作为注册中心用于服务治理、服务注册和服务发现,通过搭建集群方式完成了测试。



