架構師成長之路之限流漫談

孤獨鍵客發表於2019-05-17

閱讀本文大概需要 7.3 分鐘。

1. 我們為什麼需要限流

在上一篇架構師成長之路之服務治理漫談裡面,我們已經談到了高可用治理的部分。為了“反脆弱”,在微服務複雜拓撲的情況下,限流是保障服務彈性和拓撲健壯的重中之重。

想一想,如果業務推出了一個秒殺活動,而你沒有任何的限流措施;當你搭建了一個賬號平臺,而完全沒有對十幾個業務方設定流量配額……這些很有可能在特定場合下給你的產品帶來大量的業務損失和口碑影響。

我們通常重點關注產品業務層面正向和逆向功能的完成,而對於逆向技術保障,這一點則是企業發展過程中很容易忽視的,所以一旦業務快速增長,這將給你的產品帶來很大的隱患。

當然,也不是所有的系統都需要限流,這取決於架構師對於當前業務發展的預判。

2. 我們常見的限流手段

我們來列舉業內比較常見的一些限流手段。

2.1 訊號量計數

訊號量競爭是用來控制併發的一個常見手段。比如 C 和 Java 中都有 Semaphore 的實現可以讓你方便地上手。鼎鼎大名的彈性框架 Hystrix 也預設選擇了訊號量來作為隔離和控制併發的辦法。它的優點即在於簡單可靠,但是隻能在單機環境中使用。

2.2 執行緒池隔離

隔離艙技術中也大量使用了執行緒池隔離的方式來實現,透過限制使用的執行緒數來對流量進行限制,一般會用阻塞佇列配合執行緒池來實現。如果執行緒池和佇列都被打滿,可以設計對應拒絕策略。需要謹慎調整其引數和執行緒池隔離的個數,以避免執行緒過多導致上下文切換帶來的高昂成本。也是基於這個考慮,Hystrix 預設採用了訊號量計數的方式來控制併發。同樣,其也只能在單機環境中使用。

2.3 固定視窗計數

我們可以以第一次請求訪問的時候開始進行計數,而不嚴格按照自然時間來計數。比如可以利用 Redis 的 INCR 和 EXPIRE 組合進行計數,如下虛擬碼所示:

count = redis.incrby(key)if count == 1    redis.expire(key,3600)if count >= threshold    println("exceed...")

這種實現方式簡單粗暴,可以解決絕大部分分散式限流的問題。但是其存在的問題是:

  1. 該計數方式並不是準確計數,由於時間視窗一旦過期,則之前積累的資料就失效,這樣可能導致比如本來希望限制“一分鐘內訪問不能超過 100 次”,但實際上做不到精準的限制,會存在誤判放過本應拒絕的流量。

  2. 每次請求都將訪問一次 Redis,可能存在大流量併發時候將快取打崩最終拖垮業務應用的問題。這個在高併發場景中是非常嚴重的問題。當然,你可以選擇按照業務進行適當的快取叢集切割來緩解這種問題,但是這仍然是治標不治本。當然,如果你選擇單機限流的實現方式,則無需使用 Redis,進一步,單機限流情況下該問題不存在。

2.4 自然視窗計數

有的場景會需要以自然視窗為維度進行限制,實現方式即進行分桶計數。每個 slot 一般以時間戳為 key salt,以單 slot 時間長度內的計數值為 Value,我們可以根據實際的需求對單 slot 的時間長度進行限制,比如如果你需要限制一天傳送簡訊數不超限,則以 1 個自然天為 1 個 slot,如果希望限制 QPS,則以 1s 為 1 個 slot。然後起定時任務獲取 slot,進一步取出實際的分桶計算結果,進行判斷是否達到閾值,如果超過閾值則執行對應的限制操作。

該策略如果應用在分散式限流環境下,則會碰到若干個問題。這個後面章節中會提到。另外,該策略本質上其實是也是一種特殊的固定視窗計數策略,那麼固定視窗所存在的弊端,自然視窗計數也會存在。那麼我們不禁會問,如果希望規避固定視窗的一大問題——“無法準確計數”的話,要怎麼做呢?這時, “滑動視窗計數” 方式應運而生。

2.5 滑動視窗計數

滑動視窗的出現,可以很好地解決精準計數的問題。隨著時間視窗不斷地滑動,動態地進行計數判斷。可以規避自然視窗和固定視窗計數所存在的計數不準確的問題。以下有兩種常見的滑動視窗計數的實現類別。

2.5.1 基於共享分散式記憶體

