架构设计容错篇之限流

avatar

概述

在分布式的系统中,限流是指限制请求的速率,从而?;は低巢槐还氐牧髁看蚩濉?/p>

实列

avatar

生活中,限流是你再熟悉不过的东西了,当流量超过对象能承载的阀值时,为了?;ざ韵?,通?;岵捎孟蘖鞔胧┛刂屏魉?。

avatar

例如,为了防止长江水域发生洪灾,我国修建了三峡大坝,它也不负众望,一次次的经受住了洪峰考验。比如,2012年7月24日,三峡大坝经受住了建库以来最大的洪峰考验,当时入库的峰值流量达7.12万立方米/秒,出库流量为4万多立方米/秒,为下游削峰3万立方米/秒。7万立方米是什么概念?当天的流量比平均流量1万5千立方米/秒足足高出了5倍之多,而且当地百年一遇的洪水流量也就8.37万立方米/秒。如果没有大坝对洪流进行消峰,后果可想而知。

再如,地铁站在早晚高峰会对人流的流速进行限制,高速公路在节假日车流高峰会对车流的流速进行限制等等不甚枚举。

同样,我们的系统无论它是单体架构还是分布式的架构,为了?;に槐涣髁看蚩澹残枰藿?大坝"即限流。为什么?假设我们有一个系统A,它每秒能处理的请求数(QPS)是200;如果此时请求的速率是400每秒,并持续1个小时,那会发生什么呢?假设所有的请求都会进入队列,那么理论上队列中每秒会多出200个处理不掉的请求,1个小时便会多出12000个;但实际上,会比这个数多,因为多出的12000个请求不仅处理不掉而且还会占用系统的CPU、内存、网络等资源,进而导致请求处理速度下降,可能从原来的200个每秒下降到150个每秒??杉?,当请求速率超过系统的处理阀值后,请求速率越大系统处理请求的速率就越慢,请求速率大一定值后处理请求的速率将变为0。

总之,无论是生活中还是软件中,限流的目的都是为了保护对象。

问题

综上,限流面临的问题便是:如何将请求限制在阀值之下。

方案

限制请求的策略通常有限速、限量两种,或者是两种策略的组合策略。限速指的是限制单位时间内允许的请求的数量,如将请求限制在每秒5个,或3秒15个,典型的例子便是大坝限制水流流出的速度;限量指的是限制请求数量,不对请求的速率进行限制,典型的例子如:餐厅、车库,不限制对象进入的速率,但限制数量。

限量的实现比较简单,我们就不重点介绍,我们简单介绍一下限速的实现思路。假如,系统每秒处理请求的阀值是5,那么我们可以用计算器来统计该时段内的请求数量,每当有请求进入的时候计数器加一,当计数器中请求的数量超过了允许的阀值5时,那么就拒绝该请求或者缓存当系统空闲时在处理,否则允许该请求被执行。这便是限速的基本思路,下面是几种常见的限流算法,它们都能限流但效率有所差异。

Fixed Window 固定窗口
avatar

在固定窗口算法中,以一个固定的时间长度为单位即窗口,统计进入该窗口的请求数量——每进入一个新的请求,请求数量便加一;当请求数量大于预设的阀值时,请求将不被允许执行,反之,当请求数量小于预设阀值时,请求会被允许执行;上一个窗口结束时,便会开启下一个窗口,相邻的两个窗口不相交,上一个窗口的结束时间是下一个窗口的开始时间。

伪代码如下:


public class FixedWindow {
    //窗口大小
    protected int window;
    //请求数量阀值
    protected int threshold;
    //当前窗口请求数量
    protected int count;
    //当前窗口开始时间
    protected Long windowStartTime;

    public FixedWindow(){
        //......
    }

    public boolean tryAcquire(){
        long currentTime = System.currentTimeMillis();  //当前时间
        if (currentTime - windowStartTime > window) {  //检查是否在时间窗口内
            count = 0;  // 请求数清零
            windowStartTime = currentTime;  //开启新窗口
        }
        if (count < threshold) {  // 小于阀值
            count++;  //计数器加1
            return true;
        }

        return false;
    }
}

avatar

该算法的优势是简单、易懂、实现复杂度低;劣势是存在临界问题——两个窗口边界中的请求数量在一个单位的窗口时间内有超过阀值的可能。比如说,我们有两个窗口A、B,窗口的时间长度为1分钟,阀值为5,A窗口占据11:00:00-11:00:59这个时间段,B窗口占据11:01:00-11:01:59这个时间段;A窗口在前40秒内都没有收到任何请求,但后20秒内接受了5个请求;B窗口在前20秒接受了5个请求,但在后40秒没有接到任何请求;那么,可以看出A,B窗口的请求数量都没有超过阀值,但位于两边界的那一分钟时段,其总数10超过了阀值5。

Sliding Window 滑动窗口

在滑动窗口算法中,同样是通过一个具有固定时长的窗口,统计进入该窗口的请求数量;但是,不同的是它将这个窗口划分成n个同等大小的小窗口,将限流阀值也均摊到小窗口上,每个小窗口占据的时间是总时间t和小窗口数量n的比值,即:t/n;当请求落到某个小窗口上时,如果该小窗口的请求数量大于小窗口阀值时,请求将不被允许,反之小于时就允许;当请求的时间不在窗口内时,窗口会向前滑动,以满足当前时间可以落在小窗口上。

下面是简单的实现:

