使用spring-cloud-zuul-rate-limit在zuul中为服务限流

前言

几个月前做过一个springCloud服务限流的任务,当时选定的技术是spring-cloud-zuul-rate-limit。
之所以选这个而不是别的,是因为项目本身使用了zuul,而且业务需求上spring-cloud-zuul-rate-limit的功能刚好都可以满足,在这种情况下就可以在符合要求的同时尽可能快的完成。

这个任务实际上只相当于一个技术调研,只要在现有技术框架的基础上能够集成和实现需求,并且成功测试就可以了。
和当时在公司略有不同的是,公司是直接使用内部maven仓库,springcloud依赖的版本号基本都是原始的。
而在我自己电脑上重新写的时候,是使用的aliyun的maven仓库,springcloud版本经过了一定的加工处理。
两个电脑不互通,也不能互传数据,所以在自己电脑上就要手动重来一遍。
于是,因为这个版本问题,再加上自己的不小心,在自己电脑上一开始无法触发限流。
我一度以为就是版本不同导致的,结果绞尽脑汁后才发现是因为自己敲错了一个符号。

言归正传,这里的集成分为了三个简单的服务,一个模拟业务服务的server,一个注册中心eureka,还有一个springcloud zuul。以下是代码示例:

spring cloud eureka server注册中心

pom文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
......
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

启动类

1
2
3
4
5
6
7
@EnableEurekaServer
@SpringBootApplication
public class CloudEurekaApplication {
public static void main(String[] args) {
SpringApplication.run(CloudEurekaApplication.class, args);
}
}

配置文件

1
2
3
4
5
spring.application.name=eureka-server
server.port=1000
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.serviceUrl.defaultZone=http://127.0.0.1:1000/eureka/

可以看到,这里其实就是一个极简的eureka的微服务注册中心,所以其实没有太多需要额外解释的内容。

srping cloud eureka client业务服务

pom文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
......
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

启动类

1
2
3
4
5
6
7
@EnableDiscoveryClient
@SpringBootApplication
public class CloudServer1Application {
public static void main(String[] args) {
SpringApplication.run(CloudServer1Application.class, args);
}
}

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
public class TestController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/hello1")
public String hello1(){
return "hello1";
}
@GetMapping("/hello2")
public String hello2(){
return "hello2";
}
}

配置文件

1
2
3
4
server.port=1001
spring.application.name=hello-service
#name
eureka.client.service-url.defaultZone=http://127.0.0.1:1000/eureka

可以看出,上边也就是一个极简的springcloud eureka的客户端服务,加上了一些非常简单的api接口。

springcloud zuul

springcloud zuul ratelimit实际支持很多种数据存储方式,比如redis、jpa、jcache等,由于项目中没有使用redis,也没有使用jpa,然后当时的电脑又不能随便安装软件,所以就只能选用jcache存储,结合bucket4j一起使用。

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
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
......
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>com.netflix.servo</groupId>
<artifactId>servo-core</artifactId>
<version>0.9.4-rc.1</version>
</dependency>
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-core</artifactId>
<version>6.2.0</version>
</dependency>
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-jcache</artifactId>
<version>6.2.0</version>
</dependency>
<dependency>
<groupId>javax.cache</groupId>
<artifactId>cache-api</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>com.github.vladimir-bukhtoyarov</groupId>
<artifactId>bucket4j-hazelcast</artifactId>
<version>6.2.0</version>
</dependency>
<dependency>
<groupId>com.hazelcast</groupId>
<artifactId>hazelcast</artifactId>
<version>4.2</version>
</dependency>

启动类

