SpringCloud学习



以下为常用服务注册中间件

Eureka

Eureka自我保护

访问Eureka主页时,如果看到这样一段大红色的句子:

EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

那么表明Eureka的自我保护模式是启动的。

相反如果看见如下

THE SELF PRESERVATION MODE IS TURNED OFF. THIS MAY NOT PROTECT INSTANCE EXPIRY IN CASE OF NETWORK/OTHER PROBLEMS.

那么表明Eureka的自我保护模式是关闭的

背景:正常情况下,如果Eureka Server在一定时间内(默认90秒)没有接收到某个微服务实例的心跳,Eureka Server将会移除该实例。

原理:如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障(比如网络故障或频繁的启动关闭客户端),Eureka Server自动进入自我保护模式。不再剔除任何服务,当网络故障恢复后,该节点自动退出自我保护模式。

配置自我保护机制:eureka.server.enable-self-preservation,true表示开启,false表示关闭(不推荐)。

系统架构图

Eureka使用

使用Eureka注册服务
  1. 引入eureka server依赖
1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>${your.eureka.version}</version>
</dependency>
  1. eureka server相关配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring:
application:
# 微服务名称
name: cloud-eureka-server

eureka:
instance:
hostname: eureka7001.com #eureka服务端的实例名称
client:
register-with-eureka: false #false表示不向注册中心注册自己。
fetch-registry: false #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
service-url:
#集群指向其它eureka
#defaultZone: http://localhost:7002/,http://localhost:7003/
#单机就是7001自己
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
server:
#关闭自我保护机制,保证不可用服务被及时剔除,默认开启true
enable-self-preservation: false
eviction-interval-timer-in-ms: 2000 #续期时间,即扫描失效服务的间隔时间(缺省为60 * 1000ms)
  1. 启动类上添加@EnableEurekaServer服务注册。
使用Eureka注册客户端
  1. 引入eureka-client依赖
1
2
3
4
5
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>${your.eureka.version}</version>
</dependency>
  1. eureka client相关配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
spring:
application:
# 微服务名称
name: cloud-payment-service #注意如果自定义赋值均衡算法时找不到服务名,就改成大写:CLOUD-PAYMENT-SERVICE

eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: true
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#服务注册地址 ,http://localhost:7002/eureka,http://localhost:7002/eureka/
defaultZone: http://localhost:7001/eureka/
# 集群版
#defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/
instance:
# 服务实例名
instance-id: payment9001
#访问路径可以显示IP地址
prefer-ip-address: true
#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
lease-renewal-interval-in-seconds: 1
#Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
lease-expiration-duration-in-seconds: 2
  1. 启动类上注解@EnableEurekaClient注册进相应的服务上
  2. 访问Eureka页面,可以发现客户端已经注册进服务了

使用Eureka注册消费者
  1. 引入eureka-client依赖
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  1. 相关配置
1
2
3
4
5
6
7
8
9
10
11
eureka:
client:
#表示是否将自己注册进EurekaServer默认为true。
register-with-eureka: false
#是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
fetchRegistry: true
service-url:
#单机
defaultZone: http://localhost:7001/eureka/
# 集群
#defaultZone: http://localhost:7001/eureka/,http://localhost:7002/eureka/
  1. 在启动类上标注@EnableEurekaClient注解
使用Ribbon负载均衡

一:使用自带的轮询算法

  1. 前提:

    客户端(生产者)需要是集群版的,即eureka.client.service-url.defaultZone=http://localhost:7001/eureka/,http://localhost:7002/eureka/ ,不然就一直是访问的一个serverport

  2. 案例:赋予RestTemplate在客户端负载均衡的能力

1
2
3
4
5
6
7
8
9
@Configuration
public class ApplicationContextConfig {

@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}

注解原理具体阐述请参考:https://blog.csdn.net/Tincox/article/details/79210309

二:自定义随机规则

  1. 将自定义规则注入Spring
1
2
3
4
5
6
7
8
@Configuration
public class MySelfRule {
@Bean
public IRule myRule() {
//定义为随机
return new RandomRule();
}
}
  1. 自定义轮询算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public interface LoadBalancer {
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}

------------------------------

