关键资源
关键资源总是有限的,也就意味着处理能力也有限,所以当面对大量业务时,为了保障自己能够有序的提供服务最经济的做法就是限制同一时间处理的事务数。比如银行的工作人员,一个工作人员同时只能为一个客户服务,来多了根本处理不了,不光是一种浪费而且有可以造成混乱的局面导致工作人员无法工作。
网络请求漏斗
越上层的服务器处理的事务越轻,应付请求的能力也越强,也就意味着同一请求越上层处理时间越短。为了有效的保护下层服务器,就需要对发送给下层的请求量做限流,在下层服务器可接受的范围内。否则就可能会出现下层服务器资源耗尽而无法正常提供服务的情况。
限流场景
服务端限流
如果在服务端做限流,无论有多少个客户端,总的提供能力是固定的(感谢@ xuanbg提出的评论,指出服务端也可以对客户端做精准的判断,后续我再想想实现方案),所以不会因为客户端数量过多而导致资源不足,因为处理不过来的请求会被阻塞等待获取资源。
缺点
缺点也比较明显,由于服务提供者整体设置了最大限流数,此时所有的客户端共享同一份限流数据,那么有可能导致有的服务能分配到资源有些服务请求分配不到资源导致无法请求的情况。
客户端限流
客户端限流解决上服务端限流提到的问题,它能保证每个客户端都能得到响应。但是从其它方面考虑,必须针对不同的客户端做不同的限流策略:
- 请求量大,但时效性不高,此时将限流数控制小一些会比较合适
- 请求量大,但时效性高,此时将限流数适当调高
- 响应时间长,即慢接口,适当降低
- 主流业务,核心业务,适当调高
- 非主流业务,适当降低
- ......
缺点
-
如果客户端的数量不固定,那么有可能导致客户端数量过多造成大量请求打到服务端导致处理不了的结果,所以需要严格监控客户端的调用情况。
-
配置复杂,需要针对每个客户端做相对精准的判断
RPC实现
限流
这里指的限流是指每秒从客户端提交到服务端的请求数量。
过滤器机制可参考:简易RPC框架-过滤器机制
服务引用注解上增加限流
public @interface RpcReference {
boolean isSync() default true;
/**
* 客户端最大并发数
* @return
*/
int maxExecutesCount() default 10;
}
创建动态代理时将限流参数传递到服务端
需要修改RpcProxy类,构造函数中增加服务引用注解参数,然后在invoke方法中从服务引用注解中获取限流参数传递给request对象。
public RpcProxy(Class<T> clazz,ReferenceConfig referenceConfig,RpcReference reference) {
this.clazz = clazz;
this.referenceConfig=referenceConfig;
this.reference=reference;
this.isSync=reference.isSync();
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
...
if (this.reference != null) {
request.setMaxExecutesCount(this.reference.maxExecutesCount());
}
...
}
RpcInvocation增加限流参数
public interface RpcInvocation {
...
int getMaxExecutesCount();
}
AbstractInvoker修改buildRpcInvocation方法
从request对象中获取限流参数,传递给RpcInvocation对象。
public RpcInvocation buildRpcInvocation(RpcRequest request){
RpcInvocation rpcInvocation=new RpcInvocation() {
...
@Override
public int getMaxExecutesCount() {
return request.getMaxExecutesCount();
}
};
return rpcInvocation;
}
AccessLimitFilter
- 修改令牌管理器
按接口分配令牌管理器,令牌管理器存储在map中共享。如果未初始化则进行令牌管理器的初始化,如果已经初始化则直接申请令牌。
static class AccessLimitManager{
private final static Object lock=new Object();
private final static Map<String,RateLimiter> rateLimiterMap= Maps.newHashMap();
public static void acquire(RpcInvocation invocation){
if(!rateLimiterMap.containsKey(invocation.getClassName())) {
synchronized (lock) {
if(!rateLimiterMap.containsKey(invocation.getClassName())) {
final RateLimiter rateLimiter = RateLimiter.create(invocation.getMaxExecutesCount());
rateLimiterMap.put(invocation.getClassName(), rateLimiter);
}
}
}
else {
RateLimiter rateLimiter=rateLimiterMap.get(invocation.getClassName());
rateLimiter.acquire();
}
}
}
- 修改invoke方法
将invocation参数传递给acquire方法。
public Object invoke(RpcInvoker invoker, RpcInvocation invocation) {
logger.info("before acquire,"+new Date());
AccessLimitManager.acquire(invocation);
Object rpcResponse=invoker.invoke(invocation);
logger.info("after acquire,"+new Date());
return rpcResponse;
}
客户端
- 服务引用配置限流
这里配置每秒一个请求
@RpcReference(maxExecutesCount = 1)
private ProductService productService;
- 执行结果
如下图所示,每次请求相隔了一秒,达到了限流请求的目的。
待完善
- 支持方法级限流
以上只支持客户端接口级别的限流配置,可以再单独创建一个方法级的注解来配置相关参数。
- 支持服务端限流
服务端限流尽管有它的缺点,但为了更好的保护服务提供者,需要结合多种业务场景来配合客户端限流一起完善,取长补短共同发挥作用。
本文源码
https://github.com/jiangmin168168/jim-framework
文中代码是依赖上述项目的,如果有不明白的可下载源码