分布式系统架构下面对突增的高并发访问请求,如何实现限流以保护系统的可用性是需要关注的一个问题。分布式限流实现机制上有很多中,包括基于网关实现、基于中间件如Redis实现等,本文简要介绍限流的常用算法以及实现方案。
1、分布式限流概述
所谓“限流”是在高并发业务场景下,超过系统处理能力后,为确保系统稳定性和可用性所采取的技术手段。比如12306抢火车票的验证码图片,双十一等秒杀活动下的排队处理等,各个技术厂商的限流手段也是层出不穷。在现实生活中的限流也很常见,比如高峰期的地铁限流、黄金周的景区限流,还有疫情高峰期的排队核酸等。
在分布式系统中,面对大数据量的高并发访问请求时,可能有多个服务节点协同工作,每个节点可能会处理多个请求。因此,需要一种方法来控制每个请求的访问频率,以避免过多的请求导致系统负载过高或崩溃。常用的技术手段就是限流,限流其实就是在某个时间窗口对资源访问的限制,而分布式限流就是将整个分布式环境当一个整体来考量,当达到请求数或者并发限制后,就进行等待、排队、降级或者拒绝服务等限制。
1.1 分布式限流规则
限流通常是对时间和资源两个维度的限制:1)基于特定的时间窗口比如每分钟、每秒钟限制;2)基于可用资源的限制比如设定最大访问次数或连接数。
在实际的使用场景中会基于多个规则进行限制,常见的限流规则如下:
- 基于QPS和连接数控制:根据每个节点的QPS和连接数来设定限流值,如果某个节点的请求量超过了限流值,则拒绝该请求。
- 基于传输速率限制:通过限制每个请求的传输速率来控制请求的速率,如果传输速率超过了限制,则拒绝请求。
- 基于IP纬度限制:根据每个请求的IP地址进行限流,如果多个请求的IP地址属于同一个子网,则按照同一个限流规则进行限流。
- 基于服务器限制:根据每个请求所访问的服务器来进行限流,如果多个请求访问了同一台服务器,则按照同一个限流规则进行限流。
- 黑白名单限制:将拒绝请求的理由设置为黑名单,将允许请求的理由设置为白名单。黑名单可以根据实际需求进行设置,白名单可以从配置文件或用户输入中获取。
在实际业务场景使用过程中,需要根据具体情况进行调整和优化,以确保分布式限流算法能够满足实际需求,提高系统的性能和可用性。
1.2 分布式限流方案
分布式限流的主流方案包括从用户客户端限流、服务网关层实现限流以及基于中间件或限流组件实现限流。
- 客户端限流:类似于12306抢票的验证码限制,通过在前端增加验证访问达到限制流量的目的,从而大幅度降低系统的访问压力。
- 网关层限流策略:在分布式系统中,服务网关作为分布式链路的第一道关卡,承接了所有的用户请求。如图所示,从服务网关层->后端服务->后端缓存->数据库层,整个流量访问是一个漏斗型,在网关层做好流量的控制后,将极大的降低后端服务层的压力。常用的网关实现可以使用Nginx、Spring Cloud Gateway等。
- 基于中间件的限流策略:网关层的限流措施对后端业务来说无法控制,因此可以使用中间件比如Redis实现服务层的限流策略,利用Redis脚本化编程能力将限流模块和业务功能解耦,以及Redis高并发特性和高可用架构实现稳定的中间件层限流机制。
- 基于限流组件实现限流:一些开源的限流组件如阿里的Sentinel可以实现限流功能
2、分布式限流常用算法介绍
无论是网关层限流,还是基于中间件层的实现,具体的分布式限流算法无外乎以下几种:
- 计数器限流算法:在有效时间内计算请求次数,调用一次+1,调用结束-1。可以使用Redis的incr或其他计数工具实现。
- 滑动窗口限流算法:每过一个步长,整体时间区域滑动一下,以滑动窗口的机制减少临界值带来的超过阈值的问题。
- 漏桶限流算法:恒定速率的限流算法,不论客户端请求量是多少,服务端处理请求的速度都是恒定的。
- 令牌桶限流算法:有一个令牌桶和定时器,在一个时间段内往令牌桶里生成固定数量的令牌,请求就从桶里拿一个令牌,如果令牌没了,就排队或拒绝服务。
2.1 计数器限流算法
计数器限流算法是一种简单而有效的限流方案,它通过为每个请求分配一个计数器,当计数器达到一定值时,限制请求的处理速率或拒绝请求。这种方案简单易实现,但可能会出现限流不够精准的情况。
实现计数器限流算法的步骤如下:
- 设置一个计数器count,接收一个请求就将计数器加一,同时记录当前时间。
- 判断当前时间和上次统计时间是否为同一分钟。如果是,则判断count是否超过阈值,如果超过阈值,则返回限流拒绝。如果不是,则把count重置为1。
计数器限流算法存在的问题是当跨窗口的时间范围内的统计数据超出阈值,比如T-1m时候请求为100,T+1m是请求也是100,相当于2分钟内请求达到200,出现请求毛刺现象,显然是不满足限流的要求的。为解决这个问题,产生了滑动窗口限流算法。
2.2 滑动窗口限流算法
滑动窗口限流算法是一种基于窗口的限流方案,它通过维护一个滑动窗口来限制请求的处理速率。该算法的基本思路是将请求按照时间顺序分配到不同的窗口中,每个窗口内的请求数量可以根据实际需求进行调整。当某个窗口内的请求数量达到限制值时,限制请求的处理速率或拒绝请求。流量经过滑动时间窗口算法整形之后,可以保证任意时间窗口内,都不会超过最大允许的限流值,从流量曲线上来看会更加平滑,可以部分解决上面提到的临界突发流量问题。
实现滑动窗口限流算法的步骤如下:
- 初始化请求计数器count,窗口大小size,以及滑动窗口window。
- 接收一个请求,计算请求的时间delta,将请求分配到相应的窗口中。
- 更新窗口大小size,将window向右移动一个位置,并重新计算窗口内的请求数量。
- 如果请求数量超过限制值,则限制请求的处理速率或拒绝请求。
如图所示,如果在临界收到100个请求,在下一个窗口来临时又接收新的请求过来,但是整个滑动窗口内的请求已经达到上线,不再接受新的请求。
2.3 漏桶限流算法
漏桶(Leaky Bucket)限流算法可以理解为注水漏水的过程,往漏桶中以任意速率流入水,以固定的速率流出水。当水超过桶的容量时,会被溢出,也就是被丢弃。因为桶容量是不变的,保证了整体的速率。漏桶是将请求放入桶中,如果桶满了,后面来的数据包将会被丢失。
- 流入的水滴,可以看作是访问系统的请求,这个流入速率是不确定的;
- 桶的容量一般用来表示系统所能处理的请求数。如果桶的容量满了,也就达到限流的阀值,会丢弃水滴(即:拒绝请求);
- 流出的水滴,是恒定速率的,用来表示服务按照固定的速率处理请求。
漏桶限流算法的主要步骤:
- 初始化:定义桶的容量和漏桶的流出速率。
- 漏桶缓存:当有请求到达时,如果桶的容量未达到上限,直接接受该请求;否则,将该请求放入漏桶中等待处理。
- 处理漏桶中的请求:定时从漏桶中取出待处理的请求,以恒定的速率进行处理,直到漏桶为空。
- 外部请求处理:如果外部请求数量超过了系统的处理能力,需要对请求进行限流,防止系统过载。
- 启动时流量平缓:如果想让系统有一个陆续启动的过程,则不用一下子接受太多请求,让系统处理请求量平缓上升到最大处理能力。
需要注意的是,漏桶算法的优点是可以保证处理速度恒定,缺点是无法应对突发流量。因此,通常需要结合其他算法使用,以实现更灵活和高效的限流策略。
2.4 令牌桶限流算法
令牌桶(Token Bucket)算法是对漏桶算法的一种改进,不仅能够平滑限流,还允许一定程度的流量突发。
- 令牌:只有获得到令牌的Request请求才会被处理,其他Request请求要么排队要么被直接丢弃
- 桶:用来装令牌的地方,所有Request都从这个桶里获取令牌
令牌桶限流算法的实现主要包括以下步骤:
- 定义令牌桶的容量和令牌的释放速率:令牌桶的容量和令牌的释放速率表示流量控制的强度。令牌桶的容量代表桶中最多能存储的令牌数量,令牌的释放速率代表每单位时间(如每秒)释放的令牌数。可以根据实际需求调整令牌桶的容量和令牌的释放速率。
- 往令牌桶中添加令牌:如果有流量需要发送或请求,需要先到令牌桶中获取令牌。如果令牌桶中还有令牌,则流量可以被发送或请求。如果令牌桶已经达到桶的容量,则新的流量将被拒绝。
- 维护令牌桶中的令牌数量:为了实现动态调整流量控制强度,需要定时往令牌桶中添加新的令牌。同时,也需要定时检查令牌桶中的令牌数量,并将超过一定数量的令牌丢弃。
- 对拒绝的请求或数据流量进行处理:当拒绝新的请求或数据流量时,需要采取一些措施来处理被拒绝的请求或数据流量。例如,可以返回超时错误或重试机制等。
令牌桶限流算法的优点包括可以应对突发流量和公平性,即所有请求或数据流量都按照相同的速率被控制,不会出现有时通过很快、有时很慢的情况。缺点在于需要对令牌的数量和释放速率进行动态调整,以适应不同的网络环境和流量特征。因此,与漏桶限流算法相比,令牌桶限流算法更适用于复杂的流量控制场景。
3、分布式限流实现方案
3.1 使用Nginx实现
Nginx可以使用类似限流器的模块实现分布式限流,可以在每个进程或子进程上限制每个连接的访问速率或者控制并发连接数。下面是使用Nginx和ratelimit模块实现分布式限流的简单示例:
1) 控制速率
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
- $binary_remote_addr表示通过remote_addr这个标识来做限制,“binary_”的目的是缩写内存占用量,是限制同一客户端ip地址。
- zone=mylimit:10m表示生成一个大小为10M,名字为one的内存区域,用来存储访问的频次信息。
- rate=2r/s表示允许相同标识的客户端的访问频次,这里限制的是每秒2次,还可以有比如30r/m的。
2)控制并发连接数
#统一在http域中进行配置
#限制请求
limit_req_zone $uri zone=api_read:20m rate=50r/s;
#按ip配置一个连接 zone
limit_conn_zone $binary_remote_addr zone=perip_conn:10m;
#按server配置一个连接 zone
limit_conn_zone $server_name zone=perserver_conn:100m;
===== server =====
location / {
if (!-e $request_filename){
rewrite ^/(.*) /index.php last;
}
#请求限流排队通过 burst默认是0
limit_req zone=api_read burst=100;
#连接数限制,每个IP并发请求为50
limit_conn perip_conn 50;
#服务所限制的连接数(即限制了该server并发连接数量)
limit_conn perserver_conn 200;
#连接限速
#limit_rate 100k;
}
限流配置参数如下:
- rate=50r/s:每秒新增50个令牌
- burst=100:令牌桶一共有100个令牌
- perip_conn 50:每个IP最多并发50个连接
- perserver_conn 200:限制该server并发连接数
3.2 使用Redis实现
1)Redis+Lua脚本实现限流
Redis+Lua脚本可用于实现高并发和高性能的流量限制
-- 获取调用脚本时传入的第一个key值(用作限流的 key)
local key = KEYS[1]
-- 获取调用脚本时传入的第一个参数值(限流大小)
local limit = tonumber(ARGV[1])
-- 获取当前流量大小
-- (redis.call方法,从缓存中get和key相关的值,如果为null那么就返回0)
local curentLimit = tonumber(redis.call('get', key) or "0")
-- 判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0
-- 如果未超过,那么该key的缓存值+1,并设置过期时间,并返回缓存值+1
-- 是否超出限流(如果超出限流大小,直接返回)
if curentLimit + 1 > limit then
return 0
else
redis.call("INCRBY", key, 1) -- 没有超出 value + 1(请求数+1)
redis.call("EXPIRE", key, 2) -- 设置过期时间(设置2秒过期)
return 1 -- 放行请求
end
2)Redis实现令牌桶算法
每次访问请求时都可以从Redis获取令牌,当令牌桶中没有可用令牌时,请求则被拒绝。详细流程如下:
- 初始化令牌桶:在Redis中创建一个新键,例如“rate_limiter:myapp”,该键的值应该是一个有序集合。
- 添加令牌:在Redis中使用INCRBY命令对“rate_limiter:myapp”键的值进行自增,例如INCRBY 1。这将在有序集合中添加一个令牌。
- 获取令牌:在Redis中使用GET命令获取“rate_limiter:myapp”键的值,例如GET “rate_limiter:myapp”。这将返回有序集合中的令牌数量。
- 判断是否限流:如果获取的令牌数量大于等于令牌桶容量,则表示令牌桶已满,需要限流。否则,表示令牌桶未满,可以继续处理请求。
- 限流处理:如果需要限流,在Redis中使用LPOP命令从有序集合的左侧弹出一个令牌,例如LPOP “rate_limiter:myapp”。这将从有序集合中删除一个令牌,并返回被删除的令牌。在限流处理中,可以将被删除的令牌与客户端请求一起返回,以告诉客户端需要等待多长时间才能再次发送请求。
- 添加过期时间:为了防止因为Redis宕机而导致令牌桶中的令牌一直增加而没有上限,需要为有序集合添加一个过期时间。可以将过期时间设置为限流速率(即每秒添加的令牌数),例如“rate_limiter:myapp”键的过期时间可以设置为1秒。这样,即使Redis宕机,每隔1秒也会有一个新的令牌添加到有序集合中,从而保证有序集合中的令牌数量不会超过令牌桶容量。
- 重新初始化:如果需要重新开始限流,可以使用DEL命令删除“rate_limiter:myapp”键,并重新初始化令牌桶。
3.3 使用Spring Cloud Gateway实现
Spring cloud gateway提供了一个自实现的限流过滤器RequestRateLimiterGatewayFilterFactory,这个过滤器里面有两个参数:一个是KeyResolver,还有一个是RateLimiter,采用的是令牌桶算法实现。
1)配置Redis和限流信息
- id: server2
uri: lb://eureka-server1
predicates:
- Path=/server1/test
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@hostAddrKeyResolver}"
redis-rate-limiter.replenishRate: 1 # 令牌桶填充的速率 秒为单位
redis-rate-limiter.burstCapacity: 1 # 令牌桶总容量
redis-rate-limiter.requestedTokens: 1 # 每次请求获取的令牌数
配置参数:令牌生成的速率是1/s,令牌桶的总容量也为1,每次请求获取一个令牌。也就是一秒只允许一次请求。
参考资料:
- https://blog.csdn.net/tanwei010915/article/details/122489227
- https://blog.csdn.net/qq_38584262/article/details/108273978
- https://blog.csdn.net/kansting/article/details/126560538
- https://blog.csdn.net/tanwei010915/article/details/122489227