@Component
public class MyLb implements LoadBalancer {

private AtomicInteger atomicInteger = new AtomicInteger(0);

public final int getAndIncrement() {
int current;
int next;

do {
current = this.atomicInteger.get();
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
} while (!this.atomicInteger.compareAndSet(current, next));
System.out.println("*****第几次访问,次数next: " + next);
return next;
}

// 负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启动后rest接口计数从1开始。
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();

return serviceInstances.get(index);
}
}
  1. 自动类上标注@RibbonClient(name = “CLOUD-PAYMENT-SERVICE”,configuration= MySelfRule.class)注解

注意服务名大小写需要一致,不然可能出现服务 not found。

使用@EnableDiscoveryClient服务发现
  1. 获取服务注册实例相关信息
1
2
3
4
5
6
7
8
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");

if (instances == null || instances.size() <= 0) {
return null;
}

ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();
  1. 对比@EnableEurekaClient
相同 差异
@EnableEurekaClient 都是能够让注册中心能够发现,扫描到该服务 适用于Eureka作为注册中心
@EnableDiscoveryClient 都是能够让注册中心能够发现,扫描到该服务(推荐) 支持多个注册中心是使用
使用actuator微服务信息完善

如下图所示,我们注册中心里的eureka server注册中心状态含有主机名称

按照规范的要求只暴露服务名,去掉主机名,于是我们可以添加actuator依赖,设置相关信息

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

然后配置文件

1
2
3
4
eureka:
instance:
instance-id: payment9001
prefer-ip-address: true #鼠标悬浮访问路径时可以显示IP地址

查看修改过后的状态

zookeeper

zookeeper使用

注册Demo
  1. 引入相关依赖
1
2
3
4
5
<!-- SpringBoot整合zookeeper -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
</dependency>
  1. 注册配置
1
2
3
4
5
6
7
#服务别名----注册zookeeper到注册中心名称
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: yourip:2181
  1. 启动类上标注注解@EnableDiscoveryClient ,注册服务。

Consul

特性

Consul包含多个组件,但是作为一个整体,为你的基础设施提供服务发现和服务配置的工具.他提供以下关键特性:

  • 服务发现 Consul的客户端可用提供一个服务,比如 api 或者mysql ,另外一些客户端可用使用Consul去发现一个指定服务的提供者.通过DNS或者HTTP应用程序可用很容易的找到他所依赖的服务.
  • 健康检查 Consul客户端可用提供任意数量的健康检查,指定一个服务(比如:webserver是否返回了200 OK 状态码)或者使用本地节点(比如:内存使用是否大于90%). 这个信息可由operator用来监视集群的健康.被服务发现组件用来避免将流量发送到不健康的主机.
  • Key/Value存储 应用程序可用根据自己的需要使用Consul的层级的Key/Value存储.比如动态配置,功能标记,协调,领袖选举等等,简单的HTTP API让他更易于使用.
  • 多数据中心: Consul支持开箱即用的多数据中心.这意味着用户不需要担心需要建立额外的抽象层让业务扩展到多个区域.

官方链接:https://book-consul-guide.vnzmi.com/01_what_is_consul.html

consul使用

注册Demo
  1. 引入相关依赖
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
  1. 注册配置
1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: consul-provider-payment
# consul注册中心地址
cloud:
consul:
host: localhost
port: 8500
discovery:
#hostname: 127.0.0.1
service-name: ${spring.application.name}
  1. 启动类上标注注解@EnableDiscoveryClient ,注册服务。

Nacos

官网传送门:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html

基本概念

(1)Nacos 是阿里巴巴推出来的一个新开源项目,是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您 快速实现动态服务发现、服务配置、服务元数据及流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。 Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。

(2)常见的注册中心:

  1. Eureka(原生,2.0遇到性能瓶颈,停止维护)

  2. Zookeeper(支持,专业的独立产品。例如:dubbo)

  3. Consul(原生,GO语言开发)

  4. Nacos

相对于 Spring Cloud Eureka 来说,Nacos 更强大。Nacos = Spring Cloud Eureka + Spring Cloud Config

Nacos 可以与 Spring, Spring Boot, Spring Cloud 集成,并能代替 Spring Cloud Eureka, Spring Cloud Config

通过 Nacos Server 和 spring-cloud-starter-alibaba-nacos-discovery 实现服务的注册与发现。

(3)Nacos是以服务为主要服务对象的中间件,Nacos支持所有主流的服务发现、配置和管理。

Nacos主要提供以下四大功能:

  1. 服务发现和服务健康监测

  2. 动态配置服务

  3. 动态DNS服务

  4. 服务及其元数据管理

