类saas的多租户系统限流方案

一、问题起因

前几天在面试的时候,因为我以前有个B2B订货平台(saas系统架构,平台给每个租户,提供完全独立的在线商城服务,而所有的商城实际上还是在同一个系统中)的项目经验,所以面试官问到了这个问题,当某个租户的流量特别大,怎么保证其他租户的访问不受限制?

二、一般系统常见的限流方案

1.限制并发数

假设系统瞬时可接受的并发数为1000,那么每来一个请求将该数值减1,请求执行完成后将该数值加1,当该数值小于等于0时拒绝访问。该方法实现非常简单粗暴,java应用中可以通过java.util.corrurnent包下的Semaphore信号量的tryAcquire和release操作来实现。
该方法其实常见于一些长连接的限制上,比如db连接。对于执行时间较短,波动较大的请求,并不能很公平的限制流量,因为每个请求执行时间不一样,甚至不同系统负载下的同一个请求执行时间也不一样,如果都只获取一个并发数并不是一个很优的方案。(这个也是我当时面试的时候给面试官的方案,给每个租户一个固定并发数来限制,现在想来其实还有些欠妥)

2.漏桶(Leaky Bucket)算法

漏桶算法

初始一个定长的漏桶,将请求任务放到漏桶中,以固定的流出速率去执行请求,如果桶满了就丢弃。实现方式,事先准备一个定长队列,存放请求任务,准备一个任务执行线程池固定时间间隔取任务来执行即可,队列满了就丢弃。
该方法类似于一些消息队列如rocketmq和kafka的消息消费方式(consumer定速率的从broker上pull消息),这种方法严格限制了系统的执行速率,起到限流的作用,但对于一些可接受范围内突发的大流量请求也会被限制,导致部分请求延迟过大。

3.令牌桶(Token Bucket)算法

令牌桶算法

初始一个定长的桶,以固定的速率往桶内放令牌,溢出的令牌丢弃,每个请求可以获得任意个数令牌,如果取到了足够令牌就可以继续执行,如果取不到就拒绝该请求。Google的Guava包中的RateLimiter即是以该算法实现,实现比较简单,有兴趣的小伙伴可以自己去研究源码。
该方法算是第一种限制并发数的改进版,取得令牌数是可以动态改变的,释放令牌的数量也不会受限于每个请求的执行时间,而且可以应对可接受范围内的突发流量,应该也是目前最常用的方式。

三、针对多租户系统的限流方案(仅个人想法)

1.事先分配

事先为每个租户分配独立的限制量,当然可以简单的平均分配,也可以根据租户事先定制的值分配。这种方法应该是最容易想到,根据总的租户个数平均分配限制量后,再通过对多租户的身份识别分别做流量控制,前面提到的三种算法应该都比较容易通过改进来实现。
缺点:不够灵活,不活跃的租户的量其实是可以暂时分配或者是部分分配给其他租户的。

2.私有+公有令牌桶

①基于令牌桶算法的改进,每个租户都有一个私有的令牌桶,所有租户有个公有的令牌桶,租户先从公有令牌桶取令牌,取不到再去私有令牌桶取,每个租户有各自独立的放令牌速率,先放到私有桶中,溢出的部分再放到公有桶中,公有桶溢出后就丢弃。这种方式可以保证每个租户都有一个可以得到保障的最低流量,而且还可以将系统资源得到充分的利用。但是这种方案使用时却并不那么美好,因为放令牌操作要遍历每个私有桶,他的时间复杂度是O(n),相比较原来O(1)的时间复杂度,尤其在海量租户的情况下,严重影响系统效率。
伪代码:

long timeStamp=getNowTime();
int publicCapacity;              // 公有桶的容量
int privateCapacity;            //私有桶的容量
Map rateMap ;              //每个租户令牌放入速度
int publicTokens;            //公有桶的当前水量
Map privateTokensMap;       //私有桶的当前水量

bool grant(int tenantId, int grantTokens){ //取令牌方法
    //先执行添加令牌的操作
    putTokens();
    //先从公有桶取,再从私有桶取
    int privateTokens = privateTokensMap.get(tenantId);
    if(privateTokens+publicTokens >= grantTokens){
        int tokensDel = publicTokens - grantTokens;
        if(tokensDel >= 0){
            //完全从公有桶取
            publicTokens = tokensDel
        }else{
            //共有桶不够,从私有桶补足
            publicTokens = 0;
            privateTokensMap.put(tenantId,privateTokens+tokensDel);
        }
        return true;
    }else{
        //令牌不够,拒绝请求
        retun false;
    }
}
void putTokens(){
    long now = getNowTime();
    long timeDelta = now - timeStamp;
    timeStamp = now;
    for(Map.Entry rateEntry : rateMap){//遍历每个桶,将令牌放入,私有桶溢出的令牌放到公有桶,公有桶溢出的丢弃
        int tenantId = rateEntry.getKey();
        int rate = rateEntry.getValue();
        int privateTokens = privateTokensMap.get(tenantId);
        int tokensDelta = timeDelta*rate;
        if(privateTokens + tokenDelta > privateCapacity){
            rateEntry.setValue(privateCapacity);
            publicTokens = min(publicCapacity, publicTokens+(privateTokens + tokenDelta - privateCapacity));
        }else{
            rateEntry.setValue(privateTokens + tokenDelta);
        }
    }
}

②在上一种方案的基础下,公有桶也作为一个独立的令牌桶使用,公有桶的令牌流入速率与私有桶不同,每个租户先从公有桶尝试获取,获取不到的情况下,再从私有桶获取,但是每个桶(包括公有桶)的速率总和还是要小于等于系统可接受的最大流量。这样的时间复杂度依然是常数级别的,也可以提高部分闲置资源的使用率。借助现有的限流工具也很容易实现。
伪代码:

Map<RateLimiter> privateRateLimiterMap;    //租户的私有令牌桶,RateLimiter是普通的令牌桶实现,例如:Guava包里的令牌桶RateLimiter
RateLimiter publicRateLimiter;   //公有的令牌桶

void init(Map privateRateMap, int publicRate){ //初始化令牌桶
    publicRateLimiter = RateLimiter.create(publicRate);
    privateRateLimiterMap = new HashMap<>();
    for(Map.Entry rateEntry : privateRateMap){
        RateLimiter privateRateLimiter = RateLimiter.create(rateEntry.getValue());
        privateRateLimiterMap.put(rateEntry.getKey(), privateRateLimiter);
    }
}

bool grant(int tenantId, int grantTokens){ //取令牌方法
    if(publicRateLimiter.tryAcquire(grantTokens))
        return true;
    else
        return privateRateLimiterMap.get(tenantId).tryAcquire(grantTokens);
}

欢迎来我的个人博客逛逛: https://blog.52xtg.com

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

推荐阅读更多精彩内容