SpringCloud学习笔记(4)

Hystrix 断路器

官方地址:https://github.com/Netflix/Hystrix/wiki/How-To-Use

“断路器“本身是一种开关装置,当某个服务单元发生故障之后,涌过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

概述

服务雪崩:

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出

1
2
A-->B,C
BC-->D

如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,即所谓的“雪崩效应”。

对于高流量的应用来说,单一的后端依赖可能会导致所有服务器上的所有资源都在几秒钟内饱和。比失败更糟糕的是,这些应用程序还可能导致服务之间的延迟增加,备份队列,线程和其他系统资源紧张,导致整个系统发生更多的级联故障。这些都表示需要对故障和延迟进行隔离和管理,以便单个依赖关系的失败,不能取消整个应用程序或系统。

Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

Hystrix停止更新,进入维护阶段:https://github.com/Netflix/Hystrix

https://github.com/Netflix/Hystrix/wiki/How-To-Use

服务降级:

fallback

服务器忙碌或者网络拥堵时,不让客户端等待并立刻返回一个友好提示,fallback。

  • 对方系统不可用了,你需要给我一个兜底的方法,不要耗死。
  • 向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

降级发生的情况:

  • 程序运行异常
  • 超时
  • 服务熔断触发服务降级
  • 线程池/信号量打满也会导致服务降级
服务熔断:

break

  • 类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示
  • 就是保险丝:服务的降级->进熔断->恢复调用链路
服务限流:

flowlimit

秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟几个,有序进行

可见,上面的技术不论是消费者还是提供者,根据真实环境都是可以加入配置的。

断路案例

首先构建一个eureka作为服务中心的单机版微服务架构 ,这里使用之前eureka Server 7001模块,作为服务中心

新建 提供者 provider-hystrix-payment8001 模块:

pom 文件:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?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>cloud2022</artifactId>
<groupId>com.wzg.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>provider-hystrix-payment8001</artifactId>

<dependencies>
<!-- hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.wzg.springcloud</groupId>
<artifactId>api-commons</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

</project>

yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 8001


eureka:
client:
register-with-eureka: true #表识不向注册中心注册自己
fetch-registry: true #表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
# defaultZone: http://eureka7002.com:7002/eureka/ #设置与eureka server交互的地址查询服务和注册服务都需要依赖这个地址
defaultZone: http://eureka7001.com:7001/eureka/
# server:
# enable-self-preservation: false
spring:
application:
name: provider-hystrix-payment
# eviction-interval-timer-in-ms: 2000

主启动类:

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
32
33
package com.wzg.springcloud;

import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;

/**
* @author whlie(true){learn}
*/
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableEurekaClient
@EnableCircuitBreaker//加上这个
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class,args);
}

@Bean // 注入豪猪的servlet // 该servlet与服务容错本身无关 // springboot默认路径不是/hustrix.stream,只要在自己的项目里自己配置servlet
public ServletRegistrationBean getServlet(){
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(streamServlet);
servletRegistrationBean.setLoadOnStartup(1);
servletRegistrationBean.addUrlMappings("/hystrix.stream");
servletRegistrationBean.setName("HystrixMetricsStreamServlet");
return servletRegistrationBean;
}

}

service层:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.wzg.springcloud.Service;

import cn.hutool.core.util.IdUtil;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;

import java.util.concurrent.TimeUnit;

/**
* @author whlie(true){learn}
*/
@Service
public class PaymentService {

//服务降级

/* 可以正常访问的方法*/
public String paymentinfo_Ok(Integer id){
return "线程池:" + Thread.currentThread().getName() + "--paymentInfo_OK,id:" + id;
}

/* 超时访问的方法 */
@HystrixCommand(fallbackMethod = "paymentinfo_TimeoutHandler",commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",//时间单位
value="3000")})//超时时间
public String paymentinfo_Timeout(Integer id){
int interTime = 5;
try{
TimeUnit.SECONDS.sleep(interTime);//模拟超时
}catch (Exception e){
e.printStackTrace();
}
return "线程池:" + Thread.currentThread().getName() + "--paymentInfo_Timeout,id:" + id +
"耗时" + interTime + "秒钟--";
}

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

//服务熔断
@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"),// 失败率达到多少后跳闸
}) // 在10s内10次请求有60%失败 // 先看次数,再看百分比
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
if(id < 0) {
throw new RuntimeException("******id 不能负数");
}
String serialNumber = IdUtil.simpleUUID();// 等价于UUID.randomUUID().toString(); //pom中有hutool-all

return Thread.currentThread().getName()+"\t"+"调用成功,流水号: " + serialNumber;
}

public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id){//服务降级
return "id 不能负数,请稍后再试,/(ㄒoㄒ)/~~ id: " +id;
}
}

controller层:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.wzg.springcloud.controller;

import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import com.wzg.springcloud.Service.PaymentService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

/**
* @author whlie(true){learn}
*/
@RestController
@Slf4j
public class PaymentController {

@Resource
private PaymentService paymentService;

@Value("${server.port}")
private String serverPort;

@GetMapping("/payment/hystrix/{id}")
public String paymentInfo_OK(@PathVariable("id")Integer id){
log.info("paymentInfo_OKKKKOKKK");
return paymentService.paymentinfo_Ok(id);
}

// @GetMapping("/payment/hystrix/timeout/{id}")
// public String paymentInfo_Timeout(@PathVariable("id")Integer id){
// log.info("paymentInfo_timeout");
// return paymentService.paymentinfo_Timeout(id);
// }

/* 超时访问的方法 */
@GetMapping("/payment/hystrix/timeout/{id}")
// 服务端service
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",//超时后回调方法
commandProperties = {
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",//时间单位
value="5000")})//超时时间
public String paymentInfo_TimeOut(Integer id){
try { TimeUnit.MILLISECONDS.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
return "线程池: "+Thread.currentThread().getName()+" id: "+id+"\t"+"O(∩_∩)O哈哈~"+" 耗时(秒): ";
}

//====服务熔断
@GetMapping("/payment/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id")Integer id){
return paymentService.paymentCircuitBreaker(id);
}


// 兜底方法
public String paymentInfo_TimeOutHandler(Integer id){ // 回调函数向调用方返回一个符合预期的、可处理的备选响应
return "线程池: "+Thread.currentThread().getName()+" 8001系统繁忙或者运行报错,请稍后再试,id: "+id+"\t"+"o(╥﹏╥)o";
}

}

模拟高并发

JMeter

这里使用一个新东西 JMeter 压力测试器,模拟多个请求

下载压缩包,解压,双击 /bin/ 下的 jmeter.bat 即可启动

ctrl + S 保存后,输入请求地址开始压测。

1597472029967

从测试可以看出,当模拟的超长请求被高并发以后,访问普通的小请求速率也会被拉低。

tomcat的默认工作线程数被打满了,没有多余的线程来分解压力和处理。

上面还是服务8001自己测试,加入此时外部的消费者80页来访问,那消费者只能干等,最终导致消费端80不满意,服务端8001直接被拖死。

测试可见,当启动高并发测试时,消费者访问也会变得很慢,甚至出现超时报错。

演示问题:8001端口自身已经被打满了,80还要访问8001,80也响应慢了。

  • 超时导致服务器变慢(转圈):超时不再等待
  • 出错(宕机或程序运行出错):出错要有兜底

解决思路:

  • 对方服务(8001)超时了,调用者(80)不能一直卡死等待,必须有服务降级
  • 对方服务(8001) down机了,调用者(80)不能一直卡死等待,必须有服务降级
  • 对方服务(8001) OK,调用者(80)自己出故障或有自我要求(自己的等待时间小于服务提供者),自己处理降级

服务降级案例

新建消费者 consumer-feign-hystrix-order80 模块:以feign为服务调用,eureka为服务中心的模块,

pom

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<?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>cloud2022</artifactId>
<groupId>com.wzg.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>consumer-feign-hystrix-order80</artifactId>

<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<!--eureka-client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.wzg.springcloud</groupId>
<artifactId>api-commons</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

</project>

yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 80

spring:
application:
name: consumer-feign-hystrix-service

eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/

feign:
hystrix:
enabled: true

主启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.wzg.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
* @author whlie(true){learn}
*/
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
public class OrderHystrixMain80{//消费端
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class,args);
}
}

一般服务降级放在客户端,即 消费者端 ,但是提供者端一样能使用。

首先提供者,即8001 先从自身找问题,设置自身调用超时的峰值,峰值内正常运行,超出峰值需要有兜底的方法处理,作服务降级fallback

服务端降级@HystrixCommand

首先 对 8001的service进行配置(对容易超时的方法进行配置) :

降级配置:@HystrixCommand,可以在里面指定超时/出错的回调方法,作为兜底方法

消费端降级

上面的案例是服务端降级,现在我们服务端处理3s,然后返回。但是消费端等1s就等不住了,这时候就需要消费端也有降级方法

80的降级。原理是一样的,上面的@HystrixCommand降级可以放在服务端,也可以放在消费端。但一般放在客户端。

注意:我们自己配置过的热部署方式对java代码的改动明显,但对@HystrixCommand内属性的修改建议重启微服务。

