基于RateLimiter和Redis+Lua实现的限流组件

这是我参与8月更文挑战的第6天,活动详情查看: 8月更文挑战

限流的目的是防止恶意请求流量、恶意攻击,或者防止流量超出系统峰值。限流有很多种方案,分布在不同层,原则上是限制流量穿透到应用层。但是服务端限流也是不可或缺的,下面介绍的是我自己实现的一个可用于生成实践的简易版服务端限流方案。代码地址:基于RateLimiter和Redis+Lua实现的限流组件

令牌桶算法

令牌桶算法是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。令牌桶算法基本可以用下面的几个概念来描述:

  • 令牌将按照固定的速率被放入令牌桶中。比如每秒放10个;
  • 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
  • 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
  • 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。

如下图:

令牌桶算法.png

令牌算法是根据放令牌的速率去控制输出的速率,也就是上图的to network的速率。to network我们可以理解为消息的处理程序,执行某段业务或者调用某个RPC。

实时更新

本组件是使用Nacos作为配置中心,动态更新流控配置。

配置参数

/**
 * 吞吐率:每秒允许请求数
 */
private Double permitsPerSecond;

/**
 * 预热时间:从较低吞吐率到高吞吐率的耗时时间
 */
private Long warmupPeriod;

/**
 * 是否同步等待
 */
private Boolean sync;

/**
 * 超时时间
 */
private Long timeout;

/**
 * 校验模式 必填
 * 单机和全局[standalone, global]
 */
private String model;

/**
 * 优先级 非必填
 * 从大到小排列,大值优先级高
 * 比如同时设置了多个校验规则,包括单机和全局,则可以选择先校验全局还是先校验单机
 */
private Integer sort = 0;

/**
 * 参数名 用于动态流控时根据参数名获取参数值
 */
private List<String> paramNames;
复制代码

实现

整个代码结构如下:

代码结构.png

下面主要介绍annotation、controller和handler几个包。

annotation

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {

    /**
     * 配置中心key
     */
    String remoteKey();
}
复制代码

注解很简单,就一个远程key,用于获取远程nacos远程配置。

controller

这个包下面放的是流控控制器的实现和工厂类。

/**
 * 流控控制器
 */
public interface RateLimitController {

    /**
     * 执行流控
     * @return
     */
    boolean check();
}
复制代码

StandAloneRateLimitController和GlobalRateLimitController实现了这个接口,分别对应单机流控和全局流控。

public interface ControllerFactory {

    /**
     * 根据不同配置创建不同的流控器
     * @param key remote key
     * @param rateLimitConfig 配置 信息
     * @return
     */
    RateLimitController getInstance(String key, RateLimitConfig rateLimitConfig);
}
复制代码

工厂类可以根据不同配置创建不同的流控器,这里提供了默认的实现支持上面两种,也可以自己扩展实现。

handler

public interface RateLimitHandler {

    /**
     * 执行流控
     * @param key key for nacos
     * @param rateLimitConfigs 流控配置,支持多个
     * @param params 请求参数,支持根据某一个或多个参数动态流控
     * @return
     */
    boolean doCheck(String key, List<RateLimitConfig> rateLimitConfigs, Map<String, Object> params);
}
复制代码

流控处理类也比较简单,就是根据工厂类和流控配置拿到具体的流控控制器实例,然后执行流控。这里有两个注意的地方,一个是动态流控,一个是可能存在的OOM问题。

动态流控

流控支持按某个维度进行动态流控,比如用户或者IP等,在配置的paramNames属性中可以配置具体的参数列表,后台根据获取到的参数值动态拼接key,以此实现动态流控。

OOM问题

对接口中的动态参数进行流控,事先并不知道会有多少个动态参数,但要对每个参数都构造限流器进行跟踪。如果参数过多,会导致内存问题(OOM)。

解决思路:动态参数的限流器需要“过期”,某段时间不用后就让其自动过期,释放内存。使用guava的LoadingCache存储动态参数的限流器,设置限流器过期时间;使用LRU算法淘汰那些“最近最少使用的”限流器,防止大量动态参数撑爆内存。

测试

在项目里已添加了测试项目,只需要修改配置启动即可。当然在这之前你需要启动nacos并添加配置。配置参考如下:

{
    "test":[
        {
            "permitsPerSecond":10,
            "warmupPeriod":10,
            "sync":false,
            "timeout":1000,
            "sort": 0,
            "model":"standalone"
        },
		{
            "permitsPerSecond":100,
            "model":"global",
            "paramNames":["user"],
            "sort":1
        }
    ]
}
复制代码

使用JMeter来简单测试下(如果你有兴趣可以自己添加个定时器,现象会更明显):

JMeter.png