乐趣区

关于java:高并发架构下的系统限流保护策略

当零碎并发流量过大的时候,有可能会造成零碎被压垮导致整个服务不可用的问题。

针对这个场景,个别的解决方案是:如果超过这个流量,咱们就回绝提供服务,从而使得咱们的服务不会挂掉。

当然,限流尽管可能爱护零碎不被压垮,然而对于被限流的用户,就会很不开心。所以限流其实是一种有损的解决方案。然而相比于全副不可用,有损服务是最好的一种解决办法

限流的作用

除了后面说的限流应用场景之外,限流的设计还能避免歹意申请流量、歹意攻打

所以,限流的基本原理是通过对并发拜访 / 申请进行限速或者一个工夫窗口内的申请进行限速来爱护零碎,一旦达到限度速率则能够拒绝服务(定向到谬误页或者告知资源没有了)、排队或期待(秒杀、下单)、降级(返回兜底数据或默认数据或默认数据,如商品详情页库存默认有货)

个别互联网企业常见的限流有:限度总并发数(如数据库连接池、线程池)、限度刹时并发数(nginx 的 limit_conn 模块,用来限度刹时并发连接数)、限度工夫窗口内的均匀速率(如 Guava 的 RateLimiter、nginx 的 limit_req 模块,限度每秒的均匀速率);其余的还有限度近程接口调用速率、限度 MQ 的生产速率。另外还能够依据网络连接数、网络流量、CPU 或内存负载等来限流。

有了缓存当前再加上限流,在解决高并发的时候就可能从容应对,不必放心霎时流量导致系统挂掉或雪崩,最终做到有损服务而不是不服务;然而限流须要评估好,不能乱用,否则一些失常流量呈现一些奇怪的问题而导致用户体验很差造成用户散失。

常见的限流算法

滑动窗口

发送和接受方都会保护一个数据帧的序列,这个序列被称作窗口。发送方的窗口大小由接受方确定,目标在于管制发送速度,免得接受方的缓存不够大,而导致溢出,同时管制流量也能够防止网络拥塞。上面图中的 4,5,6 号数据帧曾经被发送进来,然而未收到关联的 ACK,7,8,9 帧则是期待发送。能够看出发送端的窗口大小为 6,这是由承受端告知的。此时如果发送端收到 4 号 ACK,则窗口的左边缘向右膨胀,窗口的右边缘则向右扩大,此时窗口就向前“滑动了”,即数据帧 10 也能够被发送。

滑动窗口演示地址

漏桶(管制传输速率 Leaky bucket)

漏桶算法思路是,一直的往桶外面注水,无论注水的速度是大还是小,水都是按固定的速率往外漏水;如果桶满了,水会溢出;

桶自身具备一个恒定的速率往下漏水,而上方时快时慢的会有水进入桶内。当桶还未满时,上方的水能够退出。一旦水满,上方的水就无奈退出。桶满正是算法中的一个要害的触发条件(即流量异样判断成立的条件)。而此条件下如何解决上方流下来的水,有两种形式

在桶满水之后,常见的两种解决形式为:

  1. 临时拦挡住上方水的向下流动,期待桶中的一部分水漏走后,再放行上方水。
  2. 溢出的上方水间接摈弃。

特点

  1. 漏水的速率是固定的
  2. 即便存在注水 burst(忽然注水质变大)的状况,漏水的速率也是固定的

令牌桶(可能解决突发流量)

令牌桶算法是网络流量整形(Traffic Shaping)和速率限度(Rate Limiting)中最常应用的一种算法。典型状况下,令牌桶算法用来管制发送到网络上的数据的数目,并容许突发数据的发送。

令牌桶是一个寄存固定容量令牌(token)的桶,依照固定速率往桶里增加令牌; 令牌桶算法实际上由三局部组成:两个流和一个桶,别离是令牌流、数据流和令牌桶

令牌流与令牌桶

零碎会以肯定的速度生成令牌,并将其搁置到令牌桶中,能够将令牌桶设想成一个缓冲区(能够用队列这种数据结构来实现),当缓冲区填满的时候,新生成的令牌会被扔掉。这里有两个变量很重要:

第一个是生成令牌的速度,个别称为 rate。比方,咱们设定 rate = 2,即每秒钟生成 2 个令牌,也就是每 1/2 秒生成一个令牌;

第二个是令牌桶的大小,个别称为 burst。比方,咱们设定 burst = 10,即令牌桶最大只能包容 10 个令牌。

