过去的两个月项目有点忙,确实没有闲下来写点东西,难得空闲下来,就介绍一下之前写的一个流控组件。它基于令牌桶算法实现,其中单机流控基于Guava的Ratelimiter,全局流控基于Redis+Lua实现,使用阿里开源的Nacos作为配置中心。
代码地址
令牌桶算法
令牌桶算法是一个存放固定容量令牌(token)的桶,按照固定速率往桶里添加令牌。令牌桶算法基本可以用下面的几个概念来描述:
- 令牌将按照固定的速率被放入令牌桶中。比如每秒放10个;
- 桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
- 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
- 如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
如下图:
令牌算法是根据放令牌的速率去控制输出的速率,也就是上图的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;
实现
整个代码结构如下:
下面主要介绍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算法淘汰那些“最近最少使用的”限流器,防止大量动态参数撑爆内存。
总结
整个代码逻辑不算复杂,思路也比较简单,已经上传了完整的实现代码和demo。作为前两年工作的总结吧。