Skip to content

Commit bebfe9f

Browse files
author
yangjingjing
committed
init blog
1 parent aeb355f commit bebfe9f

28 files changed

+2090
-60
lines changed

_posts/2020-09-11-Cloud网关Gateway源码.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ public Mono<Void> filter(ServerWebExchange exchange) {
212212
```
213213
FilteringWebHandler#handle方法首先获取请求对应的路由的过滤器和全局过滤器,将两部分组合;然后对过滤器列表排序,AnnotationAwareOrderComparatorOrderComparator的子类,支持SpringOrdered接口的优先级排序;最后按照优先级,生成过滤器链,对该请求进行过滤处理。这里过滤器链是通过内部静态类DefaultGatewayFilterChain实现,该类实现了GatewayFilterChain接口,用于按优先级过滤。
214214

215-
## 路由定义定位器
215+
216216

217217

218218

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
layout: post
3+
categories: [Gateway]
4+
description: none
5+
keywords: Gateway
6+
---
7+
# Cloud网关Gateway路径切割
8+
9+
10+
## Spring Cloud Gateway 路径重写正则表达式的理解 (RewritePath GatewayFilter Factory)
11+
12+
首先
13+
官网对于 RewritePath GatewayFilter Factory 的解释是这样的:
14+
```
15+
spring:
16+
cloud:
17+
gateway:
18+
routes:
19+
- id: rewritepath_route
20+
uri: https://example.org
21+
predicates:
22+
- Path=/red/**
23+
filters:
24+
- RewritePath=/red(?<segment>/?.*), $\{segment}
25+
26+
```
27+
对于请求路径 /red/blue,当前的配置在请求到到达前会被重写为 /blue,由于YAML的语法问题,$符号后面应该加上\
28+
29+
然后
30+
在解释正则表达式前,我们需要学习一下java正则表达式分组的两个概念:
31+
32+
命名分组:(?<name>capturing text)
33+
将匹配的子字符串捕获到一个组名称中,后面可通过分组名获得匹配结果。例如这里的示例,就是将 capturing text 捕获到名称为 name 的组中
34+
35+
引用捕获文本:${name}
36+
将名称为name的命名分组对应的内容替换到此处
37+
38+
那么就很好解释官网的这个例子了,
39+
对于配置文件中的: - RewritePath=/red(?<segment>/?.*), $\{segment}详解:
40+
41+
(?<segment>/?.*):
42+
?<segment>
43+
名称为 segment 的组
44+
/?
45+
字符/出现0次或1次
46+
.*
47+
任意字符出现0次或多次
48+
合起来就是:将 /?.*匹配到的结果捕获到名称为segment的组中
49+
50+
$\{segment}:
51+
将名称为 segment 的分组捕获到的文本置换到此处。
52+
注意,\的出现是由于避免 yaml 语法认为这是一个变量(因为在 yaml 中变量的表示法为 ${variable},而这里我们想表达的是字面含义),在 gateway 进行解析时,会被替换为 ${segment}
53+
54+
最后
55+
业务举例:
56+
https://spring.io/projects/** 这个路径重写为 https://spring.io/regexp/**
57+
```
58+
spring:
59+
cloud:
60+
gateway:
61+
routes:
62+
- id: rewritepath_route
63+
uri: https://spring.io
64+
predicates:
65+
- Path=/projects/**
66+
filters:
67+
- RewritePath=/projects(?<segment>/?.*), /regexp$\{segment}
68+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
layout: post
3+
categories: [Gateway]
4+
description: none
5+
keywords: Gateway
6+
---
7+
# Cloud网关Gateway路由定义定位器
8+
9+
## 简介
10+
RouteDefinitionLocator 是路由定义定位器的顶级接口,它的主要作用就是读取路由的配置信息(org.springframework.cloud.gateway.route.RouteDefinition)。
11+
12+
它有五种不同的实现类
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
---
2+
layout: post
3+
categories: [Gateway]
4+
description: none
5+
keywords: Gateway
6+
---
7+
# 网关Gateway源码10限流
8+
9+
## 概述
10+
通过 RequestRateLimiterGatewayFilterFactory ,可以创建一个GatewayFilter的匿名内部类实例,它的内部使用Redis+Lua实现限流。限流规则由KeyResolver接口的具体实现类来决定,比如通过IP、url等来进行限流。由于用到redis,所以在项目工程里需要配置redis依赖:
11+
```
12+
<dependency>
13+
<groupId>org.springframework.boot</groupId>
14+
<artifactId>spring-boot-starter-data-redis</artifactId>
15+
</dependency>
16+
```
17+
yml配置示例:
18+
```
19+
spring:
20+
cloud:
21+
gateway:
22+
routes:
23+
- id: user-service
24+
uri: http://127.0.0.1:8081
25+
order: 10000
26+
predicates:
27+
- Path=/user/**
28+
filters:
29+
# 令牌桶容量=100个,补充令牌速率=20个/s,限流key解析器Bean对象的名字
30+
# 根据#{@beanName},从BeanFactory中获取Bean,\为转义符,避免被解析成注释...
31+
- RequestRateLimiter=100, 20, \#{@principalNameKeyResolver}
32+
```
33+
最后别忘了在yml里配redis... 这里节约篇幅就不加了哈。
34+
35+
## RequestRateLimiterGatewayFilterFactory核心源码
36+
```
37+
public GatewayFilter apply(Config config) {
38+
// snipped... 意思是有些跟主要逻辑无关的代码略过了
39+
// 这里的resolver就是KeyResolver的具体实现,用于解析限流key
40+
// 默认为PrincipalNameKeyResolver,resolve()方法内容为:
41+
// return exchange.getPrincipal().flatMap(p -> Mono.justOrEmpty(p.getName()));
42+
// 如果用过Shiro做鉴权,应该是比较熟悉principal()这个词的,其实就是拿到当前登录用户
43+
// 所以这里是取用户名作为限流key
44+
return (exchange, chain) -> resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY)
45+
.flatMap(key -> {
46+
// snipped...
47+
// limiter这里默认只有RedisRateLimiter的实现
48+
return limiter.isAllowed(routeId, key).flatMap(response -> {
49+
// snipped...
50+
// 如果允许访问,再往下走过滤器链
51+
if (response.isAllowed()) {
52+
return chain.filter(exchange);
53+
}
54+
// 被限流了,不允许访问,直接返回
55+
setResponseStatus(exchange, config.getStatusCode());
56+
return exchange.getResponse().setComplete();
57+
});
58+
});
59+
}
60+
```
61+
62+
## RedisRateLimiter核心源码
63+
```
64+
public Mono<Response> isAllowed(String routeId, String id) {
65+
// 1.加载Route的配置
66+
Config routeConfig = loadConfiguration(routeId);
67+
// 令牌补充速度
68+
// 官方注释此处直译为:一秒钟允许通过的请求数,为何?
69+
// 我每小时只充一格电,那这小时只能用一格电,尽管有时手机是满电
70+
// 我疯狂玩原神,掉电飞快,没过多久就搞没了,电量归零
71+
// 那我还是变成了从0开始,充一格用一格的状态
72+
// 由此得出一个结论,只要控制了产出就控制了消耗
73+
// 虽然听着很废话的感觉,但这样很好理解
74+
// 如果单纯的用补充速度这个词,不加解释,可能无法马上想到它造成的结果
75+
// 我觉得这点官方的注释还是非常好的,只是第一次读有点绕不过来,还以为是不是写错了地方呢
76+
int replenishRate = routeConfig.getReplenishRate();
77+
// 令牌桶容量
78+
int burstCapacity = routeConfig.getBurstCapacity();
79+
// 每个请求申请多少令牌,或者说消耗/取出多少令牌,默认是1个
80+
int requestedTokens = routeConfig.getRequestedTokens();
81+
82+
try {
83+
// 构造tokenKey和时间戳key,都是通过前缀+用户ID拼出来的,用于到redis内去取令牌
84+
List<String> keys = getKeys(id);
85+
// 2.封装Lua脚本参数列表
86+
// Instant.now().getEpochSecond()获得的是从1970-01-01 00:00:00开始的秒数
87+
List<String> scriptArgs = Arrays.asList(replenishRate + "",
88+
burstCapacity + "", Instant.now().getEpochSecond() + "",
89+
requestedTokens + "");
90+
// 3.allowed, tokens_left = redis.eval(SCRIPT, keys, args)
91+
// 执行结果里有两个返回值:是否获取令牌成功(1-成功,0-失败), 剩余令牌数
92+
Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys,
93+
scriptArgs);
94+
95+
// 4.返回执行结果
96+
return flux.onErrorResume(throwable -> {
97+
// Redis执行Lua脚本发生异常时,忽略异常,直接返回[成功,剩余令牌数:-1]
98+
// 避免Redis故障导致无法申请到令牌,所有请求直接挂了
99+
return Flux.just(Arrays.asList(1L, -1L));
100+
// 将返回的Flux<List<Long>>类型转换成 Mono<List<Long>>类型
101+
// 顺带回顾下Flux和Mono的基础知识:
102+
// 1.在开发过程中,不再返回简单的POJO对象,而必须返回其他内容,在结果可用的时候返回。
103+
// 2.在响应式流的规范中,被称为发布者(Publisher)。发布者有一个subscribe()方法,该方法允许使用者在POJO可用时获取它。
104+
// 3.发布者可以通过以下两种形式返回结果:
105+
// - Flux返回0个或多个结果,可能是无限个
106+
// - Mono返回0个或1个结果
107+
// Redis执行lua脚本只会返回一次List<Long>,失败时填充默认值也是一次,所以转成Mono
108+
}).reduce(new ArrayList<Long>(), (longs, l) -> {
109+
longs.addAll(l);
110+
return longs;
111+
}).map(results -> {
112+
// 取出Lua脚本的运行结果:是否获取令牌成功(1-成功,0-失败), 剩余令牌数
113+
// 5.塞到response的headers里返回,这里result里一共塞了四个参数:
114+
// 剩余令牌数、每秒补充的令牌数、令牌桶容量、每个请求申请的令牌数
115+
boolean allowed = results.get(0) == 1L;
116+
Long tokensLeft = results.get(1);
117+
Response response = new Response(allowed,
118+
getHeaders(routeConfig, tokensLeft));
119+
return response;
120+
});
121+
}
122+
catch (Exception e) {
123+
// 虽然redis不是天天抽风,但是万一真发生了这种事,还是留个日志告警
124+
log.error("Error determining if user allowed from redis", e);
125+
}
126+
// 6.最后的兜底尿布,碰到任何奇葩情况导致上面的执行失败了,都给个默认返回:申请令牌成功,剩余令牌数-1
127+
return Mono.just(new Response(true, getHeaders(routeConfig, -1L)));
128+
}
129+
```
130+
其实代码写的挺复杂的,参数还很多。我们碰到这种代码不要慌,就先当它里面的一堆字段和小方法不存在。因为字段很多都是定义的final字段,比如一个header的名字或者跟主体逻辑并不是有很强关联的传值字段。小方法有挺多都是组装字段到某个类或者做一些处理,我们可以先关注方法名来了解下它的作用。然后只看关键方法,一些什么异常处理、打日志、读取值的都可以先忽略。重点看它的核心逻辑是在干嘛。
131+
132+
比如这篇里,实际代码我已经省略很多了,但它总体无外乎干了这些事:
133+
134+
准备一个限流容器:一个令牌桶。你可以把它放redis,也可以自定义放到mysql、内存或者其它地方,这是可以自己实现的;
135+
准备一个限流key解析器:默认就是拿到request里的用户名作为限流key;
136+
准备一个limiter来实现限流逻辑:这里是redis实现,所以是根据限流规则的配置,从redis里存的令牌桶中拿令牌。
137+
然后没了,是不是就是这么简单?
138+
139+
学习源码,重点是不要先把自己吓死了,删繁就简的看核心逻辑,看它大概是在干啥,然后再选择性的深入细节,比如各种分支情况的处理,异常情况的处理...
140+
141+
142+
143+
144+
145+
146+
147+
148+
149+
150+
151+
152+
153+
154+
155+
156+
157+
158+
159+
160+
161+
162+
163+

0 commit comments

Comments
 (0)