SpringCloud 10 —— 网关扩展之Zuul 限流

举个例子,在突发性高并发如双十一抢购时,有时会返回一个类似“当前拥挤”的提示。其实这是淘宝为了保护自己的服务而做的限流,防止应用挂掉后产生更严重的后果。
再比如,一些在北上广等上班的同学早上乘地铁的时候7点半到8点半也会被限流,正常会开三个门的通道,这段时间只会开通一个,排队通过,这是缓解地铁站台及列车的压力。
常见的限流方式,比如Hystrix适用线程池隔离,超过线程池的负载,走熔断的逻辑。在一般应用服务器中,比如tomcat容器也是通过限制它的线程数来控制并发的;常见的限流纬度有比如通过Ip来限流、通过uri来限流、通过用户访问频次来限流。
一般限流都是在网关这一层做,比如Nginx、Openresty、kong、Zuul、Spring Cloud Gateway等。

分布式系统架构的利器:缓存、负载、限流、降级

常见的限流算法

计数器法

简单的做法是维护一个单位时间内的 计数器,每次请求计数器加1,当单位时间内计数器累加到大于设定的阈值,则之后的请求都被拒绝,直到单位时间已经过去,再将 计数器 重置为零。此方式有个弊端:如果在单位时间1s内允许100个请求,在10ms已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”。

常用的更平滑的限流算法有两种:漏桶算法 和 令牌桶算法。下面介绍下二者。

漏桶算法

漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。

可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率

令牌桶算法

令牌桶算法 和漏桶算法 效果一样但方向相反的算法,更加容易理解。随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100,则间隔是 10ms)往桶里加入 Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了。新请求来临时,会各自拿走一个 Token,如果没有 Token 可拿了就阻塞或者拒绝服务。

令牌桶的另外一个好处是可以方便的改变速度。一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。

简单实现

新建项目模块cloud-gateways-zuul-extend,引入依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        <dependency>
            <groupId>group.group.zhouning</groupId>
            <artifactId>cloud-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

启动类

@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class ZuulExtendApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulExtendApplication.class, args);
    }
}

应用配置

配置应用的基本信息(端口、应用名、eureka、redis、zuul),其中redis的连接信息需要修改为自己的,我这里为了防止连接信息暴露,将连接信息配置在环境变量中了,可以通过下图所示设置,网关转发到cloud-ribbon-server上面,方便观察结果

server:
  port: 8680
spring:
  application:
    name: gateways-zuul-extend
  redis:
    host: ${redis.host} #配置的redis地址
    password: ${redis.pwd} #配置的redis密码
    port: 6379
eureka:
  client:
    service-url:
      defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka
zuul:
  routes:
    ribbon-server:
      path: /ribbon/**

基于Guava

新建限流过滤器

接下来进入正题,我们先尝试用Guava提供的限流方法,两句代码即可实现单个应用的限流。为了演示方便,这里将速度限制在每秒10次,这里生产推荐使用Apollo、Nacos等分布式配置中心,并从中读取,可以使修改数据不用重启应用。

/**
 * 基于guava单节点限流
 */
public class LmitFilter extends ZuulFilter {
    //可以修改为基于配置中心的方式 限流10
    private static volatile RateLimiter rateLimiter= RateLimiter.create(10.0);

    public LmitFilter(){
        super();
    }

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run(){
        rateLimiter.acquire();
        return null;
    }
}

创建配置类
把过滤器加入到配置

@Configuration
public class ZuulConfig {
    @Bean
    public LmitFilter lmitFilter(){
        return new LmitFilter();
    }
}

启动项目进行压测

image.png

压测工具我们用Apache的ab压测,下载地址

Windows系统下,在apache安装目录的bin目录下,运行命令行

(注意如果有前面测试用的休眠,注释掉)

./ab -n 10000 -c 50 http://127.0.0.1:8680/ribbon/user/1

参数说明:
-n : 发出10000个请求
-c : 模拟50并发,相当50人同时访问
后面是测试url

如图,当我们限流10和限流1000时明显差异
10限流

1000

可以看到在,限流速率为10的情况下需要用时999.029秒(自己测试时建议100,不然太慢了。。。或者不要发这么多包),而限流速率为1000的情况下需要用时9.231秒,可以看到限流的效果是显著的。