可以採用 Redis ZSet,儲存結構如下圖所示。Key 為功能 ID,Value 為 UUID,Score 也記為同一時間戳。整個過程簡單概括為“新增記錄、設定失效時間、計數、刪除過期記錄”四部分。使用 ZADD、EXPIRE、ZCOUNT 和 zremrangeScore 來實現,並同時注意開啟 Pipeline 來儘可能提升效能。

架構師成長之路之限流漫談

虛擬碼如下:

// 開啟pipepipeline = redis.pielined()// 增加一條請求pipeline.zadd(key, getUUID(), now)// 重新設定失效時間pipeline.expire(key, 3600)// 統計在滑動視窗內,有多少次的請求count = pipeline.zcount(key, expireTimeStamp, now)// 刪除過期記錄pipeline.zremrangeByScore(key, 0, expireTimeStamp - 1)pipeline.sync()if count >= threshold    println("exceed")

但是該方法,有一個比較突出的問題。就是這是一個重操作,將引發高 QPS 下 Redis 的效能瓶頸,也將消耗較多的資源和時間。一般我們可以付出秒級的時延,對其做多階段非同步化的處理。比如將計數、刪除過期資料和新增記錄分為三部分去進行非同步處理。此處就不進一步展開了。

2.5.2 基於本地記憶體

第一個方案中,分散式滑動視窗的難度在於,不得不進行記憶體共享來達到視窗計數準確的目的。如果考慮分發時進行 Key Based Routing 是不是能解決這個問題?在付出非冪等、複雜度抬升等一定代價的情況下,引入基於本地記憶體的分散式限流實現方式。

實現方式有如下兩種:

  1. 如果可以接受準實時計算的話,可以採用 Storm,使用 filedsGroup,指定 Key 到對應的 Bolt 去處理;

  2. 如果需要實時計算的話,那麼就採用 RPC 框架的 LB 策略為指定 Key 的一致性 Hash。然後路由到對應的服務例項去處理。

以上兩個實現方式,當到達 Bolt 或者服務例項後,即可基於本地記憶體進行處理,處理方式也有三種。

  1. 採用 Esper,用 DSL 語句即可簡單實現滑動視窗。

  2. Storm 1.0 之後提供了滑動視窗的實現。

  3. 如果希望自實現滑動視窗(不推薦),實現思路也比較簡單即:迴圈佇列+自然視窗滑動計數。

迴圈佇列來解決無限後延的時間裡,計數空間重複利用的問題。而此處,我們看到了一個熟悉的名詞——“自然視窗計數”。沒錯,底層仍然採用自然視窗計數,但是區別在於,我們會對自然視窗切分更細的粒度,每次批次超前獲取多個分桶,來進行加和計算。這樣就可以實現滑動視窗的效果,你可以認為,當分桶被細化到 10s、5s 甚至越來越細的時候,計數將趨近於更加準確。

2.6 令牌桶和漏桶演算法計數

令牌桶的示意圖如下:

架構師成長之路之限流漫談

而漏桶的示意圖如下:

架構師成長之路之限流漫談

這個在業內也是鼎鼎大名。基本談起限流演算法,這兩個演算法必然會被提起,令牌桶可以有流量應對突發流量,漏桶則強調對流量的整型。二者的模型是相反的。令牌桶和漏桶演算法在單機限流中較為常見,而在分散式限流中罕見蹤跡。

對於令牌桶來說,你可以採用定時任務去做投遞令牌的動作,也可以採用演算法的方式去進行簡單的計算。Guava Ratelimiter 採用的是後者。

令牌桶的優勢之一,在於可以有部分餘量用以應對突發流量。但是在實際生產環境中,這不一定是安全的。如果我們的服務沒有做好應對更高突發流量的準備,那麼很有可能會引發服務雪崩。所以考慮到這一點,Guava 採用了令牌桶 + 漏桶結合的策略來進行限流。對於預設業務,採用標準令牌桶方式進行“可超支”限速,而對於無法突然應對高峰流量的業務,會採用緩慢提升投放令牌速率(即逐步縮短業務請求等待時間)的方式來進行熱啟動控制,具體見 Guava Ratelimiter 原始碼註釋描述,此處不贅述,其效果如下圖所示:

架構師成長之路之限流漫談

3. 微服務限流幾個考慮的點

以上的限流手段,有的能應用在單機環境,有的能應用在分散式環境。而在高併發的分散式環境中,我們需要考慮清楚如下幾個問題如何解決。

