在上一篇架構師成長之路之服務治理漫談裏面,咱們已經談到了高可用治理的部分。爲了「反脆弱」,在微服務複雜拓撲的狀況下,限流是保障服務彈性和拓撲健壯的重中之重。redis
想想,若是業務推出了一個秒殺活動,而你沒有任何的限流措施;當你搭建了一個帳號平臺,而徹底沒有對十幾個業務方設定流量配額……這些頗有可能在特定場合下給你的產品帶來大量的業務損失和口碑影響。算法
咱們一般重點關注產品業務層面正向和逆向功能的完成,而對於逆向技術保障,這一點則是企業發展過程當中很容易忽視的,因此一旦業務快速增加,這將給你的產品帶來很大的隱患。緩存
固然,也不是全部的系統都須要限流,這取決於架構師對於當前業務發展的預判。安全
咱們來列舉業內比較常見的一些限流手段。bash
信號量競爭是用來控制併發的一個常見手段。好比 C 和 Java 中都有 Semaphore 的實現可讓你方便地上手。鼎鼎大名的彈性框架 Hystrix 也默認選擇了信號量來做爲隔離和控制併發的辦法。它的優勢即在於簡單可靠,可是隻能在單機環境中使用。架構
隔離艙技術中也大量使用了線程池隔離的方式來實現,經過限制使用的線程數來對流量進行限制,通常會用阻塞隊列配合線程池來實現。若是線程池和隊列都被打滿,能夠設計對應拒絕策略。須要謹慎調整其參數和線程池隔離的個數,以免線程過多致使上下文切換帶來的高昂成本。也是基於這個考慮,Hystrix 默認採用了信號量計數的方式來控制併發。一樣,其也只能在單機環境中使用。併發
咱們能夠以第一次請求訪問的時候開始進行計數,而不嚴格按照天然時間來計數。好比能夠利用 Redis 的 INCR 和 EXPIRE 組合進行計數,以下僞代碼所示:框架
count = redis.incrby(key)
if count == 1
redis.expire(key,3600)
if count >= threshold
println("exceed...")複製代碼
這種實現方式簡單粗暴,能夠解決絕大部分分佈式限流的問題。可是其存在的問題是:運維
1.該計數方式並非準確計數,因爲時間窗口一旦過時,則以前積累的數據就失效,這樣可能致使好比原本但願限制「一分鐘內訪問不能超過 100 次」,但實際上作不到精準的限制,會存在誤判放過本應拒絕的流量。異步
2.每次請求都將訪問一次 Redis,可能存在大流量併發時候將緩存打崩最終拖垮業務應用的問題。這個在高併發場景中是很是嚴重的問題。固然,你能夠選擇按照業務進行適當的緩存集羣切割來緩解這種問題,可是這仍然是治標不治本。固然,若是你選擇單機限流的實現方式,則無需使用 Redis,進一步,單機限流狀況下該問題不存在。
有的場景會須要以天然窗口爲維度進行限制,實現方式即進行分桶計數。每一個 slot 通常以時間戳爲 key salt,以單 slot 時間長度內的計數值爲 Value,咱們能夠根據實際的需求對單 slot 的時間長度進行限制,好比若是你須要限制一天發送短信數不超限,則以 1 個天然天爲 1 個 slot,若是但願限制 QPS,則以 1s 爲 1 個 slot。而後起定時任務獲取 slot,進一步取出實際的分桶計算結果,進行判斷是否達到閾值,若是超過閾值則執行對應的限制操做。
該策略若是應用在分佈式限流環境下,則會碰到若干個問題。這個後面章節中會提到。另外,該策略本質上實際上是也是一種特殊的固定窗口計數策略,那麼固定窗口所存在的弊端,天然窗口計數也會存在。那麼咱們不由會問,若是但願規避固定窗口的一大問題——「沒法準確計數」的話,要怎麼作呢?這時,「滑動窗口計數」方式應運而生。
滑動窗口的出現,能夠很好地解決精準計數的問題。隨着時間窗口不斷地滑動,動態地進行計數判斷。能夠規避天然窗口和固定窗口計數所存在的計數不許確的問題。如下有兩種常見的滑動窗口計數的實現類別。
能夠採用 Redis ZSet,存儲結構以下圖所示。Key 爲功能 ID,Value 爲 UUID,Score 也記爲同一時間戳。整個過程簡單歸納爲「添加記錄、設置失效時間、計數、刪除過時記錄」四部分。使用 ZADD、EXPIRE、ZCOUNT 和 zremrangeScore 來實現,並同時注意開啓 Pipeline 來儘量提高性能。
僞代碼以下:
// 開啓pipe
pipeline = 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 的性能瓶頸,也將消耗較多的資源和時間。通常咱們能夠付出秒級的時延,對其作多階段異步化的處理。好比將計數、刪除過時數據和新增記錄分爲三部分去進行異步處理。此處就不進一步展開了。
第一個方案中,分佈式滑動窗口的難度在於,不得不進行內存共享來達到窗口計數準確的目的。若是考慮分發時進行 Key Based Routing 是否是能解決這個問題?在付出非冪等、複雜度擡升等必定代價的狀況下,引入基於本地內存的分佈式限流實現方式。
實現方式有以下兩種:
1.若是能夠接受準實時計算的話,能夠採用 Storm,使用 filedsGroup,指定 Key 到對應的 Bolt 去處理;
2.若是須要實時計算的話,那麼就採用 RPC 框架的 LB 策略爲指定 Key 的一致性 Hash。而後路由到對應的服務實例去處理。
以上兩個實現方式,當到達 Bolt 或者服務實例後,便可基於本地內存進行處理,處理方式也有三種。
1.採用 Esper,用 DSL 語句便可簡單實現滑動窗口。
2.Storm 1.0 以後提供了滑動窗口的實現。
3.若是但願自實現滑動窗口(不推薦),實現思路也比較簡單即:循環隊列+天然窗口滑動計數。
循環隊列來解決無限後延的時間裏,計數空間重複利用的問題。而此處,咱們看到了一個熟悉的名詞——「天然窗口計數」。沒錯,底層仍然採用天然窗口計數,可是區別在於,咱們會對天然窗口切分更細的粒度,每次批量超前獲取多個分桶,來進行加和計算。這樣就能夠實現滑動窗口的效果,你能夠認爲,當分桶被細化到 10s、5s 甚至愈來愈細的時候,計數將趨近於更加準確。
令牌桶的示意圖以下:
而漏桶的示意圖以下:
這個在業內也是鼎鼎大名。基本談起限流算法,這兩個算法必然會被提起,令牌桶能夠有流量應對突發流量,漏桶則強調對流量的整型。兩者的模型是相反的。令牌桶和漏桶算法在單機限流中較爲常見,而在分佈式限流中罕見蹤影。
對於令牌桶來講,你能夠採用定時任務去作投遞令牌的動做,也能夠採用算法的方式去進行簡單的計算。Guava Ratelimiter 採用的是後者。
令牌桶的優點之一,在於能夠有部分餘量用以應對突發流量。可是在實際生產環境中,這不必定是安全的。若是咱們的服務沒有作好應對更高突發流量的準備,那麼頗有可能會引起服務雪崩。因此考慮到這一點,Guava 採用了令牌桶 + 漏桶結合的策略來進行限流。對於默認業務,採用標準令牌桶方式進行「可超支」限速,而對於沒法忽然應對高峯流量的業務,會採用緩慢提高投放令牌速率(即逐步縮短業務請求等待時間)的方式來進行熱啓動控制,具體見 Guava Ratelimiter 源碼註釋描述,此處不贅述,其效果以下圖所示:
以上的限流手段,有的能應用在單機環境,有的能應用在分佈式環境。而在高併發的分佈式環境中,咱們須要考慮清楚以下幾個問題如何解決。
一旦出現這種問題,則可能致使收集的數據相互污染而致使判斷出錯。因此一方面,在運維層面須要確保機器時鐘可以定期同步。另外一方面,須要有準實時檢測的手段,及時發現時鐘誤差太大或者時鐘回退的機器,基於必定策略篩選出不合格的數據來源,將其刨除出計算範圍併發出警告。
你須要考慮你的限流策略迭代的頻繁程度,推進業務方改造的成本,語言/技術棧異構狀況,是否有須要進行立多系統聯合限流的場景,以此來進行決策。若是採用 SDK 方式,你須要作好碰到這幾個棘手問題的心理準備。
而若是採用 Server 方式,你則須要更多考慮高併發下數據堆積,機器資源消耗,以及對業務方性能的影響問題。通常業內採用的是富 SDK 的方式來作,可是對於上述的 SDK 會面臨的幾個問題沒有很好的解決方案。而 ServiceMesh 領軍人物 Istio 採用了 Mixer 來實現 Server 端限流的方式,可是碰到了很嚴重的性能問題。因此這是一個很困難的選擇。
回顧下架構師成長之路之服務治理漫談一篇中所講到的服務治理髮展路徑,是否是有點驚人的類似?是否是也許限流的將來,不在 SDK 也不在 Server,而在於 ServiceMesh?我不肯定,但我以爲這是一個很好的探索方向。
這是一個頗有意思的問題,限流自己是爲了「反脆弱」而存在的,可是若是你的分佈式複雜拓撲中遍及限流功能,那麼之後你每一個服務的擴容,新的功能上線,拓撲結構的變動,都有可能會致使局部服務流量的驟增,進一步引起限流致使業務有損問題。這就是「反脆弱」的自己也有可能會致使「脆弱」的出現。因此,當你進行大規模限流能力擴張覆蓋的時候,須要謹慎審視你的限流能力和成熟度是否可以支撐起如此大規模的應用。
咱們置身於複雜服務拓撲和各類調用鏈路中,這一方面確實給限流帶來了很大的麻煩,但另外一方面,咱們是否是能夠思考一下,這些複雜度,自己是否是能夠帶給咱們什麼樣的利好?好比:底層服務扛不住,那麼是否是能夠在更上層的調用方入口進行限流?如此是否是能夠給予用戶更友好提示的同時,也可避免鏈路上服務各自限流後帶來的系統級聯處理壓力?微服務的本質是自治沒錯,可是咱們是否是能夠更好地對各個服務的限流自治能力進行編排,以達到效率、體驗、資源利用的優化?
相信你們都會有本身的答案。這件事情自己的難度是在於決策的準確性,但若是能很好地進行落地實現,則意味着咱們的限流從自動化已經逐步轉向了智能化。這也將是更高一層次的挑戰和機遇。
在高併發限流場景下,準確性和實時性理論上不可兼得。在特定的場景中,你須要做出你的選擇,好比前文介紹的基於 Redis ZSet 實現的滑動窗口實時計算方式能夠知足實時性和準確性,但其會帶來很明顯的性能問題。因此咱們須要做出咱們的權衡,好比犧牲準確性將滑動窗口退化爲固定窗口來保障性能;或者犧牲實時性,對滑動窗口多階段去作異步化,分析和決策兩階段分離,來保障性能。這取決於你的判斷。
限流是高可用治理中核心的一環,實現方式也五花八門,每種方式也都有各自的問題,本文只是作了一個簡單的回顧。但願隨着 ServiceMesh、AIOps 等理論的興起,咱們對於限流是什麼,能作什麼,怎麼實現,可以釋放出更大的空間去想象。