万字带你搞懂Java架构-限流技术设计

本文详细探讨了限流在微服务架构中的重要性,从dubbo和springcloud服务治理模式出发,分析了dubbo的框架级限流、线程池限流和集成第三方组件的限流策略。接着,介绍了springcloud中的hystrix和sentinel限流组件。此外,文章还讨论了网关层限流的必要性。在限流策略部分,讲解了令牌桶、漏桶和滑动时间窗口算法。最后,提供了基于guava、sentinel和redis+lua的限流实现方案,并展示了如何自定义starter实现限流。
摘要由CSDN通过智能技术生成

一、背景

限流对于一个微服务架构系统来说具有非常重要的意义,否则其中的某个微服务将成为整个系统隐藏的雪崩因素,为什么这么说?

举例来讲,某个SAAS平台有100多个微服务应用,但是作为底层的某个或某几个应用来说,将会被所有上层应用频繁调用,业务高峰期时,如果底层应用不做限流处理,该应用必将面临着巨大的压力,尤其是那些个别被高频调用的接口来说,最直接的表现就是导致后续新进来的请求阻塞、排队、响应超时...最后直到该服务所在JVM资源被耗尽。

二、限流概述

在大多数的微服务架构在设计之初,比如在技术选型阶段,架构师会从一个全局的视角去规划技术栈的组合,比如结合当前产品的现状考虑是使用dubbo?还是springcloud?作为微服务治理的底层框架。甚至为了满足快速的上线、迭代和交付,直接以springboot为基座进行开发,后续再引入新的技术栈等...

所以在谈论某个业务场景具体的技术解决方案时不可一概而论,而是需要结合产品和业务的现状综合评估,以限流来说,在下面的不同的技术架构下具体在选择的时候可能也不一样。

2.1 dubbo 服务治理模式

选择dubbo框架作为基础服务治理对于那种偏向内部平台的应用还是不错的,dubbo底层走netty,这一点相比http协议来说,在一定场景下还是具有优势的,如果选择dubbo,在选择限流方案上可以做如下的参考。

2.1.1 dubbo框架级限流

dubbo官方提供了完善的服务治理,能够满足大多数开发场景中的需求,针对限流这个场景,具体来说包括如下手段,具体的配置,可以参考官方手册;

客户端限流

  • 信号量限流 (通过统计的方式)
  • 连接数限流 (socket->tcp)

服务端限流

  • 线程池限流 (隔离手段)
  • 信号量限流 (非隔离手段)
  • 接收数限流 (socket->tcp)
2.1.2 线程池设置

多线程并发操作一定离不开线程池,Dubbo自身提供了支持了四种线程池类型支持。生产者<dubbo:protocol>标签中可配置线程池关键参数,线程池类型、阻塞队列大小、核心线程数量等,通过配置生产端的线程池数量可以在一定程度上起到限流的效果。

2.1.3 集成第三方组件

如果是springboot框架的项目,可以考虑直接引入地方的组件或SDK,比如hystrix,guava,sentinel原生SDK等,如果技术实力足够强甚至可以考虑自己造轮子。

2.2 springcloud 服务治理模式

如果你的服务治理框架选用的是springcloud或springcloud-alibaba,其框架自身的生态中已经包含了相应的限流组件,可以实现开箱即用,下面列举几种常用的基于springcloud框架的限流组件。

2.2.1 hystrix

Hystrix是Netflix开源的一款容错框架,在springcloud早期推出市场的时候,作为springcloud生态中用于限流、熔断、降级的一款组件。

Hystrix提供了限流功能,在springcloud架构的系统中,可以在网关启用Hystrix,进行限流处理,每个微服务也可以各自启用Hystrix进行限流。

Hystrix默认使用线程隔离模式,可以通过线程数+队列大小进行限流,具体参数配置可以参考官网相关资料。

2.2.2 sentinel

Sentinel 号称分布式系统的流量防卫兵,属于springcloud-alibaba生态中的重要组件,面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。

2.3 网关层限流

随着微服务规模的增加,整个系统中很多微服务都需要实现限流这种需求时,就可以考虑在网关这一层进行限流了,通常来说,网关层的限流面向的是通用的业务,比如那些恶意的请求,爬虫,攻击等,简单来说,网关层面的限流提供了一层对系统整体的保护措施。

三、常用限流策略

3.1 限流常用的算法

不管是哪种限流组件,其底层的限流实现算法大同小异,这里列举几种常用的限流算法以供了解。

3.1.1 令牌桶算法

令牌桶算法是目前应用最为广泛的限流算法,顾名思义,它有以下两个关键角色:

  • 令牌 :获取到令牌的Request才会被处理,其他Requests要么排队要么被直接丢弃;
  • 桶 :用来装令牌的地方,所有Request都从这个桶里面获取令牌