然后对 80 进行服务降级:很明显 service 层是接口,所以我们对消费者,在它的 controller 层进行降级。继续使用@HystrixCommand注解指定方法超时后的回调方法

目前问题:

  • 每个业务方法对应一个兜底的方法,代码膨胀
  • 同样和自定义分开

我们定义一个全局的兜底方法,这样就不用每个方法都得写兜底方法了。

全局兜底@DefaultProperties
service降级@FeignClient

下面解决业务逻辑混在一起的问题(解耦):我们改在service层进行服务降级

服务降级,客户端去调用服务端,碰上服务端宕机或关闭。

本次案例降级处理是在客户端80实现完成的,与服务端8001没有关系。只需要为Feign客户端定义的接口添加一个服务降级处理的实现类即可实现解耦。

在这种方式一般是在客户端,即消费者端,首先上面再controller中添加的 @HystrixCommand 和 @DefaultProperties 两个注解去掉。就是保持原来的controller

服务熔断案例

实际上服务熔断 和 服务降级 没有任何关系,就像 java 和 javaScript

服务熔断,有点自我恢复的味道

参考:https://blog.csdn.net/www1056481167/article/details/81157171

服务雪崩

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C有调用其他的微服务,这就是所谓的”扇出”,如扇出的链路上某个微服务的调用响应式过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统雪崩,所谓的”雪崩效应”

Hystrix

Hystrix是一个用于分布式系统的延迟和容错的开源库。在分布式系统里,许多依赖不可避免的调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整个服务失败,避免级联故障,以提高分布式系统的弹性。

断路器

“断路器”本身是一种开关装置,当某个服务单元发生故障监控(类似熔断保险丝),向调用方法返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方法无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延。乃至雪崩。

服务熔断

熔断机制是应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回”错误”的响应信息。

当检测到该节点微服务响应正常后恢复调用链路,在SpringCloud框架机制通过Hystrix实现,Hystrix会监控微服务见调用的状况,当失败的调用到一个阈值,默认是5秒内20次调用失败就会启动熔断机制,熔断机制的注解是@HystrixCommand

熔断的状态:

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

涉及到断路器的三个重要参数快照时间窗、请求总数阀值、错误百分比阀值

  • 1:快照时间窗:断路器确定是否打开需要统计一些请求和错误数据而统计的时间范围就是快照时间窗,默认为最近的10秒。
  • 2:请求总数阀值:在快照时间窗内,必须满足请求总数阀值才有资格熔断。默认为20,意味着在10秒内,如果该hystrix命令的调用次数不足20次,即使所有的请求都超时或具他原因失败,断路器都不会打开。
  • 3:错误百分比阀值:当请求总数在快照时间窗内超过了阀值,上日发生了30次调用,如果在这30次调用中,有15次发生了超时异常,也就是超过50%的错误百分比,在默认设定50%阀值情况,这时候就会将断路器打开。

The precise way that the circuit opening and closing occurs is as follows:

  1. Assuming the volume across a circuit meets a certain threshold (HystrixCommandProperties.circuitBreakerRequestVolumeThreshold())…
  2. And assuming that the error percentage exceeds the threshold error percentage (HystrixCommandProperties.circuitBreakerErrorThresholdPercentage())…
  3. Then the circuit-breaker transitions from CLOSED to OPEN.
  4. While it is open, it short-circuits all requests made against that circuit-breaker.
  5. After some amount of time (HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()), the next single request is let through (this is the HALF-OPEN state). If the request fails, the circuit-breaker returns to the OPEN state for the duration of the sleep window. If the request succeeds, the circuit-breaker transitions to CLOSED and the logic in 1. takes over again.经过一段时间后,如果有1个尝试成功了,就慢慢尝试恢复

参考:https://github.com/Netflix/Hystrix/wiki/How-it-Works#CircuitBreaker

实验效果为,多次出错调用fallback后,调用正常的也出错调用fallback。过了一会又自己恢复了。
展望:以后用的是Sentinel代替。

关于解耦以后的全局配置说明:

例如上面提到的全局服务降级,并且是feign+hystrix整合,即 service 实现类的方式,如何做全局配置?

上面有 做全局配置时,设置超时时间的方式,我们可以从中获得灵感,即在yml文件中 进行熔断配置:

1
2
3
4
5
6
7
8
9
hystrix:
command:
default:
circuitBreaker:
enabled: true
requestVolumeThreshold: 10
sleepWindowInMilliseconds: 10000
errorThresholdPercentage: 60
12345678

Hystrix DashBoard

