抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Gateway高级功能

实现熔断降级

为什么要实现熔断降级?

​ 在分布式系统中,网关作为流量的入口,因此会有大量的请求进入网关,向其他服务发起调用,其他服务不可避免的会出现调用失败(超时、异常),失败时不能让请求堆积在网关上,需要快速失败并返回给客户端,想要实现这个要求,就必须在网关上做熔断、降级操作。

为什么在网关上请求失败需要快速返回给客户端?

​ 因为当一个客户端请求发生故障的时候,这个请求会一直堆积在网关上,当然只有一个这种请求,网关肯定没有问题(如果一个请求就能造成整个系统瘫痪,那这个系统可以下架了),但是网关上堆积多了就会给网关乃至整个服务都造成巨大的压力,甚至整个服务宕掉。因此要对一些服务和页面进行有策略的降级,以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的得到正确的相应,所以需要网关上请求失败需要快速返回给客户端。

引入POM依赖

Spring Cloud Gateway也可以利用Hystrix的熔断特性,在流量过大时进行服务降级,同时项目中必须加上Hystrix的依赖。

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

配置文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
spring:
cloud:
gateway:
routes:
- id: order-server
uri: http://localhost:8083/
predicates:
- Path=/order/**
filters:
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/fallback


hystrix:
command:
fallbackcmd:
execution:
isolation:
thread:
#超时时间,若不设置超时时间则有可能无法触发熔断
timeoutInMilliseconds: 5000 

过滤器Hystrix,作用是通过Hystrix进行熔断降级,当上游的请求,进入了Hystrix熔断降级机制时,就会调用fallbackUri配置的降级地址。需要注意的是,还需要单独设置Hystrix的commandKey的超时时间

降级Controller

上述配置中给出了熔断之后返回路径,因此,在Gateway服务模块添加/fallback路径,以作为服务熔断时的返回路径。

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class GatewayController {

@GetMapping("fallback")
public Map fallback() {
Map<String, String> response = new HashMap<>();
response.put("code", "500");
response.put("message", "服务暂时不可用");
return response;
}
}

测试

将被调用的服务进行休眠5S用来测试熔断,然后调用接口进行测试

使用 curl 测试,命令行输入:

1
curl http://localhost:9066/order/createorder/1

测试发现发生了熔断,调用了熔断的controller

重试路由器

通过简单的配置,Spring Cloud Gateway就可以支持请求重试功能,但是被调用服务需要做好幂等性处理,重试需要慎用。

配置文件配置

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
routes:
- id: order-server
uri: http://localhost:8083/
predicates:
- Path=/order/**
filters:
- name: Retry
args:
retries: 3
status: 500

Retry GatewayFilter通过四个参数来控制重试机制,参数说明如下:

  • retries:重试次数,默认值是 3 次。
  • statuses:HTTP 的状态返回码,取值请参考:org.springframework.http.HttpStatus。
  • methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值参考:org.springframework.http.HttpMethod。
  • series:一些列的状态码配置,取值参考:org.springframework.http.HttpStatus.Series。符合的某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有5个值。

测试

被调用方抛出异常
1
2
3
......
System.out.println("创建订单,userId:" + userId);
int k = 1 / 0;
调用接口
1
curl http://localhost:9066/order/createorder/1

查看被调用方日志

查看打印结果,我们发现总共调用了4次,第一次失败后 又重试了三次

1
2
3
4
5
6
7
8
9
10
11
创建订单,userId:1
2020-06-01 17:07:07.392 ERROR 3692 .....

创建订单,userId:1
2020-06-01 17:07:07.409 ERROR 3692 .....

创建订单,userId:1
2020-06-01 17:07:07.420 ERROR 3692 .....
创建订单,userId:1
2020-06-01 17:07:07.429 ERROR 3692 .....

使用上述配置进行测试,当后台服务不可用时,会在控制台看到请求四次的日志,证明此配置有效。

分布式限流

在高并发的系统中,往往需要在系统中做限流,一方面是为了防止大量的请求使服务器过载,导致服务不可用,另一方面是为了防止网络攻击。

​ 常见的限流方式,比如Hystrix适用线程池隔离,超过线程池的负载,走熔断的逻辑。在一般应用服务器中,比如tomcat容器也是通过限制它的线程数来控制并发的;也有通过时间窗口的平均速度来控制流量。常见的限流纬度有比如通过Ip来限流、通过uri来限流、通过用户访问频次来限流。

​ 一般限流都是在网关这一层做,比如Nginx、Openresty、kong、zuul、Spring Cloud Gateway等;也可以在应用层通过Aop这种方式去做限流。

常见的限流算法

计数器算法

​ 计数器算法采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”

漏桶算法

​ 漏桶算法为了消除”突刺现象”,可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。

​ 在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。

​ 这种算法,在使用过后也存在弊端:无法应对短时间的突发流量。

令牌桶算法

​ 从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。

Spring Cloud Gateway限流

在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现上述三种过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用在Redis内的通过执行Lua脚本实现了令牌桶的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:

引入POM依赖

Spring Cloud Gateway本身集成了限流操作,Gateway限流需要使用Redis,pom文件中添加Redis依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
配置文件配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
redis:
host: 192.168.64.128
port: 6379
database: 0
cloud:
gateway:
routes:
- id: order-server
uri: http://localhost:8083/
predicates:
- Path=/order/**
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@hostKeyResolver}"
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3

在上面的配置问价中,配置了Redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:

  • BurstCapacity:令牌桶的总容量。
  • replenishRate:令牌通每秒填充平均速率。
  • Key-resolver:用于限流的解析器的Bean对象的名字。它使用SpEL表达式#{@beanName}从Spring容器中获取bean对象。

注意:filter下的name必须是RequestRateLimiter。

key-resolver实现

Key-resolver参数后面的bean需要自己实现,然后注入到Spring容器中。

用户ID限流

这里根据用户ID限流,请求路径中必须携带userId参数

1
2
3
4
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}

KeyResolver需要实现resolve方法,比如根据userid进行限流,则需要用userid去判断。实现完KeyResolver之后,需要将这个类的Bean注册到Ioc容器中。

根据IP限流

如果需要根据IP限流,定义的获取限流Key的bean为:

1
2
3
4
@Bean
public KeyResolver hostKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
根据Path限流

还可以根据请求路径进行限流

1
2
3
4
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}

配置HTTPS

在Web服务应用中,为了数据的传输安全,使用安全证书,使用TLS/SSL加密

TLS/ SSL简介

  • TLS:安全传输层协议(TLS)用于在两个通信应用程序之间提供保密性和数据完整性
  • SSL:SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。
    TLS与SSL在传输层与应用层之间对网络连接进行加密。

生成证书

使用java自带的 keytool

1
keytool -genkeypair -alias client -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore keystore.p12 -validity 3650

配置证书

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
server:
port: 443
ssl:
enabled: true
key-alias: client
key-store: classpath:ssl/keystore.p12
key-store-password: 123456
key-store-type: PKCS12


spring:
application:
name: gateway-server
cloud:
gateway:
httpclient:
ssl:
handshake-timeout: PT10000S
close-notify-read-timeout: PT0S
close-notify-flush-timeout: PT3000S
routes:
- id: order-server
uri: lb://order-server
predicates:
- Path=/order/**
证书文件配置

网关可以通过常规的 Spring server configuration 来侦听HTTPS上的请求

HTTPS默认端口 443

1
2
3
4
5
6
7
8
server:
port: 443
ssl:
enabled: true
key-alias: client
key-store: classpath:ssl/keystore.p12
key-store-password: 123456
key-store-type: PKCS12
下游HTTPS配置

网关路由可以路由到HTTP和HTTPS后端。如果路由到HTTPS后端,则可以将网关配置为信任所有具有证书的下游服务

1
2
3
4
5
6
spring:
cloud:
gateway:
httpclient:
ssl:
useInsecureTrustManager: true
pem证书配置

不建议在生产环境使用不安全的信任管理器。对于生产部署,可以使用一组已知证书配置网关,这些证书可以通过以下方式进行配置:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
httpclient:
ssl:
trustedX509Certificates:
- cert1.pem
- cert2.pem

如果Spring Cloud Gateway未配置受信任证书,则使用默认信任库(可以使用系统属性javax.net.ssl.trustStore覆盖)。

TLS 握手

网关维护一个用于路由到后端的client池。当通过HTTPS通信时,客户端启动一个TLS握手,其中可能会有很多超时。这些超时可以这样配置

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
httpclient:
ssl:
handshake-timeout-millis: 10000
close-notify-flush-timeout-millis: 3000
close-notify-read-timeout-millis: 0

测试

访问https://localhost/order/createorder/1 因为 HTTPS默认端口443 所以可以省略

可以支持https访问

http自动转发https端口

​ Springboot默认采用Tomcat作为内嵌容器,通过设置可以轻松实现同时监听Http和https两个端口,http自动转发给https端口。然而,SpringCloud Gateway由于默认使用netty作为内嵌web容器,并且官方手册内,只有说明如何设置Https,却没有说明如何设置同时监听多个端口,并且http自动转发到https端口。
​ 虽然作为普通微服务,这个功能并不是很重要,但是作为网关,http自动转发给https端口,是一个比较实用的功能。在网上寻觅了一波,还是找到了解决方案,但并不是官方的,但是可以实现功能。后面持续关注,官方可能会给出解决方案。

整体思路

实现思路

​ SpringCloud Gateway内部启动两个netty,一个监听http的9066端口,一个监听https的443端口,其中9066端口收到的请求,都设置返回状态码为301(重定向),重定向至443端口,443端口是gateway的默认端口,收到请求后转发给后台业务微服务。

自动重定向配置类
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
@Configuration
public class HttpToHttpsRedirectConfig {

@Value("${server.port}")
private Integer serverSSLPort;
@Value("${server.http.port}")
private Integer serverHttpPort;


@PostConstruct
public void startRedirectServer() {
NettyReactiveWebServerFactory httpNettyReactiveWebServerFactory = new NettyReactiveWebServerFactory(serverHttpPort);
httpNettyReactiveWebServerFactory.getWebServer((request, response) -> {
URI uri = request.getURI();
URI httpsUri;
try {
httpsUri = new URI("https", uri.getUserInfo(), uri.getHost(), serverSSLPort, uri.getPath(), uri.getQuery(), uri.getFragment());
} catch (URISyntaxException e) {
return Mono.error(e);
}
response.setStatusCode(HttpStatus.MOVED_PERMANENTLY);
response.getHeaders().setLocation(httpsUri);
return response.setComplete();
}).start();
}
}
配置文件
1
2
3
4
5
6
7
8
9
10
server:
port: 443
http:
port: 9066
ssl:
enabled: true
key-alias: client
key-store: classpath:ssl/keystore.p12
key-store-password: 123456
key-store-type: PKCS12
测试

访问http://localhost:9066/order/createorder/1 会自动重定向到https的443端口

常见错误

  1. org.springframework.core.io.buffer.DefaultDataBufferFactory cannot be cast to org.springframework.core.io.buffer.NettyDataBufferFactory

    ​ 解决方法: 由于springcloud的gateway使用的是webflux,默认使用netty,所以从依赖中排除 tomcat相关的依赖

  2. Caused by: javax.net.ssl.SSLException: Received fatal alert: certificate_unknown

    ​ 原因:javax.net.ssl.sslexception:收到致命警报:证书未知

    获取证书:
    我是买了腾讯云的域名,目前有活动,新用户1块钱就可以买一年的域名,老用户2块钱一年。购买域名之后进行实名认证一下就可以申请证书了。
    点击链接https://console.cloud.tencent.com/ssl进入控制台,证书管理

  3. Caused by: io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record:

    ​ 产生原因:在配置了认证用户名以及证书之后,未认证用户名及证书方式请求数据,所以导致了该问题的发生。

跨域配置

​ 大家都知道spring boot 可以通过@CrossOrigin实现跨域。但是在spring cloud 里,如果要粒度那么细的去控制跨域,这个就太繁琐了,所以一般来说,会在路由zuul里实现。

未设置跨域测试

跨域测试页面
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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<link type="test/css" href="css/style.css" rel="stylesheet">
<script type="text/javascript" src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
<script type="text/javascript">
$(function(){
$("#cors").click(
function(){
$.ajax({
//headers:{"token":"","Content-Type":"application/json;charset=UTF-8","Access-Control-Allow-Origin":"*"},
url:"http://127.0.0.1:9066/order/createorder/1",
success:function(data){
console.log("start")
console.log(data)
alert(data);
}
})
});
});
</script>
<body>
<input type="button" id="cors" value="core跨域测试"
</body>
</html>
测试

corsFilter方式

在gateway服务下添加一个CorsConfiguration实现跨域,实现起来方便。代码如下

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
/**
* 跨域允许
*/
@Configuration
public class CorsConfiguration {
private static final String MAX_AGE = "18000L";

@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (CorsUtils.isCorsRequest(request)) {
HttpHeaders requestHeaders = request.getHeaders();
ServerHttpResponse response = ctx.getResponse();
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Origin, No-Cache, X-Requested-With, If-Modified-Since, Pragma, Last-Modified, Cache-Control, Expires, Content-Type, X-E4M-With,userId,token");
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}

return chain.filter(ctx);
};
}
}
测试

跨域测试成功

评论