Skip to content

Latest commit

 

History

History
284 lines (253 loc) · 9.42 KB

如何限流.md

File metadata and controls

284 lines (253 loc) · 9.42 KB

如何限流

限流一般是一种保护系统稳定性的一种措施。在开放API中比较常见。
对某一用户,或指定IP,或具体到某个API,在一个具体的时间里限制请求次数。

关键词

  • 熔断
  • 服务降级
  • 延迟处理
  • 特权处理

主流框架的实现

首先看看主要的框架是使用,再讨论自己怎么实现

Nginx

Nginx 自带的两个限流模块

  • ngx_http_limit_conn_module 连接数限流模块
  • ngx_http_limit_req_module 请求数限流模块
  • ngx_lua
  • ngx_lua_waf模块
  • OpenResty

ngx_http_limit_conn_module

http {
    limit_conn_zone $binary_remote_addr zone=addr:10m;
    limit_conn_log_level error;
    limit_conn_status 503;
    ...
    server {
        ...
        location /download/ {
            limit_conn addr 1;
        }

ab test
ab -n10 -c3 https://ifuture.pro/test/
  • limit_conn_zone: 配置限流的key以及存储这些key共用的共享内存的大小;
    • key 是$binary_remote_addr,表示IP地址,如果如果需要对总域名进行限流,key就应该使用 $server_name $host等等,能唯一表示该域名的Nginx变量;
    • zone=addr:10m中,addr表示连接数限流的区域名称,10m表示可以分配的共享空间的大小。
  • binary_remote_addr变量在64位平台中占用64字节。1M共享空间可以保存1.6万个64位的,10m就可以保存16万个。如果超过16万个,共享空间被用完,服务器将会对后续所有的请求返回 503。
  • limit_conn:配置指定key的最大连接数。样例中指定的最大连接数是1,表示Nginx最多同时允许1个连接进行location /limit 的行为操作。
  • limit_conn_status:配置被限流后返回的状态码,样例中设置的是503.
  • limit_conn_log_level:配置被限流后的日志级别,设置的是error级别

ngx_http_limit_req_module

http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
    limit_req_log_level error;
    limit_req_status 503;
    ...
    server {
        ...
        location /search/ {
            limit_req zone=one burst=5;
        }

rate=1r/s 固定请求速率设置,每秒1个请求。

抵御CC攻击

http{
    ...
    limit_req_zone $cookie_token zone=session_limit:3m rate=1r/s;
    limit_req_zone $binary_remote_addr $uri zone=auth_limit:3m rate=1r/m;
}
location /{
limit_req zone=session_limit burst=5;
rewrite_by_lua '
    local random = ngx.var.cookie_random
    if (random == nil) then
    return ngx.redirect("/auth?url=" .. ngx.var.request_uri)
    end
    local token = ngx.md5("opencdn" .. ngx.var.remote_addr .. random)
    if (ngx.var.cookie_token ~= token) then
    return ngx.redirect("/auth?url=".. ngx.var.request_uri)
    end
';
}
location /auth {
limit_req zone=auth_limit burst=1;
if ($arg_url = "")
{
return403;
}
access_by_lua '
local random = math.random(9999)
local token = ngx.md5("opencdn" .. ngx.var.remote_addr .. random)
if (ngx.var.cookie_token ~= token) then
ngx.header["Set-Cookie"] = {
    "token=" .. token, "random=" .. random
}
return ngx.redirect(ngx.var.arg_url)
end
';
}

spring-cloud-gateway

maven pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

appliaction.yml

spring:
  cloud:
    gateway:
      routes:
      - id: zzz-transfer
        uri: lb://zzz-transfer
        order: 8002
        predicates:
        - Path=/api/tengine/**
        - CacheBody={*}
        filters:
        - StripPrefix=2
        - name: RequestRateLimiter
            args:
              redis-rate-limiter.replenishRate: 1  # 允许用户每秒处理多少个请求
              redis-rate-limiter.burstCapacity: 3  # 令牌桶的容量,允许在一秒钟内完成的最大请求数
              key-resolver: "#{@hostNameKeyResolver}" #SPEL表达式去的对应的bean
      enabled: true
  redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    /**
     * IP 限流
     * @return
     */
    @Bean
    @Primary
    KeyResolver hostNameKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    }

    /**
     * API 地址限流
     * @return
     */
    @Bean
    KeyResolver apiKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getPath().value());
    }