1
2
3
4
5
6
7
@EnableZuulProxy
@SpringCloudApplication
public class CloudZuulApplication {
public static void main(String[] args) {
SpringApplication.run(CloudZuulApplication.class, args);
}
}

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server.port=5001
spring.application.name=zuul-service
zuul.routes.hello-service.path=/hello-service/**
zuul.routes.hello-service.service-id=hello-service
eureka.client.service-url.defaultZone=http://127.0.0.1:1000/eureka/

zuul.ratelimit.key-prefix=springcloud-book
zuul.ratelimit.enabled=true
zuul.ratelimit.repository=BUCKET4J_JCACHE

zuul.ratelimit.policy-list.hello-service[0].limit=2
zuul.ratelimit.policy-list.hello-service[0].quota=1
zuul.ratelimit.policy-list.hello-service[0].refresh-interval=20
zuul.ratelimit.policy-list.hello-service[0].type[0]=url

这里配置含义如下:
zuul.ratelimit.key-prefix=springcloud-book;缓存存储的自定义键前缀
zuul.ratelimit.enabled=true;启用速率限制,即启用ratelimit限流
zuul.ratelimit.repository=BUCKET4J_JCACHE;指定数据存储方式

下面的就是具体服务的限流规则配置,hello-service是服务id,尽量与eureka-client的服务名保持一致。
zuul.ratelimit.policy-list.hello-service[0].limit=2;每个刷新时间窗口对应的请求数限制;
zuul.ratelimit.policy-list.hello-service[0].quota=1;每单位时间允许访问的总时间(这个解释来自网上,需要进一步验证。目前多次测试的结果好像和网上解释的含义不太一样)
zuul.ratelimit.policy-list.hello-service[0].refresh-interval=20;刷新时间窗口的时间,单位是秒,默认60秒;
zuul.ratelimit.policy-list.hello-service[0].type[0]=url;支持的限流类型,如ORIGIN、USER、URL、URL_PATTERN等。

cache配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class HazelcastConfiguration {
@Bean("RateLimit")
Cache<String, GridBucketState> cache(){
Config config=new Config();
config.setLiteMember(false);
CacheSimpleConfig cacheSimpleConfig=new CacheSimpleConfig();
cacheSimpleConfig.setName("springcloud-book");
config.addCacheConfig(cacheSimpleConfig);
HazelcastInstance hazelcastInstance= Hazelcast.newHazelcastInstance(config);
ICacheManager cacheManager=hazelcastInstance.getCacheManager();
Cache<String,GridBucketState> cache=cacheManager.getCache("springcloud-book");
return cache;
}
}

过滤器

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
@Component
public class ErrorFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.ERROR_TYPE;
}
@Override
public int filterOrder() {
return 10;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run()
throws ZuulException
{
RequestContext context=RequestContext.getCurrentContext();
Throwable throwable=context.getThrowable();
if(throwable.getCause().getMessage().contains("ZuulException: 429 TOO_MANY_REQUESTS")){
System.out.println("429 TOO MANY REQUESTS");
}
return null;
}
}

功能验证

上边的服务中,定义了三个api,分别是/hello、/hello1和/hello2。主要验证limit、quota、refresh-interval、type的配置。
根据上边zuul的配置,目前规则是20秒的刷新时间窗口,20秒内只允许2次请求,否则会触发限流。由于上边配置的类型是url,所以这里更准确的说,是20秒内同一个url只允许2次请求,否则触发限流。
这里涉及limit、refresh-interval、type三个配置,由于多次验证quota都没效果,所以后续还需要进一步验证。

验证limit

首先,浏览器访问http://localhost::5001/hello-service/hello,20秒内发起多个请求,会发现前两个请求正常返回,后边的会出现429 TOO_MANY_REQUESTS。
然后,修改limit的值为4,重启zuul,然后浏览器再访问上边的url,20秒内发起多个请求,会看到前4次请求正常返回,后边的会出现429 TOO_MANY_REQUESTS。

以上证明这个参数对限流是生效的。

验证refresh-interval

接着上边的测试,修改refresh-interval为10,然后重启zuul。浏览器还是访问上边的url,保证20秒内超过4个请求,但是每10秒内都不超过4个,会看到都会正常返回结果。
然后,在10秒内发起超过4个的请求,会看到前4个正常返回,后边的就出现429 TOO_MANY_REQUESTS。

以上证明refresh-interval也是生效的。

验证type

为了提高效率,把上边limit改回2,refresh-interval改回20,然后重启zuul。
浏览器访问同一个url,保证20秒内超过2次,会看到不论是http://localhost::5001/hello-service/hello、http://localhost::5001/hello-service/hello1,还是http://localhost::5001/hello-service/hello2,前两次都正常返回,后边的就出现429 TOO_MANY_REQUESTS。

然后还是保证20秒内访问超过2次,但是三个url交叉访问,保证每个20秒内不会有同一个url超过两次,会看到全部都是正常返回。

以上证明当type是url时,针对的就是同一个url。

修改type为origin并重启zuul,然后20秒内同一个浏览器访问url超过2次,会看到不论是同一个url超过2次,还是不同的url超过2次,都是前两次正常返回,后边就出现429 TOO_MANY_REQUESTS。

url_pattern

type是url其实已经基本能够满足大部分需求了,但是有时候可能就是需要对某一类url进行限流,而不是单纯的针对某一个url,那就可以用到url_pattern。
url_pattern的配置相对于url的稍微复杂一点,为了方便验证,我先增加一些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
@GetMapping("/hello")
public String hello(){
return "hello";
}
@GetMapping("/hello1")
public String hello1(){
return "hello1";
}
@GetMapping("/hello2")
public String hello2(){
return "hello2";
}
@GetMapping("/test/hello3")
public String hello3(){
return "hello3";
}
@GetMapping("/test/hello4")
public String hello4(){
return "hello4";
}
@GetMapping("/test1/hello5")
public String hello5(){
return "hello5";
}
@GetMapping("/test1/hello6")
public String hello6(){
return "hello6";
}

然后再修改配置,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
zuul.ratelimit.policy-list.hello-service[0].limit=2
zuul.ratelimit.policy-list.hello-service[0].quota=1
zuul.ratelimit.policy-list.hello-service[0].refresh-interval=20
zuul.ratelimit.policy-list.hello-service[0].type[0]=url_pattern=/hello-service/test/*

zuul.ratelimit.policy-list.hello-service[0].limit=2
zuul.ratelimit.policy-list.hello-service[0].quota=1
zuul.ratelimit.policy-list.hello-service[0].refresh-interval=20
zuul.ratelimit.policy-list.hello-service[0].type[0]=url_pattern=/hello-service/test1/*

zuul.ratelimit.policy-list.hello-service[0].limit=2
zuul.ratelimit.policy-list.hello-service[0].quota=1
zuul.ratelimit.policy-list.hello-service[0].refresh-interval=20
zuul.ratelimit.policy-list.hello-service[0].type[0]=url_pattern=/hello-service/hello

重启zuul和client服务后,再次测试会看到,没有/test或者/test1开头的url以及/hello这个,无论访问多少次,都会是正常返回,而对于/test开头的,无论是同一个还是不同的url,只要达到限制条件,都会是429,/test1开头的也是一样。
和type为url时候一样,如果是在/test开头和/test1开头的url之间切换,保证相同前缀的url不触发限制,那么即使不同前缀的看起来超过了次数,最终也不会触发限流。
以上证明,type为url_pattern时,可以进一步把限流细化到某一个或者某一类url,可以对不同url进行不同的限制,不会一竿子打死,也能保证互不影响。

补充说明

文章开头提到,由于粗心大意,我一度认为是版本问题导致本机无法触发限流,实际只是因为一个符号敲错,那就是其他所有都是对的,但是zuul.ratelimit.enabled=true在最开始被我敲成了zuul-ratelimit.enabled=true。
这个地方实际我检查了不下5遍,但不知为何始终没有发现问题,直到放了差不多一个月重新再看时,才终于看到了问题所在。
这是个多么低级的错误,就如同以前遇到的数据库密码多一个末尾空格一样,很有吐血的冲动。

推荐文章