服务注册

提供者注册

  1. 需要引入相关依赖
1
2
3
4
5
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>${latest.version}</version>
</dependency>
  1. 使用yml文件注册服务
1
2
3
4
5
6
7
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
  1. 在启动类上开启注解@EnableDiscoveryClient即可
  2. 输入:localhost:8848/nacos访问管理后台,就发现已经成功注册进去啦

消费者注册和负载

  1. 引入相关依赖
1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. yml文件配置
1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848

# 消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
  1. 在启动类上开启注解@EnableDiscoveryClient即可
  2. 因为我们访问的服务提供者是个集群式的,所以我们还需要做负载均衡处理
1
2
3
4
5
6
7
8
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced //负载均衡
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
  1. 调用服务Api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@Slf4j
public class OrderNacosController {
@Resource
private RestTemplate restTemplate;

@Value("${service-url.nacos-user-service}")
private String serverURL;

@GetMapping(value = "/consumer/payment/nacos/{id}")
public String paymentInfo(@PathVariable("id") Long id) {
// 调用服务提供者中的Api
return restTemplate.getForObject(serverURL + "/payment/get/" + id, String.class);
}

}

6.访问 /consumer/payment/nacos/ ,就可以看到负载均衡成功生效了。

  1. 如果网页出现中文乱码,可以添加如下配置
1
2
3
4
5
6
7
spring:
http:
encoding:
force: true
charst: UTF-8
enabld: true
uri-encoding: UTF-8

服务配置中心

  1. 引入相关依赖
1
2
3
4
5
6
7
8
9
10
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. 针对生产环境配置文件
1
2
3
4
5
spring:
profiles:
active: dev # 表示开发环境
#active: test # 表示测试环境
#active: info
  1. 创建bootstrap.yml配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# nacos配置
server:
port: 3377

spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
group: DEV_GROUP #指定分组,默认DEFAULT_GROUP
namespace: public #指定命名空间,默认public

# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yaml
  1. 添加访问config的Api
1
2
3
4
5
6
7
8
9
10
11
@RestController
@RefreshScope //支持Nacos的动态刷新功能。
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;

@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}

@RefreshScope:支持Nacos的动态刷新功能。改了配置列表的版本号,重新访问即可拿到更改后的。

  1. 在nacos配置管理 - 配置列表,添加配置如下

  1. 访问 local host:3377/config/info,即可如图所示

持久化配置

使用MySQL + Nacos做持久化配置

  1. 在Nacos的config文件夹中找到文件nacos-mysql.sql
  2. 导入数据库,如图所示

  1. 然后在application.properties添加关联mysql的数据配置
1
2
3
4
5
6
spring.datasource.platform=mysql

db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&serverTimezone=GMT
db.user=root
db.password=youdotknown
  1. 在nacos配置管理 - 配置列表 添加配置,查看数据库数据

  1. 重启Nacos后,发现配置的数据依旧存在,成功。

以下为Springcloud常用技术栈

OpenFeign

基本介绍

我们知道OpenFeign是用在Spring Cloud中的声明式的web service client

OpenFeignServer就是一个普通的Rest服务,不同的是我们需要将他注册到eureka server上面,方便后面的OpenFeignClient调用。

服务调用Demo

  1. 引入相关依赖
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 启动类上开启@EnableFeignClients服务调用Api注解

  2. 调用

1
2
3
4
5
6
7
8
9
@Component
@FeignClient(value = "被调用的服务名", fallback = 被调用方法所在的类.class)
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);

@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}

超时控制

1
2
3
4
5
6
7
8
9
10
11
# 开启熔断机制
feign:
hystrix:
#设置feign 客户端超时时间(openFeign默认支持ribbon)
enabled: true

ribbon:
#指的是建立连接后从服务器读取到可用资源所用的时间
ReadTimeout: 5000
#指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
ConnectTimeout: 5000

我们可以在被调用的方法中加入休眠时间

1
2
3
4
5
6
7
8
public String paymentInfo_TimeOut(Integer id) {
try {
TimeUnit.MILLISECONDS.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池: " + Thread.currentThread().getName() + " id: " + id + "\t" + "O(∩_∩)O哈哈~";
}

然后如果超过了配置的时间,就会出现如下错误。

Hystrix

基本介绍

Hystrix框架通过提供熔断降级来控制服务之间的交互依赖,通过隔离故障服务并停止故障的级联效应以提高系统的总体弹性。

作用:

  • 为第三方客户端提供保护,控制延迟和失败(通常依赖网络)等故障。
  • 停止复杂的分布式系统中的级联故障。
  • 快速失败,迅速恢复。
  • 回退并在可能的情况下正常降级。
  • 启用近乎实时的监视,警报和配置控制。

Hystrix解决什么问题?

当前请求依赖多个服务,并且这些服务工作正常,能够及时响应请求。

如果某个依赖服务出现故障,它可以阻止整个用户请求,势必会影响到当前请求。例如出现故障的服务没有及时响应,请求它的时间达到了5秒,那么当前请求也会阻塞5秒。

由于WEB服务器资源是有限的,当慢请求越来越多时会造成资源等待,并加速空闲资源的消耗,直到耗尽所有资源,此时服务器已经不能响应新的正常请求,也就是服务彻底挂了。

服务降级

当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作。

引入hystrix相关依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

在启动类上开启Hystrix注解@EnableHystrix

1
2
3
4
5
6
7
8
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableCircuitBreaker
public @interface EnableHystrix {

}

Hystrix之全局服务降级DefaultProperties

1
2
3
4
5
@RestController
@DefaultProperties(defaultFallback = "payment_Global_FallbackMethod")
public class OrderHystirxController {
......
}
1
2
3
public String payment_Global_FallbackMethod() {
return "Global异常处理信息,请稍后再试,/(ㄒoㄒ)/~~";
}

然后我们需要使用全局异常方法,只需要在Contrller的方法上标注注解@HystrixCommand即可

Hystrix之服务降级订单(消费者)侧fallback

1
2
3
4
5
6
7
8
9
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeOutFallbackMethod", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id) {
// int age = 10 / 0;
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}

HystrixCommand配置的fallback方法

1
2
3
public String paymentTimeOutFallbackMethod(@PathVariable("id") Integer id) {
return "我是消费者9015,对方支付系统繁忙请10秒钟后再试或者自己运行出错请检查自己,o(╥﹏╥)o";
}

其中:

@HystrixCommand(fallbackMethod = “”})vip配置,超时时间1500

@HystrixCommand 使用的是默认的@DefaultProperties(defaultFallback = “payment_Global_FallbackMethod”)配置

如果我们编译int age = 10 / 0;那么就会报错,执行默认的全局配置defaultFallback。倘若我们进行了defaultFallback的重新配置,那么便会执行约定好的方法payment_Global_FallbackMethod。

Hystrix之服务降级支付(生产者)侧fallback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000")
})
public String paymentInfo_TimeOut(Integer id) {
// int age = 10/0;
try {
TimeUnit.MILLISECONDS.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池: " + Thread.currentThread().getName() + " id: " + id + "\t" + "O(∩_∩)O哈哈~" + " 耗时(秒): ";
}

public String paymentInfo_TimeOutHandler(Integer id) {
return "线程池: " + Thread.currentThread().getName() + " 8001系统繁忙或者运行报错,请稍后再试,id: " + id + "\t" + "o(╥﹏╥)o";
}

如果期间我们编译int age = 10 / 0 或者 休眠时间超过配置的5秒中;那么就会报错,便会执行约定好的方法paymentInfo_TimeOutHandler。

Hystrix之通配服务降级FeignFallback

1
2
3
4
5
6
@GetMapping("/consumer/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id) {
int age = 10 / 0;
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}

我们实现上述OpenFeign中的调用Api接口

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class PaymentFallbackServiceImpl implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_OK ,o(╥﹏╥)o";
}

@Override
public String paymentInfo_TimeOut(Integer id) {
return "-----PaymentFallbackService fall back-paymentInfo_TimeOut ,o(╥﹏╥)o";
}
}

服务熔断

当链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", commandProperties = {
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"),
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id) {
if (id < 0) {
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();

return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
}
1
2
3
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " + id;
}

服务熔断后,可以发现我们突然关闭被调用接口的服务或者调用过程中出现异常,再访问,就可以发现如下信息返回

HystrixProperty配置参数

circuitBreaker.enabled:是否开启断路器

circuitBreaker.requestVolumeThreshold:请求次数

circuitBreaker.sleepWindowInMilliseconds:时间窗口期:服务熔断后经过一段时间再转换为半开状态,单位毫秒

circuitBreaker.errorThresholdPercentage:x次请求下,失败率达到60%后跳闸

