• Spring cloud 多种限流方案


    在频繁的网络请求时,服务有时候也会受到很大的压力,尤其是那种网络攻击,非法的。这样的情形有时候需要作一些限制。例如:限制对方的请求,这种限制可以有几个依据:请求IP、用户唯一标识、请求的接口地址等等。

    当前限流的方式也很多:Spring cloud 中在网关本身自带限流的一些功能,基于 redis 来做的。同时,阿里也开源了一款:限流神器 Sentinel。今天我们主要围绕这两块来实战微服务的限流机制。

    首先讲 Spring cloud 原生的限流功能,因为限流可以是对每个服务进行限流,也可以对于网关统一作限流处理。

    一、实战基于 Spring cloud Gateway 的限流

    pom.xml引入依赖:
     

    1. org.springframework.boot
    2. spring-boot-starter-data-redis-reactive

    其基础是基于redis,所以:

    1. spring:
    2. application:
    3. name: gateway-service
    4. redis: #redis相关配置
    5. database: 8
    6. host: localhost
    7. port: 6379
    8. password: 123456 #有密码时设置
    9. jedis:
    10. pool:
    11. max-active: 8
    12. max-idle: 8
    13. min-idle: 0
    14. timeout: 10000ms

    接下来需要注入限流策略的 bean:

    1. @Primary
    2. @Bean(value = "ipKeyResolver")
    3. KeyResolver ipKeyResolver() {
    4. return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    5. //return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    6. //return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    7. }
    8. /**
    9. * API限流
    10. * @return
    11. *
    12. */
    13. @Bean(value = "apiKeyResolver")
    14. KeyResolver apiKeyResolver() {
    15. return exchange -> Mono.just(exchange.getRequest().getPath().value());
    16. }
    17. /**
    18. * 请求路径中必须携带userId参数
    19. * 用户限流
    20. * @return
    21. *
    22. */
    23. @Bean(value = "userKeyResolver")
    24. KeyResolver userKeyResolver() {
    25. return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
    26. }

    这里引入ipKeyResolver、apiKeyResolver、userKeyResolver三种策略,可以利用注解 @Primary 来决定其中一个被使用。

    注入bean后,需要在配置中备用:

    1. spring:
    2. application:
    3. name: gateway-service
    4. redis: #redis相关配置
    5. database: 8
    6. host: localhost
    7. port: 6379
    8. password: 123456 #有密码时设置
    9. jedis:
    10. pool:
    11. max-active: 8
    12. max-idle: 8
    13. min-idle: 0
    14. timeout: 10000ms
    15. cloud:
    16. kubernetes:
    17. discovery:
    18. all-namespaces: true
    19. gateway:
    20. discovery:
    21. locator:
    22. enabled: true
    23. lowerCaseServiceId: true
    24. routes: #路由配置:参数为一个List
    25. - id: cas-server #唯一标识
    26. uri: lb://cas-server-service #转发的地址,写服务名称
    27. order: -1
    28. predicates:
    29. - Path=/cas-server/** #判断匹配条件,即地址带有/ribbon/**的请求,会转发至lb:cas-server-service
    30. filters:
    31. - StripPrefix=1 #去掉Path前缀,参数为1代表去掉/ribbon
    32. - name: RequestRateLimiter #基于redis的Gateway的自身限流
    33. args:
    34. redis-rate-limiter.replenishRate: 1 # 允许用户每秒处理多少个请求
    35. redis-rate-limiter.burstCapacity: 3 # 令牌桶的容量,允许在一秒钟内完成的最大请求数
    36. key-resolver: "#{@ipKeyResolver}" #SPEL表达式取的对应的bean
    37. - id: admin-web
    38. uri: lb://admin-web-service
    39. order: -1
    40. predicates:
    41. - Path=/admin-web/**
    42. filters:
    43. - StripPrefix=1
    44. - name: RequestRateLimiter
    45. args:
    46. redis-rate-limiter.replenishRate: 1 # 允许用户每秒处理多少个请求
    47. redis-rate-limiter.burstCapacity: 3 # 令牌桶的容量,允许在一秒钟内完成的最大请求数
    48. key-resolver: "#{@ipKeyResolver}" #SPEL表达式取的对应的bean
    49. - id: order-service
    50. uri: lb://order-service-service
    51. order: -1
    52. predicates:
    53. - Path=/order-service/**
    54. filters:
    55. - StripPrefix=1
    56. - name: RequestRateLimiter
    57. args:
    58. redis-rate-limiter.replenishRate: 1 # 允许用户每秒处理多少个请求
    59. redis-rate-limiter.burstCapacity: 3 # 令牌桶的容量,允许在一秒钟内完成的最大请求数
    60. key-resolver: "#{@ipKeyResolver}" #SPEL表达式取的对应的bean
    61. http:
    62. encoding:
    63. charset: UTF-8
    64. enabled: true
    65. force: true
    66. mvc:
    67. throw-exception-if-no-handler-found: true
    68. main:
    69. allow-bean-definition-overriding: true # 当遇到同样名称时,是否允许覆盖注册

    这里是在原有的路由基础上加入 RequestRateLimiter限流过滤器,包括三个参数:

    1. - name: RequestRateLimiter #基于redis的Gateway的自身限流
    2. args:
    3. redis-rate-limiter.replenishRate: 3 #允许用户每秒处理多少个请求
    4. redis-rate-limiter.burstCapacity: 5 #令牌桶的容量,允许在一秒钟内完成的最大请求数
    5. key-resolver: "#{@ipKeyResolver}" #SPEL表达式取的对应的bean
    • 其中 replenishRate,其含义表示允许每秒处理请求数;
    • burstCapacity 表示允许在一秒内处理的最大请求数;
    • key-resolver 这里采用请求 IP 限流,利用SPEL 表达式取对应的 bean

    写一个小脚本来压测一下:

    1. for i in $(seq 1 30000); do echo $(expr $i \\* 3 + 1);curl -i -H "Accept: application/json" -H "Authorization:bearer b064d95b-af3f-4053-a980-377c63ab3413" -X GET http://10.10.15.5:5556/order-service/api/order/getUserInfo;done
    2. for i in $(seq 1 30000); do echo $(expr $i \\* 3 + 1);curl -i -H "Accept: application/json" -H "Authorization:bearer b064d95b-af3f-4053-a980-377c63ab3413" -X GET http://10.10.15.5:5556/admin-web/api/user/getCurrentUser;done

    上面两个脚本分别对2个服务进行压测,打印结果:

    1. HTTP/1.1 200 OK
    2. transfer-encoding: chunked
    3. X-RateLimit-Remaining: 2
    4. X-RateLimit-Burst-Capacity: 3
    5. X-RateLimit-Replenish-Rate: 1
    6. Expires: 0
    7. Cache-Control: no-cache, no-store, max-age=0, must-revalidate
    8. Set-Cookie: ORDER-SERVICE-SESSIONID=R99Ljit9XvfCapyUJDWL8I0rZqxReoY6HwcQV2n2; path=/
    9. X-XSS-Protection: 1; mode=block
    10. Pragma: no-cache
    11. X-Frame-Options: DENY
    12. Date: Thu, 19 Mar 2020 06:32:27 GMT
    13. X-Content-Type-Options: nosniff
    14. Content-Type: application/json;charset=UTF-8
    15. {"message":{"status":200,"code":0,"message":"success"},"data":"{\"message\":{\"status\":200,\"code\":0,\"message\":\"get user success\"},\"data\":{\"id\":23,\"isAdmin\":1,\"userId\":\"fbb18810-e980-428c-932f-848f3b9e7c84\",\"userType\":\"super_admin\",\"username\":\"admin\",\"realName\":\"super_admin\",\"password\":\"$2a$10$89AqlYKlnsTpNmWcCMvgluRFQ/6MLK1k/nkBpz.Lw6Exh.WMQFH6W\",\"phone\":null,\"email\":null,\"createBy\":\"admin\",\"createTime\":1573119753172,\"updateBy\":\"admin\",\"updateTime\":1573119753172,\"loginTime\":null,\"expireTime\":null,\"remarks\":\"super_admin\",\"delFlag\":0,\"loginType\":null}}"}ex
    16. 同一秒内多次后:
    17. HTTP/1.1 429 Too Many Requests
    18. X-RateLimit-Remaining: 0
    19. X-RateLimit-Burst-Capacity: 3
    20. X-RateLimit-Replenish-Rate: 1
    21. content-length: 0
    22. expr: syntax error
    23. HTTP/1.1 429 Too Many Requests
    24. X-RateLimit-Remaining: 0
    25. X-RateLimit-Burst-Capacity: 3
    26. X-RateLimit-Replenish-Rate: 1
    27. content-length: 0
    28. expr: syntax error

    从上面可以看到,执行后,会出现调用失败的情况,状态变为429 (Too Many Requests) 。

    二、基于阿里开源限流神器:Sentinel

    首先引入依赖:

    1. com.alibaba.cloud
    2. spring-cloud-starter-alibaba-sentinel

    在配置文件 application.yaml 文件中配置,需要新增2个配置:

    1. spring:
    2. application:
    3. name: admin-web
    4. cloud:
    5. kubernetes:
    6. discovery:
    7. all-namespaces: true
    8. sentinel:
    9. eager: true #取消Sentinel控制台的懒加载
    10. transport:
    11. dashboard: localhost:8080 #sentinel的Dashboard地址
    12. port: 8719 #是sentinel应用端和控制台通信端口
    13. heartbeat-interval-ms: 500 #心跳时间
    14. scg:
    15. fallback: #scg.fallback为sentinel限流后的响应配置
    16. mode: response
    17. response-status: 455
    18. response-body: 已被限流

    其中,这里面配置了一个服务:spring.cloud.sentinel.transport.dashboard,配置的是 sentinel 的 Dashboard 地址。同时 spring.cloud.sentinel.transport.port 这个端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。

    Sentinel 默认为所有的 HTTP 服务提供限流埋点,上面配置完成后自动完成所有埋点,只需要控制配置限流规则即可。

    这里我们讲下通过注解来给指定接口函数加上限流埋点,写一个 RestController,在接口函数上加上注解 @SentinelResource:

    1. @GetMapping(value = "/getToken")
    2. @SentinelResource("getToken")
    3. public Response<Object> getToken(Authentication authentication){
    4. //Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    5. authentication.getCredentials();
    6. OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
    7. String token = details.getTokenValue();
    8. return Response.ok(200, 0, "get token success", token);
    9. }

    以上代码部分完成了,接下来先安装 SentinelDashBoard,Sentinel DashBoard 下载地址:https://github.com/alibaba/Sentinel/releases 。

    下载完成后,命令启动:

    java -jar sentinel-dashboard-1.6.2.jar
    

    默认启动端口为 8080,访问 IP:8080,就可以显示 Sentinel 的登录界面,用户名与密码均为 sentinel。登录 Dashboard 成功后,多次访问接口"/getToken",可以在 Dashboard 看到相应数据,这里不展示了。接下来可以设置接口的限流功能,在 “+流控” 按钮点击打开设置界面,设置阈值类型为 qps,单机阈值为 5。

    浏览器重复请求 http://localhost:5556/admin-web/api/user/getToken 如果超过阀值就会出现如下界面信息:

    Blocked by Sentinel (flow limiting)

    此时,就看到Sentinel 限流起作用了,可以加上 spring.cloud.sentinel.scg.fallback 为sentinel 限流后的响应配置,亦可自定义限流异常信息:

    1. @GetMapping(value = "/getToken")
    2. @SentinelResource(value = "getToken", blockHandler = "handleSentinelException", blockHandlerClass = {MySentinelException.class}))
    3. public Response getToken(Authentication authentication){
    4. //Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    5. authentication.getCredentials();
    6. OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)authentication.getDetails();
    7. String token = details.getTokenValue();
    8. return Response.ok(200, 0, "get token success", token);
    9. }
    10. public class MySentinelException {
    11. public static Response handleSentinelException(BlockException e) {
    12. Map map=new HashMap<>();
    13. logger.info("Oops: " + ex.getClass().getCanonicalName());
    14. return Response.ok(200, -8, "通过注解 @SentinelResource 配置限流埋点并自定义限流后的处理逻辑", null);
    15. }
    16. }
    17. 这里讲下注解 @SentinelResource 包含以下属性:

      • value:资源名称,必需项;
      • entryType:入口类型,可选项(默认为 EntryType.OUT);
      • blockHandler:blockHandlerClass 中对应的异常处理方法名,参数类型和返回值必须和原方法一致;
      • blockHandlerClass:自定义限流逻辑处理类

      Sentinel 限流逻辑处理完毕了,但每次服务重启后,之前配置的限流规则就会被清空。因为是内存形式的规则对象。所以下面就讲下用 Sentinel 的一个特性 ReadableDataSource 获取文件、数据库或者配置中心设置限流规则,目前支持 Apollo、Nacos、ZK 配置来管理。

      首先回忆一下,一条限流规则主要由下面几个因素组成:

      • resource:资源名,即限流规则的作用对象,即为注解 @SentinelResource 的 value;
      • count:限流阈值;grade:限流阈值类型(QPS 或并发线程数);
      • limitApp:流控针对的调用来源,若为 default 则不区分调用来源;
      • strategy:基于调用关系的限流策略;
      • controlBehavior:流量控制效果(直接拒绝、排队等待、匀速器模式)

      接下来通过文件来配置:

      1. #通过文件读取限流规则
      2. spring.cloud.sentinel.datasource.ds1.file.file=classpath:flowrule.json
      3. spring.cloud.sentinel.datasource.ds1.file.data-type=json
      4. spring.cloud.sentinel.datasource.ds1.file.rule-type=flow

      在resources新建一个文件,比如 flowrule.json 添加限流规则:

      1. [
      2. {
      3. "resource": "getToken",
      4. "count": 1,
      5. "controlBehavior": 0,
      6. "grade": 1,
      7. "limitApp": "default",
      8. "strategy": 0
      9. },
      10. {
      11. "resource": "resource",
      12. "count": 1,
      13. "controlBehavior": 0,
      14. "grade": 1,
      15. "limitApp": "default",
      16. "strategy": 0
      17. }
      18. ]

      重新启动项目,出现如下日志说明成功:

      1. DataSource ds1-sentinel-file-datasource start to loadConfig
      2. DataSource ds1-sentinel-file-datasource load 2 FlowRule

      如果采用 Nacos 作为配置获取限流规则,可在文件中加如下配置:

      1. spring:
      2. application:
      3. name: order-service
      4. cloud:
      5. nacos:
      6. config:
      7. server-addr: localhost:8848
      8. discovery:
      9. server-addr: localhost:8848
      10. sentinel:
      11. eager: true
      12. transport:
      13. dashboard: localhost:8080
      14. datasource:
      15. ds1:
      16. nacos:
      17. server-addr: localhost:8848
      18. dataId: ${spring.application.name}-flow-rules
      19. data-type: json
      20. rule-type: flow

      更多开源实战利用 k8s 作微服务的架构设计代码:spring-cloud-k8s: 利用k8s实现微服务架构设计,包括网关、鉴权、负载均衡、配置管理、熔断、流控等

    18. 相关阅读:
      维也纳国际酒店8月再签9大项目,中高端酒店凭何获市场热捧
      Vim 突然报错:E576: viminfo: 缺少 ‘>‘ 位于行
      手把手操作JS逆向爬虫入门(三)---Headers请求头参数加密
      Vue.js 页面加载时触发函数
      Matlab图像处理- 车牌图像数据特征分析
      Python语法:如何使用requirements.txt文件在Python环境中安装依赖?
      如何利用分层测试概念设计针对性测试用例
      grep批量筛选指定目录下的所有日志并写入文件内
      iOS打基础之Block二三事
      golang pprof 监控系列(1) —— go trace 统计原理与使用
    19. 原文地址:https://blog.csdn.net/WXF_Sir/article/details/126279975