令牌桶主要涉及到2个过程,即令牌的生成,令牌的获取

3.1.2 漏桶算法

漏桶算法的前半段和令牌桶类似,但是操作的对象不同,结合下图进行理解。

令牌桶是将令牌放入桶里,而漏桶是将访问请求的数据包放到桶里。同样的是,如果桶满了,那么后面新来的数据包将被丢弃。

3.1.3 滑动时间窗口

根据下图,简单描述下滑动时间窗口这种过程:

  • 黑色大框为时间窗口,可以设定窗口时间单位为5秒,它会随着时间推移向后滑动。我们将窗口内的时间划分为五个小格子,每个格子代表1秒钟,同时这个格子还包含一个计数器,用来计算在当前时间内访问的请求数量。那么这个时间窗口内的总访问量就是所有格子计数器累加后的数值;
  • 比如说,我们在每一秒内有5个用户访问,第5秒内有10个用户访问,那么在0到5秒这个时间窗口内访问量就是15。如果我们的接口设置了时间窗口内访问上限是20,那么当时间到第六秒的时候,这个时间窗口内的计数总和就变成了10,因为1秒的格子已经退出了时间窗口,因此在第六秒内可以接收的访问量就是20-10=10个;

滑动窗口其实也是一种计算器算法,它有一个显著特点,当时间窗口的跨度越长时,限流效果就越平滑。打个比方,如果当前时间窗口只有两秒,而访问请求全部集中在第一秒的时候,当时间向后滑动一秒后,当前窗口的计数量将发生较大的变化,拉长时间窗口可以降低这种情况的发生概率

四、通用限流实现方案

抛开网关层的限流先不说,在微服务应用中,考虑到技术栈的组合,团队人员的开发水平,以及易维护性等因素,一个比较通用的做法是,利用AOP技术+自定义注解实现对特定的方法或接口进行限流,下面基于这个思路来分别介绍下几种常用的限流方案的实现。

4.1 基于guava限流实现

guava为谷歌开源的一个比较实用的组件,利用这个组件可以帮助开发人员完成常规的限流操作,接下来看具体的实现步骤。

4.1.1 引入guava依赖

版本可以选择更高的或其他版本

 

xml

复制代码

<dependency>     <groupId>com.google.guava</groupId>     <artifactId>guava</artifactId>     <version>23.0</version> </dependency>

4.1.2 自定义限流注解

自定义一个限流用的注解,后面在需要限流的方法或接口上面只需添加该注解即可;

 

java

复制代码

import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;   @Target(value = ElementType.METHOD) @Retention(value = RetentionPolicy.RUNTIME) public @interface RateConfigAnno {       String limitType();       double limitCount() default 5d; }

4.1.3 限流AOP类

通过AOP前置通知的方式拦截添加了上述自定义限流注解的方法,解析注解中的属性值,并以该属性值作为guava提供的限流参数,该类为整个实现的核心所在。

 

java

复制代码

import com.alibaba.fastjson2.JSONObject; import com.google.common.util.concurrent.RateLimiter; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes;   import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Method; import java.util.Objects;   @Aspect @Component public class GuavaLimitAop {       private static Logger logger = LoggerFactory.getLogger(GuavaLimitAop.class);       @Before("execution(@RateConfigAnno * *(..))")     public void limit(JoinPoint joinPoint) {         //1、获取当前的调用方法         Method currentMethod = getCurrentMethod(joinPoint);         if (Objects.isNull(currentMethod)) {             return;         }         //2、从方法注解定义上获取限流的类型         String limitType = currentMethod.getAnnotation(RateConfigAnno.class).limitType();         double limitCount = currentMethod.getAnnotation(RateConfigAnno.class).limitCount();         //使用guava的令牌桶算法获取一个令牌,获取不到先等待         RateLimiter rateLimiter = RateLimitHelper.getRateLimiter(limitType, limitCount);         boolean b = rateLimiter.tryAcquire();         if (b) {             System.out.println("获取到令牌");         }else {             HttpServletResponse resp = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();             JSONObject jsonObject=new JSONObject();             jsonObject.put("success",false);             jsonObject.put("msg","限流中");             try {                 output(resp, jsonObject.toJSONString());             }catch (Exception e){                 logger.error("error,e:{}",e);             }         }     }       private Method getCurrentMethod(JoinPoint joinPoint) {         Method[] methods = joinPoint.getTarget().getClass().getMethods();         Method target = null;         for (Method method : methods) {             if (method.getName().equals(joinPoint.getSignature().getName())) {                 target = method;                 break;             }         }         return target;     }       public void output(HttpServletResponse response, String msg) throws IOException {         response.setContentType("application/json;charset=UTF-8");         ServletOutputStream outputStream = null;         try {             outputStream = response.getOutputStream();             outputStream.write(msg.getBytes("UTF-8"));         }

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值