高并发之限流设计
前言介绍
高并发系统保护的三把利器 : 缓存、降级和限流
缓存
缓存的目的是提升系统访问速度和增大系统处理容量降级
降级是当服务出现问题或者影响到核心流程时,需要暂时屏蔽掉,待高峰或者问题解决后再打开限流
限流的目的是通过对并发访问/请求进行限速,或者对一个时间窗口内的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务、排队或等待、降级等处理
限流方式
-
限制总并发数(比如数据库连接池、线程池)
-
限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发连接数)
-
限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率)
-
限制远程接口调用速率
-
限制MQ的消费速率。
-
可以根据网络连接数、网络流量、CPU或内存负载等来限流
限流算法
常用的限流算法有: 漏桶算法和令牌桶算法, 还有简单粗暴的计数器。
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
令牌桶vs漏桶
令牌桶限制的是平均流入速率,允许突发请求,并允许一定程度突发流量
漏桶限制的是常量流出速率,从而平滑突发流入速率
限流实现
Google Guava框架提供了,Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现,不过相对不够灵活,下面简单介绍下个人自己实现的限流工具
设计初衷
一般系统发展迭代过程都是先以功能开发为主,后期考虑的才是性能,因此如何在原有系统上无代码侵入的情况下实现接口限流,是我本次设计实现限流工具的主要目标。基于目前主流的Java Web开发都在使用Spring全家桶,很容易想到利用SpringAOP的方式来达到想要的效果。
实现介绍
利用面向切面编程的思想,通过自定义注解的方式在无代码侵入的情况下实现系统方法级别的限流,目前主要实现了一下几种方式:
-
基于内存实现计数器限流 CounterRateLimiter
-
基于Guava SmoothBursty 的封装GuavaRateLimiter
-
基于Redis实现计数器限流 RedisCounterRateLimiter
-
基于Redis实现令牌桶限流 RedisTokenBucketRateLimiter
-
未完待续…
限流的类型主要支持:
全部限制ALL, 基于IP限制, 基于登录用户名限制和用户自定义限制(如:用户组,用户id)
限流接口定义
public interface RateLimiter {
public void rateLimit(String key, long limit, long refreshInterval,
long tokenBucketTimeInterval, long tokenNumber);
public String algorithm();
}
开启限流功能注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(EnableRateLimiterConfiguration.class)
@Documented
@Inherited
public @interface EnableRateLimiter {
}
方法限流注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MethodRateLimiter {
/**
*
* @return RateLimitType 限流类型。默认值:ALL。可选值:ALL,IP,USER,CUSTOM
*/
public RateLimitType type() default RateLimitType.ALL;
public RateLimitAlgorithm algorithm() default RateLimitAlgorithm.REDIS_COUNTER;
/**
*
* @return 限流次数。默认值10
*/
public long limit() default 10;
/**
*
* @return 限流时间间隔,以秒为单位。默认值60
*/
public long refreshInterval() default 60;
/**
* 向令牌桶中添加数据的时间间隔,以秒为单位。默认值10秒
*/
public long tokenBucketTimeInterval() default 10;
/**
* 每次为令牌桶中添加的令牌数量。默认值5个
*/
public long tokenNumber() default 5;
}
MethodRateLimiter注解切面类
@Component
@Aspect
public class MethodRateLimiterAspect {
@Pointcut("@annotation(methodRateLimiter)")
public void annotationPointcut(MethodRateLimiter methodRateLimiter) {
}
@Before("@annotation(methodRateLimiter)")
public void doBefore(JoinPoint joinPoint, MethodRateLimiter methodRateLimiter) {
String key = RateLimiterUtil.getKey(methodRateLimiter.algorithm.getName(),
joinPoint,methodRateLimiter.type());
RateLimiterFactory.getRateLimiter(methodRateLimiter.algorithm().getName()).rateLimit(
key,
methodRateLimiter.limit(),
methodRateLimiter.refreshInterval(),
methodRateLimiter.tokenBucketTimeInterval(),
methodRateLimiter.tokenNumber()
);
}
}
使用方式
springboot项目yml配置redis信息
server:
port: 8888
spring:
redis:
host: 192.168.4.131
port: 6379
password: WzQ9oWgx
timeout: 2000
jedis:
pool:
max-active: 8
max-wait: 1
max-idle: 8
min-idle: 0
开启限流功能
@SpringBootApplication
@EnableRateLimiter
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
}
测试方法限流
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/testMethodRedisCounter")
@MethodRateLimiter(refreshInterval = 30, limit = 3, algorithm = RateLimitAlgorithm.REDIS_COUNTER)
public String testMethodRedisCounter(){
return "testMethodRedisCounter";
}
@RequestMapping("/testMethodCounter")
@MethodRateLimiter(refreshInterval = 30, limit = 3, algorithm = RateLimitAlgorithm.COUNTER)
public String testMethodCounter(){
return "testMethodCounter";
}
@RequestMapping("/testIpMethodCounter")
@MethodRateLimiter(type = RateLimitType.IP, refreshInterval = 30, limit = 3, algorithm = RateLimitAlgorithm.COUNTER)
public String testIpMethodCounter(){
return " testIpMethodCounter";
}
@RequestMapping("/testMethodRedisTokenBucket")
@MethodRateLimiter(tokenBucketTimeInterval = 10, tokenNumber = 3, algorithm = RateLimitAlgorithm.REDIS_TOKEN_BUCKET)
public String testMethodRedisTokenBucket(){
return "testMethodRedisTokenBucket";
}
@RequestMapping("/testMethodGuava")
@MethodRateLimiter(algorithm = RateLimitAlgorithm.GUAVA, tokenNumber = 3)
public String testMethodGuava(){
return "testMethodGuava";
}
}
源码地址
https://github.com/lyplyp/ratelimiter
上述为个人实现和自测仅供参考,有问题欢迎交流,后续也会继续完善(准备继续实现漏桶限流算法, 封装ratelimiter-spring-boot-starter ,结合springcloud-zuul实现网关限流, 利用jmeter进行模拟测试, 完善相关使用说明等等)