系统限流实践 - 应用限流

开篇

上篇学习了限流的基本知识(传送门),接下来学习一下应用限流的方法

应用级限流

针对容器限制总并发/连接/请求数

通过对容器进行配置,限制TPS/QPS阀值,防止大量请求涌入击垮系统。

如果你使用过Tomcat,其Connector 其中一种配置有如下几个参数: 
acceptCount:如果Tomcat的线程都忙于响应,新来的连接会进入队列排队,如果超出排队大小,则拒绝连接; 
maxConnections:瞬时最大连接数,超出的会排队等待; 
maxThreads:Tomcat能启动用来处理请求的最大线程数,如果请求处理量一直远远大于最大线程数则可能会僵死 
详细的配置请参考官方文档。另外如MySQL(如max_connections)、Redis(如tcp-backlog)都会有类似的限制连接数的配置

针对接口进行并发控制

如果担心接口某个时刻并发量过大了,可以细粒度地限制每个接口的总并发/请求数 
可以使用Java得Atomic来实现

public class SimpleLimit {

     private AtomicInteger requestCount;

     public void doRequest(String threadName) {
         try {
             if (requestCount.decrementAndGet() < 0) {
                 System.out.println(threadName + ":请求过多,请稍后再尝试");
             }else {
                 System.out.println(threadName + ":您的请求已受理");
             }
         } finally {
             requestCount.incrementAndGet();
         }
     }
     ......
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

测试代码

public static void main(String[] args) throws InterruptedException, IOException {
        final SimpleLimit simpleLimit = new SimpleLimit();
        final CountDownLatch latch = new CountDownLatch(1); //保证线程同一时刻start
        simpleLimit.requestCount = new AtomicInteger(10);
        for (int i = 0; i < 50; i++) {
            final int finalI = i;
            Thread t = new Thread(new Runnable() {
                public void run() {
                    try {
                        latch.await();
                        simpleLimit.doRequest("t-" + finalI);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
        latch.countDown();
        System.in.read();
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

结果 
simple-limit结果 
可以看到,程序通过requestCount把并发数量控制在10,当有50个线程并发去访问时,就会出现有得线程能竞争资源成功,有的线程竞争资源失败 
这种并发控制方式比较简单粗暴,没有平滑处理,应用的时候要根据场景使用

针对时间窗口进行并发控制

限制住一个窗口时间内接口的请求量,例如某个基础服务调用量很大,怕被突然的大流量打挂,下面是一种实现窗口时间并发控制的方法 
我们使用Guava的Cache来存储计数器,利用秒数作为Key,Value代表这一秒有多少个请求,这样就限制了一秒内的并发数。另外过期时间设置为两秒,保证一秒内的数据是存在的。

public class TimeLimit {

    private long limit = 5; //限流数

    static volatile boolean exit;

    private LoadingCache<Long, AtomicLong> counter =
            CacheBuilder.newBuilder()
                    .expireAfterWrite(2, TimeUnit.SECONDS)
                    .build(new CacheLoader<Long, AtomicLong>() {
                        @Override
                        public AtomicLong load(Long aLong) throws Exception {
                            return new AtomicLong(0);
                        }
                    });


    public void doRequest(String threadName) throws ExecutionException {
        long currentSecond = System.currentTimeMillis() / 1000;
        if (counter.get(currentSecond).incrementAndGet() > limit) {
            System.out.println(threadName + ":请求过多,请稍后再尝试");
        } else {
            System.out.println(threadName + ":您的请求已受理");
        }
    }
    ......
 }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

测试代码

public static void main(String[] args) throws InterruptedException {
        //或等当前秒数

        final TimeLimit timeLimit = new TimeLimit();
        final CountDownLatch latch = new CountDownLatch(1);
        for (int i = 0; i < 10; i++) {
            final int finalI = i;
            Thread t = new Thread(new Runnable() {
                public void run() {
                    try {
                        latch.await();
                        while (!exit) {
                            timeLimit.doRequest("t-" + finalI);
                            Thread.sleep(1000);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (ExecutionException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
        latch.countDown();
        Thread.sleep(3000);
        exit = true;
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

结果 
simple-limit2结果 
限流数为5,并发数为10,从结果上看,当一秒内请求数到了5之后,接下来的请求都会别拒绝

平滑限流接口请求数

之前的限流方式都不能很好地应对突发请求,即瞬间请求可能都被允许从而导致一些问题;因此在一些场景中需要对突发请求进行整形,整形为平均速率请求处理(比如5r/s,则每隔200毫秒处理一个请求,平滑了速率)。 
这个时候有两种算法满足我们的场景:令牌桶和漏桶算法。Guava框架提供了令牌桶算法实现,可直接拿来使用。

基本使用-1

public class BasicUsage {
    public static void main(String[] args) {
        RateLimiter rateLimiter = RateLimiter.create(5); //令牌桶容量为5,即每200毫秒产生1个令牌
        System.out.println(rateLimiter.acquire()); //阻塞获取一个令牌
        System.out.println(rateLimiter.acquire());
        System.out.println(rateLimiter.acquire());
        System.out.println(rateLimiter.acquire());
        System.out.println(rateLimiter.acquire());
        System.out.println(rateLimiter.acquire());
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

结果

0.0 //马上得到令牌,所以等待时间为0
0.166525 //因为令牌桶每200毫秒产生1个令牌,所以上面消耗掉令牌后桶里没令牌,需要等待新的令牌产生后才能消费,下面的同理
0.167868    
0.195596
0.193168
0.128312
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果令牌桶里有令牌的话,acquire返回0。如果没令牌,则等待一段时间,当有令牌的时候消费令牌并则返回等待令牌所消耗的时间。

基本使用-2

public class BasicUsage {
    public static void main(String[] args) {
        RateLimiter rateLimiter = RateLimiter.create(5); //令牌桶容量为5,即每200毫秒产生1个令牌
        System.out.println(rateLimiter.acquire(10)); //透支令牌
        System.out.println(rateLimiter.acquire());
        System.out.println(rateLimiter.acquire());
        System.out.println(rateLimiter.acquire());
        System.out.println(rateLimiter.acquire());
        System.out.println(rateLimiter.acquire());
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

结果

0.0
1.996642
0.197655
0.19933
0.200378
0.200474
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

令牌桶允许一定程度的透支,不过接下来的请求需要等透支完的补充回来后才能继续执行。

基本使用-3

public class BasicUsage {
    public static void main(String[] args) throws InterruptedException {
        RateLimiter rateLimiter = RateLimiter.create(1000); //每秒投放1000个令牌
        for (int i = 0; i < 10; i++) {
            if (rateLimiter.tryAcquire()) { //tryAcquire检测有没有可用的令牌,结果马上返回
                System.out.println("处理请求");
            } else {
                System.out.println("拒绝请求");
            }
        }
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

结果

处理请求
处理请求
处理请求
拒绝请求
拒绝请求
拒绝请求
拒绝请求
拒绝请求
拒绝请求
拒绝请求
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

没有获取到令牌的请求将会被拒绝,这里可以添加一些额外的处理,例如增加异步操作,把处理不过来的请求加入队列,待处理完之后发消息通知用户处理结果。

模拟请求

模拟一个TPS为10的接口

public class MockUsage {

    RateLimiter rateLimiter = RateLimiter.create(10); //TPS为10

    public void doRequest(String threadName) {
        boolean isAcquired = rateLimiter.tryAcquire();
        if (isAcquired) {
            System.out.println(threadName + ":下单成功");
        } else {
            System.out.println(threadName + ":当前下单人数过多,请稍后再试");
        }
    }
    ...... 
}    
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

测试

public static void main(String[] args) throws InterruptedException, IOException {
        final MockUsage mockUsage = new MockUsage();
        final CountDownLatch latch = new CountDownLatch(1);//两个工人的协作
        final Random random = new Random(10);
        for (int i = 0; i < 20; i++) { //模拟一秒内20个并发
            final int finalI = i;
            Thread t = new Thread(new Runnable() {
                public void run() {
                    try {
                        latch.await();
                        int sleepTime = random.nextInt(1000); //随机sleep [0,1000)毫秒
                        Thread.sleep(sleepTime);
                        mockUsage.doRequest("t-" + finalI);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
        }
        latch.countDown();
        System.in.read();
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

结果

t-8:下单成功
t-2:下单成功
t-10:下单成功
t-11:当前下单人数过多,请稍后再试
t-1:当前下单人数过多,请稍后再试
t-3:当前下单人数过多,请稍后再试
t-17:当前下单人数过多,请稍后再试
t-6:当前下单人数过多,请稍后再试
t-5:下单成功
t-0:当前下单人数过多,请稍后再试
t-19:当前下单人数过多,请稍后再试
t-4:下单成功
t-16:当前下单人数过多,请稍后再试
t-18:当前下单人数过多,请稍后再试
t-9:下单成功
t-13:下单成功
t-14:下单成功
t-12:下单成功
t-15:下单成功
t-7:当前下单人数过多,请稍后再试
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

上面模拟了一个TPS为10的接口,同时一秒内20个并发请求,观察发现一半的请求下单成功,而另一半的需要稍后再试,符合我们的预期

总结

上面的是应用层的限流策略,有基于Atomic的请求数限流,有基于GuavaCache的针对时间窗口请求数限流,也有基于令牌桶的平滑限流。Atomic和GuavaCache这两种方法比较粗粒度,简单的场景下可以使用。 
令牌桶的方法比较细粒度,使用上更灵活,适合复杂一点的场景。 
不过以上的方式只是对单应用进行了限流,在分布式和流行的微服务的场景下有心无力,接下来将会继续学习在分布式和微服务的场景下如何限流。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值