除了隔离依赖服务的调用以外,Hystrix还提供了准实时的调用监控(HystrixDashboard),Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息,并以统计报表和图形的形式展示给用户,包括每秒执行多少请求多少成功,多少失败等。Netflix通过hystrix-metrics-event-stream实现了对以上指标的监控。SpringCloud也提供了HystrixDashboard的整合,对监控内容转化成可视化界面。

新建模块 consumer-hystrix-dashboard9001 :

pom 文件:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?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>cloud2022</artifactId>
<groupId>com.wzg.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>consumer-hystrix-dashboard9001</artifactId>

<dependencies>
<!-- hystrix Dashboard-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

<!-- 常规 jar 包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
</dependency>
<!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<dependency>
<groupId>com.wzg.springcloud</groupId>
<artifactId>api-commons</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>


<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

</project>

yml文件只需要配置端口号,主启动类加上这样注解:@EnableHystrixDashboard

启动测试:访问 http://ocalhost:9001/hystrix

监控实战

下面使用上面 9001 Hystrix Dashboard 项目,来监控 8001 项目 Hystrix 的实时情况:

注意8001被监控的时候pom里要actuator依赖,此外还要有

  • 主启动类上加@EnableCircuitBreaker,用于对豪猪熔断机制的支持
  • 然后就可以取页面里看熔断情况了。输入localhost:8001/hystrix.stream

1597558486510

服务网关

Gateway

内容过多,开发可参考 https://docs.spring.io/ 官网文档

简介

SpringCloud Gateway是SpringCloud的一个全新项目,基于Spring5.O+Springboot 2.0和ProjectReactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API路由管理方式。

SpringCloudGateway作为SpringCloud生态系统中的网关,目标是替代Zuul,在SpringCloud2.0以上版本中,没有对新版本的zuul2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而webFlux框架底层则使用了高性能的Reactor模式通信框架Netty

Gateway是什么

Gateway特性:

  • 基于SpringFramework5,ProjectReactor和SpringBoot 2.0进行构建;
  • 动态路由:能够匹任何请求属性;
  • 可以对路由指定Predicate(断言)和Filter(过滤器)·
  • 集成Hystrix的断路器功能;
  • 集成SpringCloud服务发现功能;
  • 易于编写的Predicate(断言)和Filter(过滤器)·
  • 请求限流功能;
  • 支持路径重写。

GateWay的三大核心概念:

  • Route(路由):路由是构建网关的基本模块,它由ID、目标URI、一系列的断言和过滤器组成,如果断言为true则匹配该路由
  • Predicate(断言):参考的是Java8的java.util.function.predicate。开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
    • 匹配条件
  • Filter(过滤):指的是spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
  • 例子:通过断言虽然进来了,但老师要罚站10min(过滤器操作),然后才能正常坐下听课

web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。
而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了

Spring Cloud Gateway Diagram

客户端向Spring Cloud Gateway发出请求。然后在Gateway HandlerMapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler

Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。

过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(post)执行业务逻辑。

Filter在pre类型的过滤器可以做参数校验,权限校验,流量监听,日志输出,协议转换等,

在post类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

入门配置

新建模块 gateway-gateway9527

现在实现,通过Gateway (网关) 来访问其它项目,这里选择之前8001项目,要求注册进Eureka Server 。其它没要求。

pom文件:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?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>cloud2022</artifactId>
<groupId>com.wzg.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>gateway-gateway9527</artifactId>

<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--eureka-client gateWay网关作为一种微服务,也要注册进服务中心。哪个注册中心都可以,如zk-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- gateway和spring web+actuator不能同时存在,即web相关jar包不能导入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
</dependency>
<dependency><!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
<groupId>com.wzg.springcloud</groupId>
<artifactId>api-commons</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>


<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>

</project>

yml文件:

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
32
33
34
35
36
server:
port: 9527
spring:
application:
name: gateway9527
## GateWay配置
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes: #多个路由
- id: payment_routh # 路由ID , 没有固定的规则但要求唯一,建议配合服务名
uri: lb://PROVIDER-SERVICE # lb 属于GateWay 的关键字,代表是动态uri,即代表使用的是服务注册中心的微服务名,它默认开启使用负载均衡机制
# uri: http://localhost:8001 # 匹配后提供服务的路由地址 #uri+predicates # 要访问这个路径得先经过9527处理
predicates:
- Path=/payment/** # 断言,路径相匹配的进行路由
#- After=2022-02-21T15:51:37.485+08:00[Asia/Shanghai]
#- Cookie=username,wzg
#- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式

- id: payment_routh2 # 路由ID , 没有固定的规则但要求唯一,建议配合服务名
uri: lb://PROVIDER-SERVICE
# uri: http://localhost:8001 # 匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由

# 注册进 eureka Server # 网关他本身也是一个微服务,也要注册进注册主中心
eureka:
instance:
hostname: gateway-service
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
register-with-eureka: true
fetch-registry: true

主启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.wzg.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
* @author whlie(true){learn}
*/
@SpringBootApplication
@EnableEurekaClient
public class GatewayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GatewayMain9527.class,args);
}
}

