基于kubernetes的分布式限流

做為一個數(shù)據(jù)上報系統(tǒng)厘贼,隨著接入量越來越大峭咒,由于 API 接口無法控制調(diào)用方的行為,因此當(dāng)遇到瞬時請求量激增時绅你,會導(dǎo)致接口占用過多服務(wù)器資源伺帘,使得其他請求響應(yīng)速度降低或是超時,更有甚者可能導(dǎo)致服務(wù)器宕機忌锯。

一伪嫁、概念

限流(Ratelimiting)指對應(yīng)用服務(wù)的請求進行限制,例如某一接口的請求限制為 100 個每秒,對超過限制的請求則進行快速失敗或丟棄偶垮。

1.1 使用場景

限流可以應(yīng)對:

  • 熱點業(yè)務(wù)帶來的突發(fā)請求张咳;
  • 調(diào)用方 bug 導(dǎo)致的突發(fā)請求;
  • 惡意攻擊請求似舵。

1.2 維度

對于限流場景晶伦,一般需要考慮兩個維度的信息:
時間
限流基于某段時間范圍或者某個時間點,也就是我們常說的“時間窗口”啄枕,比如對每分鐘、每秒鐘的時間窗口做限定
資源
基于可用資源的限制族沃,比如設(shè)定最大訪問次數(shù)频祝,或最高可用連接數(shù)。
??限流就是在某個時間窗口對資源訪問做限制脆淹,比如設(shè)定每秒最多100個訪問請求常空。

image.png

1.3 分布式限流

分布式限流相比于單機限流,只是把限流頻次分配到各個節(jié)點中盖溺,比如限制某個服務(wù)訪問100qps漓糙,如果有10個節(jié)點,那么每個節(jié)點理論上能夠平均被訪問10次烘嘱,如果超過了則進行頻率限制昆禽。

二蝗蛙、分布式限流常用方案

基于Guava的客戶端限流
Guava是一個客戶端組件,在其多線程模塊下提供了以RateLimiter為首的幾個限流支持類醉鳖。它只能對“當(dāng)前”服務(wù)進行限流捡硅,即它不屬于分布式限流的解決方案。

網(wǎng)關(guān)層限流
服務(wù)網(wǎng)關(guān)盗棵,作為整個分布式鏈路中的第一道關(guān)卡壮韭,承接了所有用戶來訪請求。我們在網(wǎng)關(guān)層進行限流纹因,就可以達到了整體限流的目的了喷屋。目前,主流的網(wǎng)關(guān)層有以軟件為代表的Nginx瞭恰,還有Spring Cloud中的Gateway和Zuul這類網(wǎng)關(guān)層組件屯曹,也有以硬件為代表的F5。

中間件限流
將限流信息存儲在分布式環(huán)境中某個中間件里(比如Redis緩存)寄疏,每個組件都可以從這里獲取到當(dāng)前時刻的流量統(tǒng)計是牢,從而決定是拒絕服務(wù)還是放行流量。

限流組件
目前也有一些開源組件提供了限流的功能陕截,比如Sentinel就是一個不錯的選擇驳棱。Sentinel是阿里出品的開源組件,并且包含在了Spring Cloud Alibaba組件庫中农曲。

Guava的Ratelimiter設(shè)計實現(xiàn)相當(dāng)不錯社搅,可惜只能支持單機,網(wǎng)關(guān)層限流如果是單機則不太滿足高可用乳规,并且分布式網(wǎng)關(guān)的話還是需要依賴中間件限流形葬,而redis之類的網(wǎng)絡(luò)通信需要占用一小部分的網(wǎng)絡(luò)消耗。阿里的Sentinel也是同理暮的,底層使用的是redis或者zookeeper笙以,每次訪問都需要調(diào)用一次redis或者zk的接口。那么在云原生場景下冻辩,我們有沒有什么更好的辦法呢猖腕?

對于極致追求高性能的服務(wù)不需要考慮熔斷、降級來說恨闪,是需要盡量減少網(wǎng)絡(luò)之間的IO倘感,那么是否可以通過一個總限頻然后分配到具體的單機里面去,在單機中實現(xiàn)平均的限流咙咽,比如限制某個ip的qps為100老玛,服務(wù)總共有10個節(jié)點,那么平均到每個服務(wù)里就是10qps,此時就可以通過guava的ratelimiter來實現(xiàn)了蜡豹,甚至說如果服務(wù)的節(jié)點動態(tài)調(diào)整麸粮,單個服務(wù)的qps也能動態(tài)調(diào)整。

三余素、基于kubernetes的分布式限流

