本文记录了笔者在平时工作中自接到任务后做的需求调研、技术选型、分析设计、代码实现等一系列过程,总结记录下来作为积累,亦可为有需要的参考下。
为啥要做网关限流
随着业务不断的增长,用户大幅增加,现有的硬件设施短时间内无法到位,系统优化已做过一轮又一轮,为保证系统健康稳定的运行,考虑在如下几种情形下该如何处理呢?
- 某天请求量大增,且都是商户的正常业务请求,超过了系统处理能力,商户请求大量超时了,怎么办?
- A商户一直在调用查询接口导致其他商户的正常业务请求大量超时,怎么办?
第一种情况是商户都是正常业务请求,但因为同时请求的商户太多了,导致请求量太大,超出了系统处理能力导致请求超时。这种情况下需要确认系统的TPS(QPS)、系统吞吐量等一些系统性能健康指标,然后将请求控制在安全指标范围内即可,关于如何确认这些系统指标,网上有许多资料,本文不在此详述。
第二种情况是因为某个商户A占用了大量的请求资源导致其他商户的请求大量超时,这时应该对商户A的请求做限制,不让他过多的占用宝贵的请求资源,将其占用的让给其他商户请求使用。
网关限流选型
确认需求后就要考虑要怎么做了。有没有现成的工具可以直接拿来用啊?等等诸如此类的一些想法。
但首先,基于之前的分析,我们希望我们的限流具备的功能有:
- 能够控制系统总请求量
- 能够控制某个商户A的总请求量
- 能够控制某个接口(如交易查询接口)的总请求量
- 能够控制商户A的某个接口(如交易查询接口)的总请求量
- 能够控制其他自定义维度的总请求量,如某个请求IP进来的总请求量
带着这些目的,查找相关资料,最终有三个解决方案/工具可供选择。如下:
- Guava的RateLimiter基于令牌桶算法的本地限流工具。其适用于单服务限流,多台集群服务限流不准确。
- Alibaba的Sentinel分布式系统的流量防卫兵,1.4.0 开始引入了集群流控模块,之前版本也是基于本地限流的模式。1.4.0之前会有流量不均导致总体限流效果不佳的问题。假设集群中有 10 台机器,我们给每台机器设置单机限流阈值为 10 QPS,理想情况下整个集群的限流阈值就为 100 QPS。不过实际情况下流量到每台机器可能会不均匀,会导致总量没有到的情况下某些机器就开始限流。因此仅靠单机维度去限制的话会无法精确地限制总体流量。(在选型时,1.4.0版本还未发布)
- 基于Redis + Lua的分布式限流模式开发。Redis + Lua模式是个高性能、原子性的,所以其支持集群模式下的精确限流。
具体对比如下表:
解决方案 | 特点 | 适用场景 |
---|---|---|
Guava的RateLimiter | 使用令牌桶算法,即一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝。当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不到,该请求就要被限流,要么直接丢弃,要么在缓冲区等待。可以实现平滑突发限流和平滑预热限流。 | 支持单机限流,不支持集群限流 |
Alibaba的Sentinel | 分布式服务架构的高可用流量防护组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。 | 1.4.0之前版本支持单机限流,集群总体限流效果不佳;1.4.0及之后版本支持单机限流和集群限流 |
基于Redis + Lua的分布式限流模式 | 高性能、原子性的流量累加计数判断限流模式 | 支持单机限流,支持集群限流 |
因我们的系统是集群部署的,且在选型时Sentinel系统的1.4.0版本还未发布,故我们选择了第三种方案,即基于Redis + Lua的分布式限流模式开发限流功能。现在,Sentinel系统已经发展得比较完善了,而且不单单只有限流,还有其他如流量整形、熔断降级、系统负载保护、热点防护等多个维度来防护系统,是个比较合适的选择。我们在选型时,应首选现有的比较成熟易用的组件。这里主要是记录整理当时的整个过程。
限流系统设计演进
技术选型完成之后就到系统设计了。第一版设计是为网关限流量身定制的(数据库表字段是按照网关的参数设置的),之后评审时觉得应该可以用到其他系统的限流,但这版设计肯定是不符合要求了,后面再设计一版通过了。
- V0版本——为网关接口限流量身打造
网关限流配置表如下(BIZ_SYS、PARTNER_ID和SERVICE为联合唯一索引):
名称 | 描述 | 字段类型 | 长度 | 是否主键 | 是否必填 |
---|---|---|---|---|---|
ID | 主键 | VARCHAR2 | 32 | 是 | 是 |
BIZ_SYS | 业务系统,规则配置使用的系统 | VARCHAR2 | 16 | 否 | 是 |
PARTNER_ID | 平台(商户)ID,该字段为空时取ALL字符串 | VARCHAR2 | 32 | 否 | 是 |
SERVICE | 网关接口服务名,该字段为空时取ALL字符串 | VARCHAR2 | 64 | 否 | 是 |
PERMITS | 许可数(限流值) | NUMBER | 否 | 是 | |
LIMIT_SECONDS | 限流时长(单位秒),默认值1 | NUMBER | 10 | 否 | 是 |
MEMO | 备注 | VARCHAR2 | 128 | 否 | 否 |
STATUS | 状态 1-有效,2-失效 | CHAR | 1 | 否 | 是 |
GMT_CREATE | 创建时间 | TMIESTAMP | 否 | 是 | |
CREATOR | 创建人 | VARCHAR2 | 50 | 否 | 是 |
GMT_MODIFIED | 最后修改时间 | TMIESTAMP | 否 | 是 | |
MODIFIER | 最后修改人 | VARCHAR2 | 50 | 否 | 是 |
该版因只针对网关的特殊参数字段(平台ID、服务名)做限流,其他的字段(如IP等)无法处理,且无法适用于其他系统,故须改进设计。
- V1版本——支持自定义限流规则,满足多维度限流要求
TP_TRAFFIC_RULE_TYPE_CONF(限流规则类型配置表)如下:
名称 | 描述 | 字段类型 | 长度 | 是否主键 | 是否必填 |
---|---|---|---|---|---|
RULE_TYPE_CODE | 规则类型编码 | VARCHAR2 | 16 | 是 | 是 |
BIZ_SYS | 业务系统,规则配置使用的系统 | VARCHAR2 | 16 | 否 | 是 |
RULE_TYPE_NAME | 规则类型名称 | VARCHAR2 | 64 | 否 | 是 |
RULE_FACTOR | 规则要素,规则使用的字段名,多个以英文,分开,如指定商户指定接口服务名的规则要素为partnerId,service | VARCHAR2 | 64 | 否 | 否 |
MEMO | 备注 | VARCHAR2 | 128 | 否 | 否 |
STATUS | 状态 1-有效,2-失效 | CHAR | 1 | 否 | 是 |
GMT_CREATE | 创建时间 | TMIESTAMP | 否 | 是 | |
CREATOR | 创建人 | VARCHAR2 | 50 | 否 | 是 |
GMT_MODIFIED | 最后修改时间 | TMIESTAMP | 否 | 是 | |
MODIFIER | 最后修改人 | VARCHAR2 | 50 | 否 | 是 |
TP_TRAFFIC_RULE_CONF(网关限流配置表)如下(BIZ_SYS、RULE_TYPE_CODE、RULE_FACTOR_VALUE为联合唯一索引):
名称 | 描述 | 字段类型 | 长度 | 是否主键 | 是否必填 |
---|---|---|---|---|---|
ID | 主键 | VARCHAR2 | 32 | 是 | 是 |
BIZ_SYS | 业务系统,规则配置使用的系统 | VARCHAR2 | 16 | 否 | 是 |
RULE_TYPE_CODE | 规则类型编码 | VARCHAR2 | 16 | 否 | 是 |
RULE_FACTOR_VALUE | 配置具体限流规则要素值,json字符串,如指定商户a指定接口服务b的规则要素值为{“partnerId”:“a”,“service”:“b”} | VARCHAR2 | 256 | 否 | 是 |
PERMITS | 许可数(限流值) | NUMBER | 否 | 是 | |
LIMIT_SECONDS | 限流时长(单位秒),默认值1 | NUMBER | 10 | 否 | 是 |
MEMO | 备注 | VARCHAR2 | 128 | 否 | 否 |
STATUS | 状态 1-有效,2-失效 | CHAR | 1 | 否 | 是 |
GMT_CREATE | 创建时间 | TMIESTAMP | 否 | 是 | |
CREATOR | 创建人 | VARCHAR2 | 50 | 否 | 是 |
GMT_MODIFIED | 最后修改时间 | TMIESTAMP | 否 | 是 | |
MODIFIER | 最后修改人 | VARCHAR2 | 50 | 否 | 是 |
该版可根据不同限流要求配置对应限流规则以达到限流目的。也满足其他系统接入限流功能需求。
基于配置规则的限流配置思路:
如需要控制网关查询接口(接口编码为query)允许每秒100个请求,并且a商户的网关查询接口允许每秒5个请求,该如何设置?
Step1.配置限流规则类型:并将规则类型以接入的业务系统BIZ_SYS的维度缓存在redis中,如网关接入的缓存key=gateway,限流规则类型配置表记录如下:
RULE_TYPE_CODE | BIZ_SYS | RULE_TYPE_NAME | RULE_FACTOR | MEMO | STATUS |
---|---|---|---|---|---|
spclService | gateway | 指定接口规则类型 | service | 指定接口规则类型 | 1 |
spclSerMerchant | gateway | 指定接口指定商户规则类型 | service,partnerId | 指定接口指定商户规则类型 | 1 |
Step2.配置限流规则:并将限流规则配置以{BIZ_SYS}{RULE_TYPE_CODE}{依次遍历拼接RULE_FACTOR_VALUE}做为key放入redis缓存起来供后续累加计数使用。
如下配置redis的缓存key依次为:
gateway_spclService_query、
gateway_spclSerMerchant_query_null(这里因partnerId未配置值所以取null,该配置为spclSerMerchant规则类型query接口服务的默认配置)、
gateway_spclSerMerchant_query_a
ID | BIZ_SYS | RULE_TYPE_CODE | RULE_FACTOR_VALUE | PERMITS | LIMIT_SECONDS | STATUS |
---|---|---|---|---|---|---|
1 | gateway | spclService | {“service”:“query”} | 100 | 1 | 1 |
2 | gateway | spclSerMerchant | {“service”:“query”} | 2 | 1 | 1 |
3 | gateway | spclSerMerchant | {“service”:“query”,“partnerId”:“a”} | 5 | 1 | 1 |
上表中3条限流规则分别表示:
1.gateway系统接入的spclService规则类型匹配规则要素字段为service=query时,限定流量为每秒100个请求;
2.gateway系统接入的spclSerMerchant规则类型匹配规则要素字段为service=query且未找到指定partnerId值配置时的默认配置,限定流量为每秒2个请求;
3.gateway系统接入的spclSerMerchant规则类型匹配规则要素字段为service=query且partnerId=a时,限定流量为每秒5个请求。
其中,对于配置1和2的区别再做下说明。
配置1的限流规则类型为spclService,该类型只配置了一个要素字段service,所以该配置是针对接口服务名service这个字段做的一个总限流配置,即不管是哪个商户的请求,只要是调query接口服务的,都要累加到这个规则中做限流判断。
而对于配置2的限流规则类型为spclSerMerchant,该类型配置了两个要素字段,即service和partnerId,而配置2中只有service=query,partnerId字段配置没有,这表示的是一条指定接口指定商户的默认规则配置,当未找到针对该商户的配置时则使用该配置。例如,b商户也同样调用query接口,因找不到针对b商户的spclSerMerchant类型规则配置,故其使用配置2这条规则。同理,若c商户也同样调用query接口时也使用配置2这条规则。那有同学就问了,这不是和配置1一样了吗?都用在配置2这条规则上了。答案是否定的。看是使用了同一个规则,时则它是借用该配置,复制一个副本到自己的规则配置中!b商户调用query接口的配置2这条规则在redis的缓存key为gateway_spclSerMerchant_query_b,而c商户调用query接口的配置2这条规则在redis的缓存key为gateway_spclSerMerchant_query_c,所以针对b、c商户调用query接口的限流计算也是分开独立计算的。
注:这里的所有配置都会立即更新到redis中,以保证配置是最新的。
限流逻辑流程
1.通过业务系统gateway查询到有效的限流规则类型列表
2.根据列表个数开启多线程执行限流逻辑
3.拼装限流规则key去redis查找限流规则,拼装格式为{BIZ_SYS}{RULE_TYPE_CODE}{依次遍历拼接RULE_FACTOR_VALUE}。其中,BIZ_SYS和RULE_TYPE_CODE在1.中的规则类型有,RULE_FACTOR_VALUE值拼装则是根据限流服务公布出去的接口Map参数获取的,按规则类型表的RULE_FACTOR中的配置顺序依次获取拼装成redis的限流规则key。
如a商户调用query接口,通过gateway查询到配置了两条有效的限流规则类型spclService和spclSerMerchant,再开启2个线程分别处理限流逻辑,其中spclService的RULE_FACTOR配置了service字段则拼装的查找redis限流规则key为gateway_spclService_query,spclSerMerchant的RULE_FACTOR配置了service,partnerId字段则拼装的查找redis限流规则key为gateway_spclSerMerchant_query_a。
4.调用redis LUA脚本计算是否限流
5.合并多线程计算限流的结果返回调用系统
限流代码
接入限流的业务系统调用限流方法如下:
/**
* 限流服务接口
*
*/
public interface RateLimitFacade {
/**
* 限流判断方法
*
* @param bizSys 限流请求的业务系统编码,如gateway
* @param rateLimitReq 限流请求map
* @return 限流结果,当限流时success=false并返回限流对应的resultCode、unityResultCode及错误信息,未限流时success=true并且resultCode、unityResultCode=S0001
*/
ModelResult<String> rateLimit(String bizSys, Map<String,String> rateLimitReq);
}
限流实现类:
/**
* @ClassName RateLimitFacadeImpl
* @Description 限流服务接口实现
*
**/
@Service
@Component
@Slf4j
public class RateLimitFacadeImpl implements RateLimitFacade {
@Resource
private RateLimitCoreService rateLimitCoreService;
@Override
public ModelResult<String> rateLimit(String bizSys, Map<String, String> rateLimitReq) {
try {
//请求对象参数校验
Assert