限流实现基于Redis

基于redis 可以实现对整个集群的限流,Redis我们可以根据需要部署层单例、哨兵、集群均可。而且集群所承载的并发能力很高。我们的实现思路就是对每秒钟的请求次数加1,超过我们设置的阀值就返回提示信息。

添加maven依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

redis的连接信息上面的配置中已经写了(可以自己在对应地方写入redis配置)

Redis 配置类

我们需要创建一个支持Long值存储的bean,实现整数的每次+1,我们需要的key 是String型的序列化,value 是Long型的。

@Configuration
public class RedisConfig {

    @Autowired
    RedisConnectionFactory redisConnectionFactory;

    @Bean(name = "longRedisTemplate")
    public RedisTemplate<String,Long> redisTemplate(){
        RedisTemplate<String,Long> redisTemplate=new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericToStringSerializer<>(Long.class));
        redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Long.class));
        return redisTemplate;
    }
}

创建过滤器

这里我们设置了基于Redis的限流速率为100,并且给定了为0,并且设置的缓存的时间为1000秒,到期自动清除,针对每秒请求的数据对Redis的key +1,当操作我们配置的速率就会返回当前访问量较大,请稍后重试,这种提示在很多网站都有,现在我们可以自己实现。如果Redis发生异常,在使用Guava的限流。

/**
基于Redis的限流过滤
*/
@Slf4j
public class LmitFilterCluster extends ZuulFilter {

    //但节点限流
    private static volatile RateLimiter rateLimiter= RateLimiter.create(10.0);
    //集群的限流速度
    private static final long LIMIT_RATE_CLUSTER=100L;
    //初始值
    private static final long LIMIT_INIT_VALUE=0L;
    //设置ContentType类型
    public static final String APPLICATION_JSON_CHARSET_UTF8 = "application/json;charset=utf8";
    //每次限流缓存时间
    private static final int LIMIT_CACHE_TIME=1000;

    @Autowired
    @Qualifier("longRedisTemplate")
    private RedisTemplate<String,Long> redisTemplate;
    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run(){
        RequestContext ctx = RequestContext.getCurrentContext();
        long currentSecond = System.currentTimeMillis() / 1000;
        String key="cloud-zuul-extend:"+"limit:"+currentSecond;
        try{
            if(!redisTemplate.hasKey(key)){
                redisTemplate.opsForValue().set(key,LIMIT_INIT_VALUE,LIMIT_CACHE_TIME, TimeUnit.SECONDS);
            }
            Long increment = redisTemplate.opsForValue().increment(key, 1);
            log.info(increment.toString());
            if(increment>=LIMIT_RATE_CLUSTER){
                ctx.setSendZuulResponse(false);
                //失败之后通知后续不应该执行了
                ctx.set("isShould",false);
                ctx.setResponseBody(JSONUtil.toJsonStr(ApiResult.failedMsg("当前访问量较大,请稍后重试")));
                ctx.getResponse().setContentType(APPLICATION_JSON_CHARSET_UTF8);
                return null;
            }

        }catch (Exception e){
            log.error("LmitFilterCluster exception:{}",e);
            rateLimiter.acquire();
        }
        return null;
    }
}

修改前面的Config配置类,把原来的LmitFilter改成Redis的LmitFilterCluster 。

@Configuration
public class ZuulConfig {

//    /**
//     * 限流单体版
//     * @return
//     */
//    @Bean
//    public LmitFilter lmitFilter(){
//        return new LmitFilter();
//    }

    /**
     * 限流集群版
     * @return
     */
    @Bean
    public LmitFilterCluster lmitFilterCluster(){
        return new LmitFilterCluster();
    }
}

启动项目,我们和前面一样进行压测。

ab -n 1000 -c 50 http://127.0.0.1:8680/ribbon/user/1

可以看到只走了Redis限流,没有Guava限流,在Redis中也可以看到具体的计数值。

更新时间:2020-05-04 15:03:05

本文由 寻非 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.zhouning.group/archives/springcloud10网关扩展之zuul限流
最后更新:2020-05-04 15:03:05

评论

Your browser is out of date!

Update your browser to view this website correctly. Update my browser now

×