在Spring Boot應(yīng)用中豹休,定義一個filter,獲取請求參數(shù)里的key(ip桨吊、userId等)威根,然后根據(jù)key來獲取rateLimiter,其中视乐,rateLimiter的創(chuàng)建由數(shù)據(jù)庫定義的限頻數(shù)和副本數(shù)來判斷洛搀,最后,再通過rateLimiter.tryAcquire來判斷是否可以通過佑淀。

企業(yè)微信截圖_868136b4-f9e2-4813-bc02-281a66756ecd.png

3.1 kubernetes中的副本數(shù)

在實際的服務(wù)中留美,數(shù)據(jù)上報服務(wù)一般無法確定客戶端的上報時間、上報量伸刃,特別是對于這種要求高性能谎砾,服務(wù)一般都會用到HPA來實現(xiàn)動態(tài)擴縮容,所以捧颅,需要去間隔一段時間去獲取服務(wù)的副本數(shù)景图。

func CountDeploymentSize(namespace string, deploymentName string) *int32 {
    deployment, err := client.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
    if err != nil {
        return nil
    }
    return deployment.Spec.Replicas
}

用法:GET host/namespaces/test/deployments/k8s-rest-api直接即可。

3.2 rateLimiter的創(chuàng)建

在RateLimiterService中定義一個LoadingCache<String, RateLimiter>碉哑,其中挚币,key可以為ip、userId等扣典,并且妆毕,在多線程的情況下,使用refreshAfterWrite只阻塞加載數(shù)據(jù)的線程贮尖,其他線程則返回舊數(shù)據(jù)笛粘,極致發(fā)揮緩存的作用。

private final LoadingCache<String, RateLimiter> loadingCache = Caffeine.newBuilder()
        .maximumSize(10_000)
        .refreshAfterWrite(20, TimeUnit.MINUTES)
        .build(this::createRateLimit);
//定義一個默認最小的QPS
private static final Integer minQpsLimit = 3000;

之后是創(chuàng)建rateLimiter湿硝,獲取總限頻數(shù)totalLimit和副本數(shù)replicas闰蛔,之后是自己所需的邏輯判斷,可以根據(jù)totalLimit和replicas的情況來進行qps的限定图柏。

public RateLimiter createRateLimit(String key) {
    log.info("createRateLimit,key:{}", key);
    int totalLimit = 獲取總限頻數(shù),可以在數(shù)據(jù)庫中定義
    Integer replicas = kubernetesService.getDeploymentReplicas();
    RateLimiter rateLimiter;
    if (totalLimit > 0 && replicas == null) {
        rateLimiter = RateLimiter.create(totalLimit);
    } else if (totalLimit > 0) {
        int nodeQpsLimit = totalLimit / replicas;
        rateLimiter = RateLimiter.create(nodeQpsLimit > minQpsLimit ? nodeQpsLimit : minQpsLimit);
    } else {
        rateLimiter = RateLimiter.create(minQpsLimit);
    }
    log.info("create rateLimiter success,key:{},rateLimiter:{}", key, rateLimiter);
    return rateLimiter;
}

3.3 rateLimiter的獲取

根據(jù)key獲取RateLimiter任连,如果有特殊需求的話蚤吹,需要判斷key不存在的嘗盡

public RateLimiter getRateLimiter(String key) {
  return loadingCache.get(key);
}

3.4 filter里的判斷

最后一步,就是使用rateLimiter來進行限流,如果rateLimiter.tryAcquire()為true裁着,則進行filterChain.doFilter(request, response)繁涂,如果為false,則返回HttpStatus.TOO_MANY_REQUESTS

public class RateLimiterFilter implements Filter {
    @Resource
    private RateLimiterService rateLimiterService;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        String key = httpServletRequest.getHeader("key");
        RateLimiter rateLimiter = rateLimiterService.getRateLimiter(key);
        if (rateLimiter != null) {
            if (rateLimiter.tryAcquire()) {
                filterChain.doFilter(request, response);
            } else {
                httpServletResponse.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            }
        } else {
            filterChain.doFilter(request, response);
        }
    }
}

四二驰、性能壓測

為了方便對比性能之間的差距扔罪,我們在本地單機做了下列測試,其中桶雀,總限頻都設(shè)置為3萬矿酵。

無限流

企業(yè)微信截圖_ea1b7815-3b89-43bf-aed7-240e135bdad1.png

使用redis限流

其中,ping redis大概6-7ms左右矗积,對應(yīng)的全肮,每次請求需要訪問redis,時延都有大概6-7ms棘捣,性能下降明顯

企業(yè)微信截圖_9118c4e0-c7f3-4e74-9649-4cd4d56fda79.png

自研限流

性能幾乎追平無限流的場景辜腺,guava的rateLimiter確實表現(xiàn)卓越