数据流

数据流是真正的进入零碎的流量,对于 http 接口来说,如果均匀每秒钟会调用 2 次,则认为速率为 2 次 /s。

有以下三种情景可能产生:

数据流的速率 等于 令牌流的速率。这种状况下, 每个到来的数据包或者申请都能对应一个令牌, 而后无提早地通过队列;

数据流的速率 小于 令牌流的速率。通过队列的数据包或者申请只耗费了一部分令牌,剩下的令牌会在令牌桶里积攒下来,直到桶被装满。剩下的令牌能够在突发申请的时候消耗掉。

数据流的速率 大于 令牌流的速率。这意味着桶里的令牌很快就会被耗尽。导致服务中断一段时间,如果数据包或者申请继续到来, 将产生丢包或者回绝响应。

比方后面举的例子,生成令牌的速率和令牌桶的大小别离为 rate = 2, burst = 10,则零碎能接受的突发申请速率为 10 次 /s,均匀申请速率为 2 次 /s。三种情景中的最初一种情景是这个算法的外围所在,这个算法十分准确,实现非常简单并且对服务器的压力能够忽略不计,因而利用得相当宽泛,值得学习和利用

特点

  1. 令牌能够积攒:桶中最大的令牌数是 b,示意能够积攒的最大令牌数
  2. 容许突发流量:桶中 token 能够积攒到 n(b<=n<=0),此时如果有 n 个突发申请同时达到,这 n 个申请是能够同时容许解决的

限流算法实战

Semaphore

Semaphore 比拟常见的就是用来做限流操作了。比方上面的场景中,模仿 20 个客户端申请过去,咱们为了缩小拜访的压力,通过 Semaphore 来限度申请的流量。

public class SemaphoreTest {public static void main(String[] args) {  
        // 线程池 
        ExecutorService exec = Executors.newCachedThreadPool();  
        // 只能 5 个线程同时拜访 
        final Semaphore semp = new Semaphore(5);  
        // 模仿 20 个客户端拜访 
        for (int index = 0; index < 20; index++) {
            final int NO = index;  
            Runnable run = new Runnable() {public void run() {  
                    try {  
                        // 获取许可 
                        semp.acquire();  
                        System.out.println("Accessing:" \+ NO);  
                        Thread.sleep((long) (Math.random() * 10000));  
                        // 拜访完后,开释 
                        semp.release();} catch (InterruptedException e) {}}  
            };  
            exec.execute(run);  
        }  
        // 退出线程池 
        exec.shutdown();}  
}

Guava 的 RateLimiter 实现

在 Guava 中 RateLimiter 的实现有两种:Bursty 和 WarmUp

bursty 是基于 token bucket 的算法实现,比方

RateLimiter rateLimiter=RateLimiter.create(permitPerSecond); // 创立一个 bursty 实例

rateLimiter.acquire(); // 获取 1 个 permit,当令牌数量不够时会阻塞直到获取为止

  1. 引入 jar 包

    <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
       <version>23.0</version>
    </dependency>
  2. 编写测试代码

    public class PayService {RateLimiter rateLimiter=RateLimiter.create(10);//qps=10
    
        public void doRequest(String threadName){if(rateLimiter.tryAcquire()){System.out.println(threadName+": 领取胜利");
            }else{System.out.println(threadName+": 以后领取人数过多,请稍候再试");
            }
        }
    
        public static void main(String[] args) throws IOException {PayService payService=new PayService();
            CountDownLatch latch=new CountDownLatch(1);
            Random random=new Random(10);
            for (int i = 0; i < 20; i++) {
                int finalI = i;
                new Thread(()->{
                    try {latch.await();
                        int sleepTime = random.nextInt(1000);
                        Thread.sleep(sleepTime);
                        payService.doRequest("t-"+ finalI);
                    }catch (Exception e){e.printStackTrace();
                    }
                }).start();}
            latch.countDown();
            System.in.read();}
    }

下一篇文章来剖析 Alibaba 开源的限流框架 Sentinel!

版权申明:本博客所有文章除特地申明外,均采纳 CC BY-NC-SA 4.0 许可协定。转载请注明来自 Mic 带你学架构
如果本篇文章对您有帮忙,还请帮忙点个关注和赞,您的保持是我一直创作的能源。欢送关注同名微信公众号获取更多技术干货!

退出移动版