熔断类型:

  • 熔断打开:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入半熔断状态
  • 熔断关闭:熔断关闭不会对服务进行熔断
  • 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断。

服务限流

1
2
3
4
5
6
7
8
9
10
11
@HystrixCommand(
fallbackMethod = "helloError",
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "5"),
@HystrixProperty(name = "maximumSize", value = "5"),
@HystrixProperty(name = "maxQueueSize", value = "10")
}
)
public String sayHello(String name) {
...
}

ThreadPool的配置

配置项 配置说明 默认值
coreSize 核心线程数,请求高峰时99.5%的平均响应时间 + 向上预留一些即可 10
maximumSize 最大线程数 10
keepAliveTimeMinutes 线程的有效时长 1分钟
maxQueueSize 配置线程池等待队列长度,默认值:-1,-1表示不等待直接拒绝,测试表明线程池使用直接拒绝测试。 -1

图形化Dashboard监控

  1. 引入相关依赖
1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 在启动类上开启注解 @EnableHystrixDashboard
  2. 在被监控的服务中添加如下配置,否则访问服务监控页面会出现: Unable to connect to Command Metric Stream.
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 此配置是为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
* ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
* 只要在自己的项目里配置上下面的servlet就可以了
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}

4.启动项目我们就可以访问到监控首页啦

  1. 输入:https//ip:port/hystrix.stream

Sentinel

Sentinel是什么?

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。它的功能类似SpringCloud原生组件Hystrix

Hystrix存在的问题

  • 需要我们程序员自己手工搭建监控平台
  • 没有一套web界面可以给我们进行更加细粒度化的配置,流量控制,速率控制,服务熔断,服务降级。。

这个时候Sentinel应运而生

  • 单独一个组件,可以独立出来
  • 直接界面化的细粒度统一配置

约定 > 配置 >编码,虽然我们的需求都可以写在代码里,但是尽量使用注解和配置代替编码

它的特征

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel-features-overview

Sentinel简单示例

  1. 相关依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<!--SpringCloud ailibaba sentinel-datasource-nacos 后续做持久化用到-->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.6.3</version>
</dependency>

<!--SpringCloud ailibaba sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
  1. 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 8401

spring:
application:
name: sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8070 #配置Sentinel dashboard地址
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
  1. api接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@RestController
@Slf4j
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------testA";
}

@GetMapping("/testB")
public String testB() {
log.info(Thread.currentThread().getName() + "\t" + "...testB");
return "------testB";
}


@GetMapping("/testD")
public String testD() {
log.info("testD 异常比例");
int age = 10 / 0;
return "------testD";
}

@GetMapping("/testE")
public String testE() {
log.info("testE 测试异常数");
int age = 10 / 0;
return "------testE 测试异常数";
}

}

启动服务,此时启动sentinel会发现里面什么都没有,因为sentinel采用的是懒加载模式,访问http://localhost:8401/testA``http://localhost:8401/testB,即可

image-20201016170117431

流控规则

基本介绍

image-20201016170237916

字段说明

  • 资源名:唯一名称,默认请求路径
  • 针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认default(不区分来源)
  • 阈值类型 / 单机阈值
    • QPS:(每秒钟的请求数量):但调用该API的QPS达到阈值的时候,进行限流
    • 线程数:当调用该API的线程数达到阈值的时候,进行限流
  • 是否集群:不需要集群
  • 流控模式
    • 直接:api都达到限流条件时,直接限流
    • 关联:当关联的资源达到阈值,就限流自己
    • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【API级别的针对来源】
  • 流控效果
    • 快速失败:直接失败,抛异常
    • Warm UP:根据codeFactory(冷加载因子,默认3),从阈值/CodeFactor,经过预热时长,才达到设置的QPS阈值
    • 排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须设置QPS,否则无效

服务熔断

雪崩效应

在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。服务雪崩效应是一种因“服务提供者”的不可用导致“服务消费者”的不可用,并将不可用逐渐放大的过程。

如果下图所示:A作为服务提供者,B为A的服务消费者,C和D是B的服务消费者。A不可用引起了B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。

参考-博主-纯洁的微笑

Spring Cloud调用接口过程

Spring Cloud 在接口调用上,大致会经过如下几个组件配合:

Feign —–>Hystrix —>Ribbon —>Http Client(apache http components 或者 Okhttp) 具体交互流程上,如下图所示:

(1)接口化请求调用当调用被@FeignClient注解修饰的接口时,在框架内部,将请求转换成Feign的请求实例feign.Request,交由Feign框架处理。

(2)Feign :转化请求Feign是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,封装了Http调用流程。

(3)Hystrix:熔断处理机制 Feign的调用关系,会被Hystrix代理拦截,对每一个Feign调用请求,Hystrix都会将其包装成HystrixCommand,参与Hystrix的流控和熔断规则。如果请求判断需要熔断,则Hystrix直接熔断,抛出异常或者使用FallbackFactory返回熔断Fallback结果;如果通过,则将调用请求传递给Ribbon组件。

(4)Ribbon:服务地址选择 当请求传递到Ribbon之后,Ribbon会根据自身维护的服务列表,根据服务的服务质量,如平均响应时间,Load等,结合特定的规则,从列表中挑选合适的服务实例,选择好机器之后,然后将机器实例的信息请求传递给Http Client客户端,HttpClient客户端来执行真正的Http接口调用;

(5)HttpClient :Http客户端,真正执行Http调用根据上层Ribbon传递过来的请求,已经指定了服务地址,则HttpClient开始执行真正的Http请求

sentinel整合Ribbon + openFeign + fallback

搭建 9003 和 9004 服务提供者

不设置任何参数

然后在使用 84作为服务消费者,当我们值使用 @SentinelResource注解时,不添加任何参数,那么如果出错的话,是直接返回一个error页面,对前端用户非常不友好,因此我们需要配置一个兜底的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback") //没有配置
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);

if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}

return result;
}
设置fallback
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",fallback = "handlerFallback") //fallback只负责业务异常
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);

if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}

return result;
}

//本例是fallback
public CommonResult handlerFallback(@PathVariable Long id,Throwable e) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(444,"兜底异常handlerFallback,exception内容 "+e.getMessage(),payment);
}

加入fallback后,当我们程序运行出错时,我们会有一个兜底的异常执行,但是服务限流和熔断的异常还是出现默认的

设置blockHandler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback",blockHandler = "blockHandler" ,fallback = "handlerFallback") //blockHandler只负责sentinel控制台配置违规
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);

if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}

return result;
}

//本例是blockHandler
public CommonResult blockHandler(@PathVariable Long id,BlockException blockException) {
Payment payment = new Payment(id,"null");
return new CommonResult<>(445,"blockHandler-sentinel限流,无此流水: blockException "+blockException.getMessage(),payment);
}
blockHandler和fallback一起配置
1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback", blockHandler = "blockHandler") //blockHandler只负责sentinel控制台配置违规
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);

if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}

return result;
}

若blockHandler 和 fallback都进行了配置,则被限流降级而抛出 BlockException时,只会进入blockHandler处理逻辑

异常忽略

使用exceptionsToIgnore,当遇到IllegalArgumentException异常时,会直接返回错误信息页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback", fallback = "handlerFallback", exceptionsToIgnore = {IllegalArgumentException.class})
public CommonResult<Payment> fallback(@PathVariable Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);

if (id == 4) {
throw new IllegalArgumentException("IllegalArgumentException,非法参数异常....");
} else if (result.getData() == null) {
throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
}

return result;
}

与Hystrix对比

参考文档传送门:https://www.cnblogs.com/kabuda/p/13831829.html

Gateway

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

网关基本概念

1、API 网关介绍

API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:

(1)客户端会多次请求不同的微服务,增加了客户端的复杂性。

(2)存在跨域请求,在一定场景下处理相对复杂。

(3)认证复杂,每个服务都需要独立认证。

(4)难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。

(5)某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。

以上这些问题可以借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性

Gateway 基本介绍

Spring cloud gateway是 spring 官方基于 Spring 5.0、Spring Boot2.0 和 Project Reactor 等技术开发的网关,Spring Cloud Gateway 旨在为微服务架构提供简单、有效和统一的 API 路由管理方式,Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其 不仅提供统一的路由方式,并且还基于 Filer 链的方式提供了网关基本的功能,例如:安全、监控/埋点、限流等

核心概念

网关提供 API 全托管服务,丰富的 API 管理功能,辅助企业管理大规模的 API,以降低管理成本和安全风险,包括协议适配、协议转发、安全策略、防刷、流量、监控日志等功能。一般来说网关对外暴露的 URL 或者接口信息,我们统称为路由信息。 如果研发过网关中间件或者使用过 Zuul 的人,会知道网关的核心是 Filter 以及 Filter Chain(Filter 责任链)。Sprig Cloud Gateway 也具有路由和 Filter 的概念。下面介绍一下 Spring Cloud Gateway 中几个重要的概念。

