目录

    1. 流控

    缓存、降级和限流是保护高并发系统的常用方法。缓存以空间换时间、减少了 CPU 和网络调用的耗时;降级保护了核心服务的高可用,高峰时段延时或拒绝处理非核心请求;限流是通过限制并发请求来保护系统。

    限流就是,在有限资源的情况下,每个 API 接口单位时间内的服务能力有限,如果,对 API 接口的访问次数不加控制,会造成 API 接口的滥用,甚至招致 DDos 攻击。同时,如下图,API 接口的延迟,也会随着请求量的提升而迅速提升。因此,需要控制单位时间内, API 接口的请求量。

    2. 什么是时间窗口

    时间窗口,指的是一个统计周期的时间段。有两种时间窗口,一种是自然时间窗口,一种是滑动时间窗口。

    上图,就是一个自然时间窗口,每分钟一个窗口。例如,15:01~15:02 统计一次。这种方式的问题是,如果每分钟限流为 N ,那么图中红色时间窗口的最大值是 2N。此时,限流失去了效果。因此,便有了滑动窗口。

    滑动窗口的核心,就是将时间划分为更细的粒度。例如,现在是 15:02:20 ,那么统计的滑动窗口就是 15:01:20 ~ 15:02:20,不同于上面以分钟作为最小粒度,这里的滑动窗口以秒作为最小粒度,从而获得更加精准的流量控制。

    3. 流量控制算法

    • 计数器算法 计数器算法的思路是限制一个接口在某个维度(IP、用户、某种资源)上的响应次数。通过设置一个计数器,每响应一次,计数器加一,当计数器超过阈值时,拒绝服务。这种算法对总数量进行了简单的限制,而不是平均速率限流。
    • 漏桶算法 请求以一定速率进入到漏桶中,漏桶以一定速率响应请求,当水流入速度过大时,拒绝服务。

    • 令牌桶算法 按照固定速率往桶里添加令牌。随着时间流逝,系统会按恒定时间间隔往桶里加入Token,如果桶已经满了,就不再添加。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务

    漏桶算法能够限制数据的传输速率,请求超过处理速率时,会被直接丢弃;而令牌桶算法能够在限制数据的平均传输速率的同时,还可以通过加快添加令牌的速率来处理突发请求。

    4. 不同类型的限流器

    • 请求限流器 限制每个用户每秒可发送 N 个请求
    • 并发请求限流器 限制每秒最高请求数。请求限流器限制的是累积量,而并发限制的是峰值。
    • 基于使用量的负载降级 将请求分为关键 API 请求和非关键 API 请求。设计系统时,为关键 API 请求预留一定资源,当非关键 API 请求需要占用预留资源时,不预分配,直接拒绝服务。
    • 基于 Worker 利用率的负载降级 如果某个 worker 太忙,无法处理分配给它的请求,它会缓慢降级非关键请求,当然是先从测试请求开始。如果降低测试请求的过程中,worker 的处理能力恢复到好的状态,那我们就可以开始缓慢地恢复流量。

    5. 实现

    对于 Nginx 接入层限流可以使用 Nginx 自带了两个模块:连接数限流模块 ngx_http_limit_conn_module 和漏桶算法实现的请求限流模块 ngx_http_limit_req_module。

    • ngx_http_limit_conn_module

    limit_conn 是对某个 KEY 对应的总的网络连接数进行限流。

    http {
        # 配置限流 Key 和存放 Key 对应信息的共享内存区域大小
        limit_conn_zone $binary_remote_addr zone=addr:10m; 
        # 被限流后,默认日志级别
        limit_conn_log_level error; 
        # 被限流后,返回的状态码
        limit_conn_status 503;
        ...
        server {
        ...
        location /limit {
            # 配置存放 Key 和计数器的共享内存区域,指定 Key 的最大连接数
            limit_conn addr 1;
        }
    

    此处使用 Key 为 $binary_remote_addr 表示 IP 地址,还可以使用 $server_name 表示域名,不同的 Key 值从不同维度限制流量。

    测试

    ab -n 5 -c 5 http://127.0.0.1/limit/
    
    • ngx_http_limit_req_module
    http {
        ## 对每一个IP的请求限制为1次每秒
        limit_req_zone $binary_remote_addr zone=perip:10m rate=1r/s;
        # 被限流后,默认日志级别
        limit_conn_log_level error; 
        # 被限流后,返回的状态码
        limit_conn_status 503;
        ...
        server {
        ...
    
        location /limit {
            # burst 配置桶的大小,nodelay 表示非延迟模式,允许突发处理请求。
            limit_req zone=perip burst=3 nodelay;
        }
    }    
    

    测试

    ab -n 5 -c 5 http://127.0.0.1/limit/
    
    • 设置 IP 黑白名单
    location / {
        allow 192.168.1.1/24;
        allow 127.0.0.1;
        deny 1.2.3.4;
        # deny  all;
    }
    
    • django-ratelimit

      django-ratelimit 是一个基于缓存的接口限速包,使用装饰器对 API 接口进行流量控制。。

    安装

    pip install django-ratelimit
    

    使用

    from ratelimit.decorators import ratelimit
    
    @ratelimit(key='ip')
    def myview(request):
        # ...
    
    @ratelimit(key='ip', rate='100/h')
    def secondview(request):
        # ...
    

    这里的 Key 表示的是统计的维度,可以是 ip、get 中获取的某个参数、post 中获取的某个参数、header 中获取的某个参数、user、user_or_ip。rate 表示的是限速 X/u,X 表示数字,u 表示时间单位,可以是,s、m、h、d。

    rate 还可以是一个函数,只需要返回指定的格式即可。通过这种方式,可以实现一些特殊的限制功能。比如,匿名用户和登录用户可以采用不同的限制值。

    6. 分布式流控

    分布式限流最关键的是要将限流服务做成原子化。

    解决方案是,通过 Redis + Lua 或者 Nginx + Lua 的技术,实现对请求并发数和总数在时间窗口下的控制。使用 Lua 实现令牌通或漏桶算法。

    7. 参考