
**QPS(TPS):**每秒钟,request/事务的数量
并发数: 系统同时处理的request/事务数
响应时间: 一般取平均响应时间
单体应用中,QPS(TPS)= 并发数/平均响应时间
一个系统吞吐量通常由QPS(TPS)、并发数两个因素决定,每套系统这两个值都有一个相对极限值,在应用场景访问压力下,只要某一项达到系统最高值,系统的吞吐量就上不去了,如果压力继续增大,系统的吞吐量反而会下降,原因是系统超负荷工作,上下文切换、内存等等其它消耗导致系统性能下降。
tomcat配置中默认配置的最大请求数是 150,也就是说同时支持 150 个并发。
(BIO模式下)由此可见,当客户端请求连接达到150并发以后,请求不会被socket接受,而是进入TCP的完全连接队列中,队列的大小由acceptCount值决定,默认是100;而当队列大于acceptCount值时,则报“connection refused”错误。
再比如,如今项目都是服务化,如果你的系统理论是时间单位内可服务10W用户,但是却突然来了100W用户,由于用户流量的随机性,如果不加以限流,很有可能这100W用户一下子就压垮了系统,导致所有人都得不到服务,即,服务不可用。
所以说:限流是解决高并发大流量的一种方案,至少是可以保证应用的可用性。
二、服务限流怎么做对系统服务进行限流,一般有如下几个模式:
1、熔断这个模式是需要系统在设计之初,就要把熔断措施考虑进去。当系统出现问题时,如果短时间内无法修复,系统要自动做出判断,开启熔断开关,拒绝流量访问,避免大流量对后端的过载请求。系统也应该能够动态监测后端程序的修复情况,当程序已恢复稳定时,可以关闭熔断开关,恢复正常服务。
2、服务降级例如,在电商平台中,如果突发流量激增,可临时将商品评论、积分等非核心功能进行降级,停止这些服务,释放出机器和CPU等资源来保障用户正常下单,而这些降级的功能服务可以等整个系统恢复正常后,再来启动,进行补单/补偿处理。
即,将系统的所有功能服务进行一个分级,当系统出现问题,需要紧急限流时,将不是那么重要的功能进行降级处理,停止服务,这样可以释放出更多的资源供给核心功能的去用。
除了功能降级以外,还可以采用不直接 *** 作数据库,而全部读缓存、写缓存的方式作为临时降级方案。
这个模式需要在系统的前端设置一个流量缓冲池,将所有的请求全部缓冲进这个池子,不立即处理。然后后端真正的业务处理程序从这个池子中取出请求依次处理,常见的可以用队列模式来实现。
4、特权处理这个模式需要将用户进行分类,通过预设的分类,让系统优先处理需要高保障的用户群体,其它用户群的请求就会延迟处理或者直接不处理。
4.1、熔断技术熔断的技术可以重点参考Netflix的开源组件hystrix的做法,主要有三个模块:熔断请求判断算法、熔断恢复机制、熔断报警。
Hystrix服务熔断
系统维护一个计数器,来一个请求就加1,请求处理完成就减1,当计数器大于指定的阈值,就拒绝新的请求。
基于这个简单的方法,可以再延伸出一些高级功能,比如阈值可以不是固定值,是动态调整的。另外,还可以有多组计数器分别管理不同的服务,以保证互不影响等。
就是基于FIFO队列,所有请求都进入队列,后端程序从队列中取出待处理的请求依次处理。基于队列的方法,也可以延伸出更多的玩法来,比如可以设置多个队列以配置不同的优先级。
比如:审核单排队,尽管业务单再不停的提交待审核单,审核系统接受到以后分别进入对应的队列。然后通过分单、抢单机制对审核单FIFO。
首先还是要基于一个队列,请求放到队列里面。但除了队列以外,还要设置一个令牌桶,另外有一个脚本以持续恒定的速度往令牌桶里面放令牌,后端处理程序每处理一个请求就必须从桶里拿出一个令牌,如果令牌拿完了,那就不能处理请求了。我们可以控制脚本放令牌的速度来达到控制后端处理的速度,以实现动态流控。
三、限流典型算法 1、漏桶算法
漏桶算法非常简单,就是将流量放入桶中并按照一定的速率流出。如果流量过大时候并不会提高流出效率,而溢出的流量也只能是抛弃掉了。这种算法很简单,但也非常粗暴,无法应对突发的大流量。
令牌桶算法是按照恒定的速率向桶中放入令牌,每当请求经过时则消耗一个或多个令牌。当桶中的令牌为 0 时,请求则会被阻塞。
特殊场景下(创建下单,订单支付等,但是查询除外)一定的时间内,如果是参数是完全一样的,可以理解为重复下单。为了防止有些动态参数,比如,访问接口的带的参数时间戳,在技术设计的时候需要动态过滤某些参数。
2、核心技术实现并不是所有接口都需要防止重复,比如分页列表,数据详情等查询接口。所以“防止重复提交”技术需要动态的引用。由于服务系统分布式部署,需要考虑请求的随机性。
所以技术栈方面采用AOP注解、Redis。
基于方法级别的注解,可选择性使用。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DuplicateLimit {
int maxCount();
int seconds();
}
3.2、基于AOP
1、对方法入参进行验证,区分是GET和POST请求,因为参数的获取不一样
2、参数可以选择性过滤一些动态参数,以及生成对应的md5摘要
3、通过注解信息获取该方法单位时间内,同一个请求的最大并发数
4、基于Reids *** 作该请求参数的md5摘要,判断是否可以继续处理流程
@Aspect
@Slf4j
@Component
public class DuplicateLimitAspect {
@Resource
private RedisTemplate redisTemplate;
final String GET = "GET";
final String POST = "POST";
final String[] excludeKeys = new String[]{"timestamp", "submitTime"};
@Pointcut("@annotation(com.xiu.core.boot.access.DuplicateLimit)")
public void limitAccess() {
}
@Around("limitAccess()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
//防止MQ消息
if (Objects.isNull(attributes)) {
return joinPoint.proceed(args);
}
HttpServletRequest request = attributes.getRequest();
//获取拦截的方法相关信息
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Object target = joinPoint.getTarget();
//为了获取注解信息
Method currentMethod = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
//获取注解信息
DuplicateLimit duplicateLimit = currentMethod.getAnnotation(DuplicateLimit.class);
//判断控制器方法参数中是否有RequestBody注解
Annotation[][] annotations = currentMethod.getParameterAnnotations();
boolean isRequestBody = isRequestBody(annotations);
String redisKey;
//请求方法
String requestMethod = request.getMethod();
//请求参数
String requestParam = StringUtils.EMPTY;
if (GET.equals(requestMethod)) {
JSonObject param = RequestHelperUtil.getRequestParams(request);
requestParam = JSONUtil.toJsonStr(param);
}
if (POST.equals(requestMethod) && isRequestBody) {
Object param = RequestHelperUtil.postRequestParams(args);
requestParam = JSONUtil.toJsonStr(param);
}
// 限流策越,根据package和方法名称组成Key(默认)
if (StringUtils.isEmpty(requestParam)) {
String packageName = (methodSignature.getMethod().getDeclaringClass()).getName();
redisKey = packageName + "_API->" + methodSignature.getName();
}
redisKey = RequestHelperUtil.reqParamMd5(requestParam, excludeKeys);
//最大次数
int maxLimit = duplicateLimit.maxCount();
//多长时间的最大次数
int time = duplicateLimit.seconds();
Integer limit = redisTemplate.opsForValue().get(redisKey);
//第一次进入
if (null == limit) {
redisTemplate.opsForValue().set(redisKey, 1, time, TimeUnit.SECONDS);
return joinPoint.proceed();
}
//次数+1
if (limit < maxLimit) {
redisTemplate.opsForValue().set(redisKey, (limit + 1), time, TimeUnit.SECONDS);
return joinPoint.proceed();
}
log.info("当前[{}]请求超出设定的访问次数限制seconds:{},maxLimit:{},请稍后再试... ...", redisKey, time, maxLimit);
return ResultVo.fail(RespStatusEnum.ACCESS_LIMIT.getMsg());
}
protected boolean isRequestBody(Annotation[][] annotations) {
boolean isRequestBody = false;
for (Annotation[] annotationArray : annotations) {
for (Annotation annotation : annotationArray) {
if (annotation instanceof RequestBody) {
isRequestBody = true;
break;
}
}
}
return isRequestBody;
}
}
3.3、用例
定义一个3秒最多1次请求的并发接口
@OperationLog(value = "防重放POST", type = LogOperationEnum.ADD)
@ApiOperation(value = "防重放接口POST")
@PostMapping("/accessLimit/post")
@DuplicateLimit(maxCount = 1, seconds = 3)
public ResultVo accessLimitPost(@RequestBody ParamVo paramVo) {
return ResultVo.success(paramVo.getUsername());
}
通过swagger接口,连续点击
[learn-demo_8B3FB6BD9A9049919D2136800ADEDD3A] [2021-10-20 17:15:28.804] [http-nio-8080-exec-2] [INFO ] [xiu.core.boot.log.OperationLogAspect:88 ] - param:{"ip":"0:0:0:0:0:0:0:1","path":"/test/accessLimit/post","requestMethod":"POST","param":{"id":9521,"username":"顶楼大叔","timestamp":201110201714343},"costTime":"9ms"},
[learn-demo_F7F1B0C717174F7EA955AD3A8E1604EF] [2021-10-20 17:15:30.599] [http-nio-8080-exec-3] [INFO ] [x.c.boot.access.DuplicateLimitAspect:117 ] - 当前[67CD8F30F90BF78813FD922FC03B60C9]请求超出设定的访问次数限制seconds:3,maxLimit:1,请稍后再试... ...
[learn-demo_4AD0CD6474934082AA5D0803821D2868] [2021-10-20 17:15:31.158] [http-nio-8080-exec-4] [INFO ] [x.c.boot.access.DuplicateLimitAspect:117 ] - 当前[67CD8F30F90BF78813FD922FC03B60C9]请求超出设定的访问次数限制seconds:3,maxLimit:1,请稍后再试... ...
[learn-demo_859B4E8FC0424CF4B9D9FB0403139D53] [2021-10-20 17:15:31.844] [http-nio-8080-exec-5] [INFO ] [xiu.core.boot.log.OperationLogAspect:88 ] - param:{"ip":"0:0:0:0:0:0:0:1","path":"/test/accessLimit/post","requestMethod":"POST","param":{"id":9521,"username":"顶楼大叔","timestamp":201110201714343},"costTime":"0ms"},
[learn-demo_127E7C586921404A86D7C9DCF4F545CC] [2021-10-20 17:15:32.547] [http-nio-8080-exec-7] [INFO ] [x.c.boot.access.DuplicateLimitAspect:117 ] - 当前[67CD8F30F90BF78813FD922FC03B60C9]请求超出设定的访问次数限制seconds:3,maxLimit:1,请稍后再试... ...
[learn-demo_2E73FE86A80143888AB4960BC2808AE4] [2021-10-20 17:15:33.081] [http-nio-8080-exec-6] [INFO ] [x.c.boot.access.DuplicateLimitAspect:117 ] - 当前[67CD8F30F90BF78813FD922FC03B60C9]请求超出设定的访问次数限制seconds:3,maxLimit:1,请稍后再试... ...
[learn-demo_7FE4BBE80CD4481CB202EA9CE2D7D97B] [2021-10-20 17:15:33.563] [http-nio-8080-exec-8] [INFO ] [x.c.boot.access.DuplicateLimitAspect:117 ] - 当前[67CD8F30F90BF78813FD922FC03B60C9]请求超出设定的访问次数限制seconds:3,maxLimit:1,请稍后再试... ...
五、令牌桶算法技术方案实现
1、采用Guava的RateLimiter
限速器,从概念上讲,速率限制器以可配置的速率分发许可。如有必要,每个请求都会封锁,直到获得许可证,然后再进行封锁。一旦获得许可,无需发放许可证。
@Beta
@GwtIncompatible
@ElementTypesAreNonnullByDefault
public abstract class RateLimiter {
//创建一个限速器
public static RateLimiter create(double permitsPerSecond) {
return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}
//获取一个许可证
public boolean tryAcquire() {
return tryAcquire(1, 0, MICROSECONDS);
}
}
2、基于AOP
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimit {
}
@Aspect
@Slf4j
@Component
public class RateLimitAspect {
private RateLimiter rateLimiter = RateLimiter.create(5);
@Pointcut("@annotation(com.xiu.core.boot.access.RateLimit)")
public void serviceRateLimit() {
}
@Around("serviceRateLimit()")
public Object around(ProceedingJoinPoint joinPoint) {
boolean flag = rateLimiter.tryAcquire();
Object object = null;
try {
if (flag){
//返回数据
object = joinPoint.proceed();
return object;
}
} catch (Throwable e) {
log.error("rateLimit error...",e);
}
log.info("当前请求超出设定的访问次数,请稍后再试... ...");
return ResultVo.fail(RespStatusEnum.ACCESS_LIMIT.getMsg());
}
}
3、用例
定义一个1秒只能接受6个并发的接口
@OperationLog(value = "Guava 限流", type = LogOperationEnum.OTHER)
@ApiOperation(value = "Guava 限流接口")
@GetMapping("/rateLimit")
@RateLimit
public ResultVo rateLimit(String username) {
return ResultVo.success(username);
}
通过ab并发测试,定义1秒发10个并发
ab -c1 -n10 http://localhost:8080/test/rateLimit
Benchmarking localhost (be patient).....done
Server Software:
Server Hostname: localhost
Server Port: 8080
document Path: /test/rateLimit
document Length: 69 bytes
Concurrency Level: 1
Time taken for tests: 0.314 seconds
Complete requests: 10
Failed requests: 0
Total transferred: 1880 bytes
HTML transferred: 690 bytes
Requests per second: 31.84 [#/sec] (mean)
Time per request: 31.409 [ms] (mean)
Time per request: 31.409 [ms] (mean, across all concurrent requests)
Transfer rate: 5.85 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 3 31 84.9 4 273
Waiting: 3 30 83.4 4 268
Total: 3 31 84.9 5 273
Percentage of the requests served within a certain time (ms)
50% 5
66% 5
75% 5
80% 6
90% 273
95% 273
98% 273
99% 273
100% 273 (longest request)
可以看到系统日志
[learn-demo_CE23AF8A092B4CA5AA8366FFF20DDF6B] [2021-10-20 17:34:18.998] [http-nio-8080-exec-1] [INFO ] [xiu.core.boot.log.OperationLogAspect:88 ] - param:{"ip":"0:0:0:0:0:0:0:1","path":"/test/rateLimit","requestMethod":"GET","costTime":"12ms"},
[learn-demo_D1C817B6395C426C8D276397B8869290] [2021-10-20 17:34:19.040] [http-nio-8080-exec-2] [INFO ] [xiu.core.boot.log.OperationLogAspect:88 ] - param:{"ip":"0:0:0:0:0:0:0:1","path":"/test/rateLimit","requestMethod":"GET","costTime":"0ms"},
[learn-demo_3F5532D217A442AC84F22F182693293D] [2021-10-20 17:34:19.045] [http-nio-8080-exec-3] [INFO ] [xiu.core.boot.log.OperationLogAspect:88 ] - param:{"ip":"0:0:0:0:0:0:0:1","path":"/test/rateLimit","requestMethod":"GET","costTime":"0ms"},
[learn-demo_F227185A2B3742DABB236F64D978436D] [2021-10-20 17:34:19.050] [http-nio-8080-exec-4] [INFO ] [xiu.core.boot.log.OperationLogAspect:88 ] - param:{"ip":"0:0:0:0:0:0:0:1","path":"/test/rateLimit","requestMethod":"GET","costTime":"0ms"},
[learn-demo_CC977910928348A1A67110AE87CBC8D8] [2021-10-20 17:34:19.054] [http-nio-8080-exec-5] [INFO ] [xiu.core.boot.log.OperationLogAspect:88 ] - param:{"ip":"0:0:0:0:0:0:0:1","path":"/test/rateLimit","requestMethod":"GET","costTime":"0ms"},
[learn-demo_57B06E775DEE464BA6810CC5AC850872] [2021-10-20 17:34:19.058] [http-nio-8080-exec-6] [INFO ] [xiu.core.boot.log.OperationLogAspect:88 ] - param:{"ip":"0:0:0:0:0:0:0:1","path":"/test/rateLimit","requestMethod":"GET","costTime":"0ms"},
[learn-demo_21B5C462AAD04AC2B7FFF065FF682C5F] [2021-10-20 17:34:19.062] [http-nio-8080-exec-7] [INFO ] [xiu.core.boot.access.RateLimitAspect:45 ] - 当前请求超出设定的访问次数,请稍后再试... ...
[learn-demo_130CC801FCEA4C0CB49A7F1E99B1ED7A] [2021-10-20 17:34:19.065] [http-nio-8080-exec-8] [INFO ] [xiu.core.boot.access.RateLimitAspect:45 ] - 当前请求超出设定的访问次数,请稍后再试... ...
[learn-demo_5F7017BB14A94420AF5A6EE3B2EA02A3] [2021-10-20 17:34:19.068] [http-nio-8080-exec-9] [INFO ] [xiu.core.boot.access.RateLimitAspect:45 ] - 当前请求超出设定的访问次数,请稍后再试... ...
[learn-demo_F8430465AAB8469991434C5375E9A940] [2021-10-20 17:34:19.072] [http-nio-8080-exec-10] [INFO ] [xiu.core.boot.access.RateLimitAspect:45 ] - 当前请求超出设定的访问次数,请稍后再试... ...
推荐文章:
Hystrix服务熔断
基于swagger的knife4j
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)