分布式系统架构下面对突增的高并发访问请求,如何实现限流以保护系统的可用性是需要关注的一个问题。分布式限流实现机制上有很多中,包括基于网关实现、基于中间件如Redis实现等,本文简要介绍限流的常用算法以及实现方案。
所谓“限流”是在高并发业务场景下,超过系统处理能力后,为确保系统稳定性和可用性所采取的技术手段。比如12306抢火车票的验证码图片,双十一等秒杀活动下的排队处理等,各个技术厂商的限流手段也是层出不穷。在现实生活中的限流也很常见,比如高峰期的地铁限流、黄金周的景区限流,还有疫情高峰期的排队核酸等。
在分布式系统中,面对大数据量的高并发访问请求时,可能有多个服务节点协同工作,每个节点可能会处理多个请求。因此,需要一种方法来控制每个请求的访问频率,以避免过多的请求导致系统负载过高或崩溃。常用的技术手段就是限流,限流其实就是在某个时间窗口对资源访问的限制,而分布式限流就是将整个分布式环境当一个整体来考量,当达到请求数或者并发限制后,就进行等待、排队、降级或者拒绝服务等限制。
限流通常是对时间和资源两个维度的限制:1)基于特定的时间窗口比如每分钟、每秒钟限制;2)基于可用资源的限制比如设定最大访问次数或连接数。
在实际的使用场景中会基于多个规则进行限制,常见的限流规则如下:
在实际业务场景使用过程中,需要根据具体情况进行调整和优化,以确保分布式限流算法能够满足实际需求,提高系统的性能和可用性。
分布式限流的主流方案包括从用户客户端限流、服务网关层实现限流以及基于中间件或限流组件实现限流。
无论是网关层限流,还是基于中间件层的实现,具体的分布式限流算法无外乎以下几种:
计数器限流算法是一种简单而有效的限流方案,它通过为每个请求分配一个计数器,当计数器达到一定值时,限制请求的处理速率或拒绝请求。这种方案简单易实现,但可能会出现限流不够精准的情况。
实现计数器限流算法的步骤如下:
计数器限流算法存在的问题是当跨窗口的时间范围内的统计数据超出阈值,比如T-1m时候请求为100,T+1m是请求也是100,相当于2分钟内请求达到200,出现请求毛刺现象,显然是不满足限流的要求的。为解决这个问题,产生了滑动窗口限流算法。
滑动窗口限流算法是一种基于窗口的限流方案,它通过维护一个滑动窗口来限制请求的处理速率。该算法的基本思路是将请求按照时间顺序分配到不同的窗口中,每个窗口内的请求数量可以根据实际需求进行调整。当某个窗口内的请求数量达到限制值时,限制请求的处理速率或拒绝请求。流量经过滑动时间窗口算法整形之后,可以保证任意时间窗口内,都不会超过最大允许的限流值,从流量曲线上来看会更加平滑,可以部分解决上面提到的临界突发流量问题。
实现滑动窗口限流算法的步骤如下:
如图所示,如果在临界收到100个请求,在下一个窗口来临时又接收新的请求过来,但是整个滑动窗口内的请求已经达到上线,不再接受新的请求。
漏桶(Leaky Bucket)限流算法可以理解为注水漏水的过程,往漏桶中以任意速率流入水,以固定的速率流出水。当水超过桶的容量时,会被溢出,也就是被丢弃。因为桶容量是不变的,保证了整体的速率。漏桶是将请求放入桶中,如果桶满了,后面来的数据包将会被丢失。
漏桶限流算法的主要步骤:
需要注意的是,漏桶算法的优点是可以保证处理速度恒定,缺点是无法应对突发流量。因此,通常需要结合其他算法使用,以实现更灵活和高效的限流策略。
令牌桶(Token Bucket)算法是对漏桶算法的一种改进,不仅能够平滑限流,还允许一定程度的流量突发。
令牌桶限流算法的实现主要包括以下步骤:
令牌桶限流算法的优点包括可以应对突发流量和公平性,即所有请求或数据流量都按照相同的速率被控制,不会出现有时通过很快、有时很慢的情况。缺点在于需要对令牌的数量和释放速率进行动态调整,以适应不同的网络环境和流量特征。因此,与漏桶限流算法相比,令牌桶限流算法更适用于复杂的流量控制场景。
Nginx可以使用类似限流器的模块实现分布式限流,可以在每个进程或子进程上限制每个连接的访问速率或者控制并发连接数。下面是使用Nginx和ratelimit模块实现分布式限流的简单示例:
1) 控制速率
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=2r/s;
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;
}
限流配置参数如下:
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获取令牌,当令牌桶中没有可用令牌时,请求则被拒绝。详细流程如下:
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,每次请求获取一个令牌。也就是一秒只允许一次请求。
参考资料: