服务端频率控制一般有以下几种常见的方式:
一、局部频率控制
对于某一个接口I,请求频率阈值T,假设请求均匀分散到N台服务器上,每台服务器上接口I的频率阈值就是T/N,这样每台机器通过检查接口I的本地请求频率就可以做频率控制。
这种方式优点是实现简单,而且由于是本地控制,效率极高,如果流量均匀的话,频率控制也会比较实时。对于服务器配置,地理位置,路由权重一样,这种方式可以有一些使用场景。当然缺点也很明显,就是局限性比较大,没法做全局的控制,很多场景下并不能保证每台机器流量一样,另外一旦集群发生扩容/缩容,每台机器分配到的频率阈值需要额外的重新计算。
二、全局频率控制
这种方式一般会有分布式的请求频率上报,然后有一个中心化的频率控制服务汇总请求频率信息检查是否超频,在实现上又有很多种。
简易实现
基于一些Nosql系统支持的原子计数器功能,比如可以使用CKV,Redis等提供的INCR/DECR接口,汇总来自各台服务器上报的请求频率。这种方式实现上也可以有多种。
可以利用expire机制,设置每个业务key的过期时间为频率控制周期。以1分钟频率控制周期为例,伪代码如下:
代码语言:c 复制time_unit = 60
value = INCR $key
if key not exist
init_value = 0
INIT $key $init_value
EXPIRE $key $time_unit
else
if $value < $threshold
// 没有超频,放行
else
// 超频了,限制
也可以自己控制过期时间,原子计数器的value一般是64位整数,可以拆成两部分,高32位为时间部分,后32位为实际频率计数(当然这里不一定是以32位拆分,比如限制每天最大调用量,可能时间部分只需要16位,留下更多位给计数用)。以1分钟频率控制周期为例,伪代码如下:
代码语言:c 复制time_unit = 60
value = INCR $key
current_minute = current_timestamp() / $time_unit * $time_unit
if key not exist
init_value = $current_minute << 32
INIT $key $init_value
else
freq_minute = $value >> 32
freq = $value & 0xFFFFFFFF
if $current_minute != $freq_minute
// 不是当前频率周期了,重新初始化
init_value = $current_minute << 32
INIT $key $init_value
else
if $freq < $threshold
// 没有超频,放行
else
// 超频了,限制
这两种实现方式原理都差不多,能比较实时地监测到超频的情况。前一种可以设置的频率阈值范围更大,不过对nosql系统的过期机制依赖比较严重,个人相对偏好后一种方式。不过由于都需要依赖外部存储且需要经过网络,如果网络抖动以及外部存储发生故障,频率控制可能就会失效,另外由于这类方式是把业务的请求量会直接放给外部存储,对存储的性能要求也会比较高。
复杂实现
前面的几种情况都是比较简易的实现方式,可以应对大多数简单的频率控制场景。但是对于提供API接口的门户系统,前面的功能是远远不够的,所以一般都会实现一套频率控制服务,除了限频,还有结合告警,请求频率流水等形成一套完整的频控生态。
这类系统一般会在每台业务机器上部署一个agent,业务进程写入频率信息到共享内存,agent从共享内存收集再上报,后端有一个频率服务server汇总,执行频率控制策略,下发是否超频以及频率告警等。典型的架构流程如下:
这种频控方式是比较通用的一种实现,频率上报和频率检测通过共享内存解耦了,不会像原子计数器会受制于业务的请求量,频率检测也不需要经过网络,业务进程直接从本地共享内存中就能判断是否超频了,比前面基于原子计数的快。不过受制于agent的上报的实时性,决策是否超频可能会相对延迟一些。