Please enable Javascript to view the contents

后端服务之接口流量控制

 ·  ☕ 4 分钟

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 对应的总的网络连接数进行限流。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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 值从不同维度限制流量。

测试

1
ab -n 5 -c 5 http://127.0.0.1/limit/
  • ngx_http_limit_req_module
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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;
	}
}	

测试

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

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

安装

1
pip install django-ratelimit

使用

1
2
3
4
5
6
7
8
9
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. 参考


微信公众号
作者
微信公众号