使用spring-cloud-zuul-rate-limit在zuul中为服务限流(代码片段)

涂宗勋 涂宗勋     2022-12-25     232

关键词:


前言

几个月前做过一个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文件

<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>

启动类

@EnableEurekaServer
@SpringBootApplication
public class CloudEurekaApplication 
   public static void main(String[] args) 
      SpringApplication.run(CloudEurekaApplication.class, args);
   

配置文件

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文件

<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>

启动类

@EnableDiscoveryClient
@SpringBootApplication
public class CloudServer1Application 
   public static void main(String[] args) 
      SpringApplication.run(CloudServer1Application.class, args);
   

controller

@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";
   

配置文件

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文件

<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>

启动类

@EnableZuulProxy
@SpringCloudApplication
public class CloudZuulApplication 
   public static void main(String[] args) 
      SpringApplication.run(CloudZuulApplication.class, args);
   

配置文件

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配置类

@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;
   

过滤器

@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接口,使他们有一些有相同的部分,修改后如下:

@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";

然后再修改配置,如下:

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遍,但不知为何始终没有发现问题,直到放了差不多一个月重新再看时,才终于看到了问题所在。
这是个多么低级的错误,就如同以前遇到的数据库密码多一个末尾空格一样,很有吐血的冲动。

使用“使用严格”作为“使用强”的备份

】使用“使用严格”作为“使用强”的备份【英文标题】:Using"usingstrict"asabackupfor"usestrong"【发布时间】:2016-06-1604:26:29【问题描述】:有没有办法使用"usestrong";并使用"usestrict";作为备份?因为Google... 查看详情

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?

】在使用加载数据流步骤的猪中,使用(使用PigStorage)和不使用它有啥区别?【英文标题】:InpigwhileusingLoaddataflowstepwhatisdifferencewithusing(UsingPigStorage)andwithoutusingit?在使用加载数据流步骤的猪中,使用(使用PigStorage)和不使用它... 查看详情

MySQL db 在按日期排序时使用“使用位置;使用临时;使用文件排序”

】MySQLdb在按日期排序时使用“使用位置;使用临时;使用文件排序”【英文标题】:MySQLdbisusing"Usingwhere;Usingtemporary;Usingfilesort"whensortingbydate【发布时间】:2011-07-2207:56:47【问题描述】:我有一个包含一堆记录的数据库,... 查看详情

如何使用 AutoMapper 使用 EntityFramework 使用嵌套列表更新对象?

】如何使用AutoMapper使用EntityFramework使用嵌套列表更新对象?【英文标题】:HowtouseAutoMapperforupdatingObjectwithnestedListusingEntityFramework?【发布时间】:2022-01-0315:40:44【问题描述】:我想使用AutoMapper将带有嵌套列表的EntityDto映射到实体... 查看详情

qt静态编译时使用openssl有三种方式(不使用,动态使用,静态使用,默认是动态使用)