(1)路由。路由是网关最基础的部分,路由信息有一个 ID、一个目的 URL、一组断言和一组 Filter 组成。如果断言路由为真,则说明请求的 URL 和配置匹配

(2)断言。Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring5.0 框架中的 ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于 http request 中的任何信息,比如请求头和参数等。

(3)过滤器。一个标准的 Spring webFilter。Spring cloud gateway 中的 filter 分为两种类型的 Filter,分别是 Gateway Filter 和 Global Filter。过滤器 Filter 将会对请求和响应进行修改处理

如上图所示,Spring cloud Gateway 发出请求。然后再由 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway web handler。Handler 再通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑,然后返回。

路由

配置路由的两种方式
  1. Java代码配置
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();

routes.route("path_route_atguigu",
r -> r.path("/guonei")
.uri("http://news.baidu.com/guonei")).build();

return routes.build();
}
}
  1. 通过配置文件配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 9527

spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
配置动态路由

默认情况下Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#lb:在此服务中进行负载均衡,服务名不区分大小写
uri: lb://cloud-payment-service #匹配后提供服务的路由地址

- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
常用的Predicate

匹配/payment/get/下的所有请求

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#lb:在此服务中进行负载均衡,服务名不区分大小写
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai] # 时间节点后才可以访问
- Cookie=username,layu # curl ip:port/路径 --cookie "username=layu"
- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
自定义过滤器

请求必须携 uname参数,否则就会被拦截掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("***********come in MyLogGateWayFilter: " + new Date());

String uname = exchange.getRequest().getQueryParams().getFirst("uname");

if (uname == null) {
log.info("*******用户名为null,非法用户,o(╥﹏╥)o");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}

return chain.filter(exchange);
}

@Override
public int getOrder() {
return 0;
}
}

Actuator

开始使用

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

端点

Actuator端点让你监视和与应用程序交互,Spring Boot包含许多内置的端点,并允许你添加自己的端点。例如,health端点提供基本的应用程序健康信息。

可以启用或禁用每个单独的端点,这将控制端点是否被创建,以及它的bean是否存在于应用程序上下文中,要实现远程访问,端点还必须通过JMX或HTTP公开,大多数应用程序选择HTTP,将端点的ID与/actuator的前缀映射到URL。例如,默认情况下,health端点映射到/actuator/health

启用端点

默认情况下,除了shutdown之外的所有端点都启用了,要配置端点的启动,可以使用它的management.endpoint.<id>.enabled属性,下面的示例启用关闭端点:

1
management.endpoint.shutdown.enabled=true
公开端点
属性 默认
management.endpoints.jmx.exposure.exclude
management.endpoints.jmx.exposure.include *
management.endpoints.web.exposure.exclude
management.endpoints.web.exposure.include info, health

include属性列出了公开的端点的id,exclude属性列出不应公开的端点的id,exclude属性优先于include属性,includeexclude属性都可以使用端点id列表进行配置。

例如,要停止在JMX上公开所有端点,并且只公开healthinfo端点,请使用以下属性:

1
management.endpoints.jmx.exposure.include=health,info

*可用于选择所有端点,例如,要通过HTTP公开除envbeans端点之外的所有内容,请使用以下属性:

1
2
management.endpoints.web.exposure.include=*
management.endpoints.web.exposure.exclude=env,beans

*在YAML中有特殊的含义,所以如果要包含(或排除)所有端点,请务必添加引号,如下例所示:

1
2
3
4
5
management:
endpoints:
web:
exposure:
include: "*"

如果你的应用程序是对外公开的,我们强烈建议你也保护你的端点。

CORS支持

CORS支持在默认情况下是禁用的,并且只在management.endpoints.web.cors.allowed-origins属性已设置时才启用,以下配置允许从example.com域GETPOST调用:

1
2
management.endpoints.web.cors.allowed-origins=http://example.com
management.endpoints.web.cors.allowed-methods=GET,POST

参考文档Spring Boot 参考指南 - 端点

打赏
  • 版权声明: 本网站所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  1. © 2020-2021 Lauy    湘ICP备20003709号-1

请我喝杯咖啡吧~

支付宝
微信