企業(yè)微信截圖_9f6510bd-be9e-438b-aa9d-bdd13ebca953.png

五、其他問題

5.1 對于保證qps限頻準(zhǔn)確的時候乍恐,應(yīng)該怎么解決呢评疗?

在k8s中,服務(wù)是動態(tài)擴縮容的茵烈,相應(yīng)的百匆,每個節(jié)點應(yīng)該都要有所變化,如果對外宣稱限頻100qps瞧毙,而且后續(xù)業(yè)務(wù)方真的要求百分百準(zhǔn)確胧华,只能把LoadingCache<String, RateLimiter>的過期時間調(diào)小一點,讓它能夠近實時的更新單節(jié)點的qps宙彪。這里還需要考慮一下k8s的壓力矩动,因為每次都要獲取副本數(shù),這里也是需要做緩存的

5.2 服務(wù)從1個節(jié)點動態(tài)擴為4個節(jié)點释漆,這個時候新節(jié)點識別為4悲没,但其實有些并沒有啟動完,會不會造成某個節(jié)點承受了太大的壓力

理論上是存在這個可能的男图,這個時候需要考慮一下初始的副本數(shù)的示姿,擴縮容不能一蹴而就,一下子從1變?yōu)?變?yōu)閹资畟€這種逊笆。一般的話栈戳,生產(chǎn)環(huán)境肯定是不能只有一個節(jié)點,并且要考慮擴縮容的話难裆,至于要有多個副本預(yù)備的

5.3 如果有多個副本子檀,怎么保證請求是均勻的

這個是依賴于k8s的service負載均衡策略的镊掖,這個我們之前做過實驗,流量確實是能夠均勻的落到節(jié)點上的褂痰。還有就是亩进,我們整個限流都是基于k8s的,如果k8s出現(xiàn)問題缩歪,那就是整個集群所有服務(wù)都有可能出現(xiàn)問題了归薛。

參考

1.常見的分布式限流解決方案
2.分布式服務(wù)限流實戰(zhàn)
3.高性能

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市匪蝙,隨后出現(xiàn)的幾起案子主籍,更是在濱河造成了極大的恐慌,老刑警劉巖骗污,帶你破解...
    沈念sama閱讀 221,430評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件崇猫,死亡現(xiàn)場離奇詭異,居然都是意外死亡需忿,警方通過查閱死者的電腦和手機诅炉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,406評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屋厘,“玉大人涕烧,你說我怎么就攤上這事『谷鳎” “怎么了议纯?”我有些...
    開封第一講書人閱讀 167,834評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長溢谤。 經(jīng)常有香客問我瞻凤,道長,這世上最難降的妖魔是什么世杀? 我笑而不...
    開封第一講書人閱讀 59,543評論 1 296
  • 正文 為了忘掉前任阀参,我火速辦了婚禮,結(jié)果婚禮上瞻坝,老公的妹妹穿的比我還像新娘蛛壳。我一直安慰自己,他們只是感情好所刀,可當(dāng)我...
    茶點故事閱讀 68,547評論 6 397
  • 文/花漫 我一把揭開白布衙荐。 她就那樣靜靜地躺著,像睡著了一般浮创。 火紅的嫁衣襯著肌膚如雪忧吟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,196評論 1 308
  • 那天斩披,我揣著相機與錄音溜族,去河邊找鬼胸嘴。 笑死,一個胖子當(dāng)著我的面吹牛斩祭,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播乡话,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼摧玫,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了绑青?” 一聲冷哼從身側(cè)響起诬像,我...
    開封第一講書人閱讀 39,671評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闸婴,沒想到半個月后坏挠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,221評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡邪乍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,303評論 3 340
  • 正文 我和宋清朗相戀三年降狠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庇楞。...
    茶點故事閱讀 40,444評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡榜配,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出吕晌,到底是詐尸還是另有隱情蛋褥,我是刑警寧澤,帶...
    沈念sama閱讀 36,134評論 5 350
  • 正文 年R本政府宣布睛驳,位于F島的核電站烙心,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏乏沸。R本人自食惡果不足惜淫茵,卻給世界環(huán)境...
    茶點故事閱讀 41,810評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望屎蜓。 院中可真熱鬧痘昌,春花似錦、人聲如沸炬转。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,285評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽扼劈。三九已至驻啤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荐吵,已是汗流浹背骑冗。 一陣腳步聲響...
    開封第一講書人閱讀 33,399評論 1 272
  • 我被黑心中介騙來泰國打工赊瞬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贼涩。 一個月前我還...
    沈念sama閱讀 48,837評論 3 376
  • 正文 我出身青樓巧涧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親遥倦。 傳聞我的和親對象是個殘疾皇子谤绳,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,455評論 2 359