public class SlidingWindow {
    //窗口时长
    protected int windowLength;
    //请求数量阀值
    protected int threshold;
    //小窗口数量
    protected int slots;
    //小窗口阀值
    protected int smallWindowThreshold;
    //小窗口请求数量列表
    protected List<Integer> count;
    //上一次滑动时间
    protected Long windowStartTime;

    public SlidingWindow(int slots, int threshold, int windowLength){
        this.slots = slots;
        this.threshold = threshold;
        this.windowLength = windowLength;
        smallWindowThreshold=threshold/slots;
        count = new LinkedList<>();
        for (int i = 0; i < slots; i++) {

            count.add(0,0);
        }
        windowStartTime=System.currentTimeMillis();
    }

    public static void main(String[] args) {
        SlidingWindow window = new SlidingWindow(3,15,3000);
        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for (int j = 0; j < 30; j++) {
                window.tryAcquire();
            }


        }

    }

    protected int getSlotIndex(long currentTime){
        int index = Long.valueOf(currentTime - windowStartTime).intValue() % slots;
        return index;
    }

    private void trySlippingWindow(long currentTime) {
        //当前时间不再窗口内
        if(windowStartTime+windowLength<currentTime){

            //第一步:计算滑动的步数
            long duration = currentTime-(windowStartTime+windowLength);
            int smallWindowTime = windowLength/slots;
            int steps = (int)Math.ceil(new BigDecimal(duration).divide(new BigDecimal(smallWindowTime)).doubleValue());
            if(steps>=slots){
                System.out.println("窗口距离当前时间较远,滑动到当前时间");
                //重置小窗口请求数
                count.forEach(item->item=0);
                windowStartTime=currentTime;
            }else{
                System.out.println("滑动步数:"+steps);
                for (int i = 0; i < steps; i++) {
                    //第一步:移除最早的小窗口
                    count.remove(slots-1);
                    //第二步:添加新窗口
                    count.add(0,0);
                }
                windowStartTime+=windowLength/slots*steps;
            }

        }

    }

    protected boolean tryAcquire(){
        long currentTime = System.currentTimeMillis();  //当前时间
        trySlippingWindow(currentTime);
        int index = getSlotIndex(currentTime);
        if(count.get(index)>=smallWindowThreshold){
            System.out.println("当前第"+index+"窗口,请求被拒绝");
            return false;
        }else{

            count.set(index,count.get(index)+1);
            System.out.println("当前第"+index+"窗口请求被允许,请求数量:"+count.get(index));
            return true;
        }
    }


}

该算法解决了边界的问题,能将流量准确的限制在规定的阀值之下;但也存在不足之处,我们的目标除了限流外,其实还要最大化处理请求的数量。例如,当系统每秒的处理能力是5个请求,请求会持续3秒钟,每秒的请求数量分别为:4、8、4时;如果,窗口的时间长度为3秒,阀值是15,小窗口个数为3;那么,3秒后的结果便是:13个请求被允许,3个请求被拒绝。

Leaky Bucket 漏桶算法
avatar

在漏桶算法中,没有窗口的概念,它只控制请求流出的速率,不控制请求进入的速率但超过容量的请求会被拒绝。它类似于一个漏斗,请求以不定的速度灌入漏斗中,但以固定的速率流出,这样即能将请求的处理速率控制在阀值之下,又能解决固定时间窗口和滑动时间窗口不能延迟处理请求的问题。在上面例子的条件下:系统每秒的处理能力是5个请求......,如果漏斗的容量是15,请求流出的速率是每秒5个,那么3秒后的结果是:14个请求被允许,2个请求被拒绝,比滑动窗口算法要多处理一个。但漏桶算法和固定窗口以及滑动窗口一样因为其控制的是流出的速率,所以在面对突发流量时处理请求的效率不高。比如,某一时刻突然来了15个请求,漏斗算法只能按5个每秒的速度流出请求,即使此刻系统每秒能处理7个。

Token Bucket 令牌桶算法
avatar

在漏桶算法中,令牌会以固定的速率放入桶中,桶有固定的容量,令牌的数量不能超过该容量。每当有请求进入系统时,每个请求都需要从桶中取出一个令牌,拿不到令牌的请求会被拒绝,每秒取出令牌的数量是不限的只要有令牌都可以取走?;故巧厦娴奶跫拢绻暗娜萘渴?5,桶初始化后便有15个令牌,此后每隔3秒向桶中放入15个令牌;那么3秒后的结果便是:14个请求被允许,2个请求被拒绝。虽然在这个同样的例子中,令牌桶不比漏桶处理的请求数多,但是如果某一时刻突然来了15个请求,令牌桶里面刚好有15个令牌,那么它不会限制令牌被取走的速率,只要系统能处理一秒钟全拿着也是可以的。

总结

限流的两个关键点,一个是设置阀值,另一个是选择合适的算法,通常阀值的设置会比算法来得重要些。阀值的好坏,阀值设置得比系统实际负载能力低,那么就不能最大限度的处理更多的请求,存在资源浪费的现象;反之,过高会使算法发挥不了限流的作用。设置阀值最理想的方式是:根据服务实时的负载情况来动态调整。

扩展阅读

架构设计思维篇之结构

架构设计思维篇之概念

架构设计容错篇之重试

架构设计容错篇之熔断

架构设计容错篇之限流

架构设计事务篇之Mysql事务原理

架构设计事务篇之CAP定理

架构设计事务篇之分布式事务

架构设计消息篇之消息丢失

架构设计消息篇之保证消息顺序性

最后编辑于
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容