1Zuul 限流 · SpringCloud微服务实战 · 看云

1. Zuul 限流

导航

本节代码地址


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

1.1 常见的限流算法

1.1.1 计数器

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

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

1.1.2 漏桶算法

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

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

1.1.3 令牌桶算法

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

79e8f1c5e483db3de5c249867acbced9_MD5.webp

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

1.2 新建项目

为了演示方便,我们新建一个module,后续Zuul 拓展均建在此module下
6351226c797ce55ce37f39bed5c4d4f9_MD5.webp

1.3 限流实现基于Guava

利用Guava 限流属于单应用限流,单节点限流实现较简单且方便,但是仅仅对这单个应用的节点做限制。如果要实现整个分布式系统的集群限流,就需要依赖外部中间件来实现,现在常用Redis。利用Redis也会存在一定的风险,比如缓存击穿、脑裂等问题会使限流功能失效。单个应用的限流能力非常有限,如果应用并发数不大,可以使用单应用即可,否则就要使用集群限流。

1.4 添加maven配置

我们需要Zuul、Eureka和Guava 的包,方便我们之后演示Zuul 代理转发的功能

<dependencies>
    <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>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>${guava.version}</version>
    </dependency>
    <dependency>
        <groupId>com.yisu.cloud</groupId>
        <artifactId>fw-cloud-common</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

1.5 应用配置

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

a8002e0f0918b00eeef3a6a35fc9f736_MD5.png

server:
  port: 8680
spring:
  application:
    name: fw-gateways-zuul-extend
  redis:
      host: ${redis.host}
      password: ${redis.pwd}
      port: 6379
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
zuul:
  routes:
    fw-cloud-ribbon-server:
      path: /ribbon

1.6 新建限流过滤器

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


public class LmitFilter extends ZuulFilter {
    
    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;
    }
}

1.7 新建启动类

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

这样就可以了,启动项目我们压测一下,压测工具我们用Apache的ab压测,下载地址https://www.apachehaus.com/cgi-bin/download.plx,可以到群里下载
2aeb790c4ed0a75ddf4835691922cf6b_MD5.png

在Windows系统下,打开cmd命令行窗口,定位到apache安装目录的bin目录下

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

注意: -n发出100个请求,-c模拟50并发,相当50人同时访问,后面是测试url

限流为10的情况下(RateLimiter.create(10.0))

This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname:        localhost
Server Port:            8680

Document Path:          /ribbon/user/1
Document Length:        169 bytes

Concurrency Level:      50
Time taken for tests:   100.360 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      274000 bytes
HTML transferred:       169000 bytes
Requests per second:    9.96 [#/sec] (mean)
Time per request:       5018.001 [ms] (mean)
Time per request:       100.360 [ms] (mean, across all concurrent requests)
Transfer rate:          2.67 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   0.6      1       3
Processing:    80 4815 810.3   4998    5177
Waiting:       62 4811 811.1   4994    5174
Total:         82 4817 810.3   4999    5178

Percentage of the requests served within a certain time (ms)
  50%   4999
  66%   5001
  75%   5003
  80%   5004
  90%   5006
  95%   5008
  98%   5010
  99%   5011
 100%   5178 (longest request)

限流为100的情况下(RateLimiter.create(100.0))

This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname:        localhost
Server Port:            8680

Document Path:          /ribbon/user/1
Document Length:        169 bytes

Concurrency Level:      50
Time taken for tests:   9.004 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      274000 bytes
HTML transferred:       169000 bytes
Requests per second:    111.06 [#/sec] (mean)
Time per request:       450.220 [ms] (mean)
Time per request:       9.004 [ms] (mean, across all concurrent requests)
Transfer rate:          29.72 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    1   0.7      1      10
Processing:    95  433 136.9    498     586
Waiting:       13  424 150.3    496     581
Total:         97  433 136.8    499     586

Percentage of the requests served within a certain time (ms)
  50%    499
  66%    500
  75%    500
  80%    501
  90%    502
  95%    503
  98%    506
  99%    515
 100%    586 (longest request)

可以看到在,限流速率为10 的情况下需要用时100.36s,而限流速率为10 的情况下需要用时9.004s,可以看到限流的效果是显著的。

1. 8 限流实现基于Redis

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

1.9 maven 配置添加

添加spring-boot-starter-data-redis供我们在操作Redis

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

Redis连接信息上面 1.5 已经贴出来了,连接信息只有在引入Redis相关依赖包之后才能生效。

1.10 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;
    }
}

1.11 创建过滤器

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


@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;
    
    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="fw-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(FwResult.failedMsg("当前访问量较大,请稍后重试")));
                ctx.getResponse().setContentType(APPLICATION_JSON_CHARSET_UTF8);
                return null;
            }

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

1.12 启动应用

启动之前修改一下ZuulConfig的配置,只启用集群版的。

@Configuration
public class ZuulConfig {

    





    
    @Bean
    public LmitFilterCluster lmitFilterCluster(){
        return new LmitFilterCluster();
    }
}

启动之后,我们用ab压测一下并验证结果

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

可以看到只走了Redis限流,没有Guava限流,并且执行时间只需要6s中。

This is ApacheBench, Version 2.3 <$Revision: 1843412 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests


Server Software:
Server Hostname:        localhost
Server Port:            8680

Document Path:          /ribbon/user/1
Document Length:        60 bytes

Concurrency Level:      50
Time taken for tests:   6.905 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      178000 bytes
HTML transferred:       60000 bytes
Requests per second:    144.82 [#/sec] (mean)
Time per request:       345.248 [ms] (mean)
Time per request:       6.905 [ms] (mean, across all concurrent requests)
Transfer rate:          25.17 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.6      0       5
Processing:   155  243 104.6    208    2044
Waiting:      150  236 103.2    202    1989
Total:        155  243 104.6    208    2044

Percentage of the requests served within a certain time (ms)
  50%    208
  66%    221
  75%    236
  80%    280
  90%    362
  95%    484
  98%    559
  99%    563
 100%   2044 (longest request)

Redis中也可以看到具体的计数值
f698d46a8a47a10dafd0b8f57ef25162_MD5.png

a3b7d497a7a747b6fe9f2b58ddd61310_MD5.png