8001看看controller的访问地址,我们目前不想暴露8001端口,希望在8001外面套一层9527。这样别人就攻击不了8001,有网关挡着

访问测试:

  • 1 启动eureka Server,
  • 2 启动 8001 项目,
  • 3 启动 9527(Gateway项目)

可见,当我们访问 http://localhost:9527/payment/get/1 时,即访问网关地址时,会给我们转发到 8001 项目的请求地址,以此作出响应。

加入网关前:http://localhost:8001/payment/get/1

加入网关后:http://localhost:9527/payment/get/1

上面是以 yml 文件配置的路由,也有使用config类配置的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.wzg.springcloud.config;

import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author whlie(true){learn}
*/
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();

// 分别是id,本地址,转发到的地址
routes.route("path_route_wzg",
r -> r.path("/guonei").uri("http://news.baidu.com/guonei")
).build();//JDK8新特性

return routes.build();
}
}

动态配置

这里所谓的动态配置就是利用服务注册中心,来实现 负载均衡 的调用 多个微服务。

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

注意,这是GateWay 的负载均衡

对yml进行配置:让其先通过gateway,再通过gateway去注册中心找提供者

Gateway:Predicate

注意到上面yml配置中,有个predicates 属性值。

  • 1 After Route Predicate
  • 2 Before Route Predicate
  • 3 Between Route Predicate
  • 4 Cookie Route Predicate
  • 5 Header Route Predicate
  • 6 Host Route Predicate
  • 7 Method Route Predicate
  • 8 Path Route Predicate
  • 9 Query Route Predicate

具体使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由

- id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,路径相匹配的进行路由
#- After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai]
#- Cookie=username,zzyy
#- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式

predicates下面可以有多个属性,表示多个属性与操作为true这个路由才生效

predicates属性下的After属性

1
2
3
predicates:
- Path=/payment/lb/**
- After=2020-02-21T15:51:37.485+08:00[Asia/Shanghai] # 会在这个时间之后这个路由才生效

predicates属性下的Cookie属性

1
2
3
predicates:
- Cookie=username,zzyy,ch.p # 需要有这个Cookie值才生效 最后一个是正则表达式
# curl 地址 --cookie "a=b"

predicates属性下的Header属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+
12345678
spring:
cloud:
gateway:
routes:
- id: host_route
uri: https://example.org
predicates:
- Host=**.somehost.org,**.anotherhost.org # Host Route Predicate Factory接收一组参数,一组匹配的域名列表,这个模板是一个ant分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则


# 放爬虫思路,前后端分离的话,只限定前端项目主机访问,这样可以屏蔽大量爬虫。

例如我加上: - Host=localhost:** ** 代表允许任何端口

就只能是主机来访

更多属性可以参考:https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#configuring-route-predicate-factories-and-gateway-filter-factories

需要注意的是value部分写的是正则表达式

高并发工具:jmeter、postman、curl

配置错误页面:

注意,springboot默认/static/error/ 下错误代码命名的页面为错误页面,即 404.html

而且不需要导入额外的包,Gateway 里面都有。

Gateway:Filter

生命周期:

  • pre:
  • post:

种类:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue # 添加了个请求头X-Request-red

主要是配置全局自定义过滤器,其它的小配置具体看官网吧

2个主要接口:implements GlobalFilter,Ordered

场景:

  • 全局日志记录
  • 统一网关鉴权
  • 。。。

自定义全局过滤器配置类:

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
32
33
34
35
36
37
38
39
40
41
42
43
package com.wzg.springcloud.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Date;

/**
* @author whlie(true){learn}
*/
@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,请求不被接受");
//设置 response 状态码 因为在请求之前过滤的,so就算是返回NOT_FOUND 也不会返回错误页面
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
//完成请求调用
return exchange.getResponse().setComplete();
}
//过滤链放行
return chain.filter(exchange);
}

// 返回值是加载顺序,一般全局的都是第一位加载
@Override
public int getOrder() {
return 0;
}
}

总结:

在实际使用中,大部分是自己配置过滤器


SpringCloud学习笔记(4)
https://yztldxdz.top/2022/09/20/SpringCloud学习笔记(4)/
发布于
2022年9月20日
许可协议