3.1 機器時鐘不一致或者時鐘回退問題

一旦出現這種問題,則可能導致收集的資料相互汙染而導致判斷出錯。所以一方面,在運維層面需要確保機器時鐘能夠按期同步。另一方面,需要有準實時檢測的手段,及時發現時鐘偏差太大或者時鐘回退的機器,基於一定策略篩選出不合格的資料來源,將其刨除出計算範圍併發出警告。

3.2 在 SDK 還是 Server 端做限流邏輯

你需要考慮你的限流策略迭代的頻繁程度,推動業務方改造的成本,語言/技術棧異構情況,是否有需要進行立多系統聯合限流的場景,以此來進行決策。如果採用 SDK 方式,你需要做好碰到這幾個棘手問題的心理準備。

而如果採用 Server 方式,你則需要更多考慮高併發下資料堆積,機器資源消耗,以及對業務方效能的影響問題。一般業內採用的是富 SDK 的方式來做,但是對於上述的 SDK 會面臨的幾個問題沒有很好的解決方案。而 ServiceMesh 領軍人物 Istio 採用了 Mixer 來實現 Server 端限流的方式,但是碰到了很嚴重的效能問題。所以這是一個很困難的選擇。

回顧下架構師成長之路之服務治理漫談一篇中所講到的服務治理髮展路徑,是不是有點驚人的相似?是不是也許限流的未來,不在 SDK 也不在 Server,而在於 ServiceMesh?我不確定,但我覺得這是一個很好的探索方向。

3.3 限流是不是會讓你的系統變得不可控

這是一個很有意思的問題,限流本身是為了“反脆弱”而存在的,但是如果你的分散式複雜拓撲中遍佈限流功能,那麼以後你每個服務的擴容,新的功能上線,拓撲結構的變更,都有可能會導致區域性服務流量的驟增,進一步引發限流導致業務有損問題。 這就是“反脆弱”的本身也有可能會導致“脆弱”的出現。 所以,當你進行大規模限流能力擴張覆蓋的時候,需要謹慎審視你的限流能力和成熟度是否能夠支撐起如此大規模的應用。

3.4 拓撲的關聯效能給限流帶來什麼

我們置身於複雜服務拓撲和各種呼叫鏈路中,這一方面確實給限流帶來了很大的麻煩,但另一方面,我們是不是可以思考一下,這些複雜度,本身是不是可以帶給我們什麼樣的利好?比如:底層服務扛不住,那麼是不是可以在更上層的呼叫方入口進行限流?如此是不是可以給予使用者更友好提示的同時,也可避免鏈路上服務各自限流後帶來的系統級聯處理壓力?微服務的本質是自治沒錯,但是我們是不是可以更好地對各個服務的限流自治能力進行編排,以達到效率、體驗、資源利用的最佳化?

相信大家都會有自己的答案。這件事情本身的難度是在於決策的準確性,但如果能很好地進行落地實現,則意味著我們的限流從自動化已經逐步轉向了智慧化。這也將是更高一層次的挑戰和機遇。

3.5 準確性和實時性的權衡

在高併發限流場景下,準確性和實時性理論上不可兼得。在特定的場景中,你需要作出你的選擇,比如前文介紹的基於 Redis ZSet 實現的滑動視窗實時計算方式可以滿足實時性和準確性,但其會帶來很明顯的效能問題。所以我們需要作出我們的權衡,比如犧牲準確性將滑動視窗退化為固定視窗來保障效能;或者犧牲實時性,對滑動視窗多階段去做非同步化,分析和決策兩階段分離,來保障效能。這取決於你的判斷。

4. 總結

限流是高可用治理中核心的一環,實現方式也五花八門,每種方式也都有各自的問題,本文只是做了一個簡單的回顧。希望隨著 ServiceMesh、AIOps 等理論的興起,我們對於限流是什麼,能做什麼,怎麼實現,能夠釋放出更大的空間去想象。

首發:https://cloud.tencent.com/developer/article/1408380


往期精彩回顧

推薦 11 個 GitHub 上比較熱門的 Java 專案

分庫分表?如何做到永不遷移資料和避免熱點?

Linux運維寶典:最常用的150個命令彙總

工作發狂:Mybatis 中$和#千萬不要亂用!

分享一些好用的 Chrome 擴充套件

我爸的電腦中了勒索病毒……

P7 駭客是如何發現女朋友出軌的,痛心的經歷!


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69902700/viewspace-2644708/,如需轉載,請註明出處,否則將追究法律責任。

相關文章