WhencompilingQtyoucanchooseoneoftheseoptionsbasedontheconfigurecommandline:noOpenSSLsupport(-no-openssl)QtNetworkdynamicallyopeningOpenSSLlibs(-openssl;default)QtNetworklinkingtoOpenSSL(-openssl-linke 查看详情

何时使用自旋锁?何时使用互斥体?

中断上下文只能使用自旋锁。任务睡眠时只能使用互斥体。需求建议的加锁方法低开销加锁优先使用自旋锁短期锁定优先使用自旋锁长期加锁优先使用互斥体中断上下文加锁使用自旋锁持有锁需要睡眠使用互斥体  查看详情

kettlejava脚本组件的使用说明(简单使用升级使用)

文章目录前言Kettlejava脚本组件的使用说明(简单使用、升级使用)01简单使用02升级使用前言  如果您觉得有用的话,记得给博主点个赞,评论,收藏一键三连啊,写作不易啊^_^。  而且听说点赞的人每天的运气... 查看详情

使用pidstat监控资源使用

 linux可以使用pidstat命令监控系统资源,比如监控cup使用如下:pidstat-u1还可以使用-r(内存)-d(硬盘) 查看详情

如何使用公钥加密字符串并使用 MimeKit 使用私钥解密?

】如何使用公钥加密字符串并使用MimeKit使用私钥解密?【英文标题】:HowtoencryptstringwithpublickeyanddecryptusingprivatekeyusingMimeKit?【发布时间】:2021-04-0118:27:35【问题描述】:我很难寻找有关如何使用公钥证书加密字符串并使用Mimekit... 查看详情

如何使用 webpack 使用它

】如何使用webpack使用它【英文标题】:Howtoconsumethiswithwebpack【发布时间】:2015-10-1405:17:20【问题描述】:如何在webpackreact应用程序中使用这个repo:https://github.com/chris-rudmin/Recorderjs我已经创建了一个新的库并以es6模块样式导出主... 查看详情

使用 C++ 和 Boost(或不使用?)检查是不是正在使用特定端口?

】使用C++和Boost(或不使用?)检查是不是正在使用特定端口?【英文标题】:UsingC++andBoost(ornot?)tocheckifaspecificportisbeingused?使用C++和Boost(或不使用?)检查是否正在使用特定端口?【发布时间】:2016-01-2607:04:05【问题描述】:... 查看详情

Mysql查询使用索引使用文件排序使用临时

】Mysql查询使用索引使用文件排序使用临时【英文标题】:Mysqlqueryusingindexusingfilesortusingtemporary【发布时间】:2014-08-2713:58:18【问题描述】:我的数据库中有以下两个表:表1:图片列:jeid[和其他]主键:jeid行数:160万表2:媒体... 查看详情

如何使用 React 使用 Notion API

】如何使用React使用NotionAPI【英文标题】:HowtoconsumeNotionAPIwithReact【发布时间】:2021-08-0601:54:19【问题描述】:我正在尝试使用新的NotionAPI作为我个人网站的CMS。作为一种改进方法,我尝试将它与React一起使用。但它似乎不允许CO... 查看详情

如何使用@JmsListener 暂停并开始使用消息

】如何使用@JmsListener暂停并开始使用消息【英文标题】:Howtopauseandstartconsumingmessageusing@JmsListener【发布时间】:2016-07-0223:47:57【问题描述】:我使用的是SpringBoot1.3.2版。我正在使用@JmsListener为我使用JmsTemplate创建/生成的消息使用... 查看详情

为啥使用最近最少使用的简单缓存机制?

】为啥使用最近最少使用的简单缓存机制?【英文标题】:WhyistheSimpleLeastRecentlyUsedCacheMechanismused?为什么使用最近最少使用的简单缓存机制?【发布时间】:2018-03-0208:27:47【问题描述】:我使用JProfiler检查Java微服务,同时使用JMe... 查看详情

使用 Makecert 设置密钥使用属性

】使用Makecert设置密钥使用属性【英文标题】:SettingKeyUsageattributeswithMakecert【发布时间】:2011-02-2615:45:58【问题描述】:是否可以使用makecert或任何其他我可以用来生成我自己的测试证书的工具来设置密钥使用属性?我感兴趣的... 查看详情

使用 multiDexEnabled 而不使用 Gradle,而是使用 Eclipse 构建过程

】使用multiDexEnabled而不使用Gradle,而是使用Eclipse构建过程【英文标题】:UsemultiDexEnabledwithoutGradlebutEclipsebuildprocessinstead【发布时间】:2015-01-0109:14:22【问题描述】:由于最新的SDK版本,创建具有多个dex文件(https://developer.android.co... 查看详情

使用 jQuery 使用 WCF 服务

】使用jQuery使用WCF服务【英文标题】:ConsumingWCFserviceusingjQuery【发布时间】:2011-08-1023:55:33【问题描述】:到目前为止,我已经使用了Web服务,并且运行良好。我添加了一个新的WCF服务。我正在使用jQuery调用服务。这就是我使用... 查看详情