Alibaba Sentinel

Spring Cloud Netflix 诸多组件不在更新维护后 Spring Cloud也正式加入了spring-cloud-alibaba Sentinel就是其中一员,它把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性

pom.xml

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

配置方式

  • 基于sentinel-dashboard 界面化配置

    下载 sentinel-dashboard https://github.com/alibaba/Sentinel/releases

    java -jar sentinel-dashboard.jar --server.port=8080
    
    可能在sentinel dashboard上不显示,请求一下个个服务试试

    application.yml

    spring:
      application:
        name: zzz-gateway
      cloud:
        nacos:
          discovery:
            server-addr: z.com:8848
        sentinel:
          transport:
            dashboard: z.com:8080
          enabled: true
    
  • 基于nacos config

    application.yml

    spring:
      application:
        name: zzz-gateway
      cloud:
        nacos:
          discovery:
            server-addr: ifuture.pro:8848
          config:
            server-addr: ifuture.pro:8848
            file-extension: yaml
        sentinel:
          transport:
            dashboard: ifuture.pro:8080
          datasource:
              ds:
                nacos:
                  server-addr: ifuture.pro:8848
                  dataId: spring-cloud-sentinel-nacos
                  groupId: DEFAULT_GROUP
                  rule-type: flow
                  namespace: zzzgateway
    
    - resource: "/api"
      limitApp: default
      grade: 1
      count: 1
      strategy: 0
      controlBehavior: 0
      clusterMode: true
    
      resource:资源名,即限流规则的作用对象。
      limitApp:流控针对的调用来源,若为 default 则不区分调用来源。
      grade:限流阈值类型,QPS 或线程数模式,0代表根据并发数量来限流,1代表根据QPS来进行流量控制。
      count:限流阈值
      strategy:判断的根据是资源自身,还是根据其它关联资源 (refResource),还是根据链路入口
      controlBehavior:流控效果(直接拒绝 / 排队等待 / 慢启动模式)
      clusterMode:是否为集群模式
  • 基于代码 https://github.com/alibaba/Sentinel/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8

自己实现

计数器

系统维护一个计数器,来一个请求就加1,请求处理完成就减1,当计数器大于指定的阈值,就拒绝新的请求。
基于这个简单的方法,可以再延伸出一些高级功能,比如阈值可以不是固定值,是动态调整的。另外,还可以有多组计数器分别管理不同的服务,以保证互不影响等。

队列

就是基于FIFO队列,所有请求都进入队列,后端程序从队列中取出待处理的请求依次处理。
基于队列的方法,也可以延伸出更多的玩法来,比如可以设置多个队列以配置不同的优先级。

令牌桶

首先还是要基于一个队列,请求放到队列里面。但除了队列以外,还要设置一个令牌桶,另外有一个脚本以持续恒定的速度往令牌桶里面放令牌,后端处理程序每处理一个请求就必须从桶里拿出一个令牌,如果令牌拿完了,那就不能处理请求了。我们可以控制脚本放令牌的速度来达到控制后端处理的速度,以实现动态流控。

  • 一个固定容量的桶。如:每分钟能请求的最大量
  • 一个永续线程以匀速的方式将令牌放入桶中,超过容量丢弃
  • 有请求了,将桶内令牌取出,如果没有足够的令牌则限流

Spring Redis Lua

Spring Cloud Gateway 就是基于 Redis + Lua 的方式实现限流的,主要是减少网络开销,使用Lua脚本,无需向Redis 发送多次请求,执行一次即可。

Lua 脚本位置

spring-cloud-gateway-core-2.1.0.RELEASE.jar/META-INF/scripts/request_rate_limiter.lua

可以使用DefaultRedisScript 去调用。模仿spring cloud gateway 自己写一个限流吧

String luaScript = getLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);