您好,登錄后才能下訂單哦!
閱讀本文大概需要 7.3 分鐘。
在上一篇架構師成長之路之服務治理漫談里面,我們已經談到了高可用治理的部分。為了“反脆弱”,在微服務復雜拓撲的情況下,限流是保障服務彈性和拓撲健壯的重中之重。
想一想,如果業務推出了一個秒殺活動,而你沒有任何的限流措施;當你搭建了一個賬號平臺,而完全沒有對十幾個業務方設定流量配額……這些很有可能在特定場合下給你的產品帶來大量的業務損失和口碑影響。
我們通常重點關注產品業務層面正向和逆向功能的完成,而對于逆向技術保障,這一點則是企業發展過程中很容易忽視的,所以一旦業務快速增長,這將給你的產品帶來很大的隱患。
當然,也不是所有的系統都需要限流,這取決于架構師對于當前業務發展的預判。
我們來列舉業內比較常見的一些限流手段。
信號量競爭是用來控制并發的一個常見手段。比如 C 和 Java 中都有 Semaphore 的實現可以讓你方便地上手。鼎鼎大名的彈性框架 Hystrix 也默認選擇了信號量來作為隔離和控制并發的辦法。它的優點即在于簡單可靠,但是只能在單機環境中使用。
隔離艙技術中也大量使用了線程池隔離的方式來實現,通過限制使用的線程數來對流量進行限制,一般會用阻塞隊列配合線程池來實現。如果線程池和隊列都被打滿,可以設計對應拒絕策略。需要謹慎調整其參數和線程池隔離的個數,以避免線程過多導致上下文切換帶來的高昂成本。也是基于這個考慮,Hystrix 默認采用了信號量計數的方式來控制并發。同樣,其也只能在單機環境中使用。
我們可以以第一次請求訪問的時候開始進行計數,而不嚴格按照自然時間來計數。比如可以利用 Redis 的 INCR 和 EXPIRE 組合進行計數,如下偽代碼所示:
count = redis.incrby(key)if count == 1 redis.expire(key,3600)if count >= threshold println("exceed...")
這種實現方式簡單粗暴,可以解決絕大部分分布式限流的問題。但是其存在的問題是:
該計數方式并不是準確計數,由于時間窗口一旦過期,則之前積累的數據就失效,這樣可能導致比如本來希望限制“一分鐘內訪問不能超過 100 次”,但實際上做不到精準的限制,會存在誤判放過本應拒絕的流量。
每次請求都將訪問一次 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 來盡可能提升性能。
cdn.xitu.io/2019/5/17/16ac3ae28dd1eb6d?w=938&h=260&f=png&s=10048">
偽代碼如下:
// 開啟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 的性能瓶頸,也將消耗較多的資源和時間。一般我們可以付出秒級的時延,對其做多階段異步化的處理。比如將計數、刪除過期數據和新增記錄分為三部分去進行異步處理。此處就不進一步展開了。
第一個方案中,分布式滑動窗口的難度在于,不得不進行內存共享來達到窗口計數準確的目的。如果考慮分發時進行 Key Based Routing 是不是能解決這個問題?在付出非冪等、復雜度抬升等一定代價的情況下,引入基于本地內存的分布式限流實現方式。
實現方式有如下兩種:
如果可以接受準實時計算的話,可以采用 Storm,使用 filedsGroup,指定 Key 到對應的 Bolt 去處理;
如果需要實時計算的話,那么就采用 RPC 框架的 LB 策略為指定 Key 的一致性 Hash。然后路由到對應的服務實例去處理。
以上兩個實現方式,當到達 Bolt 或者服務實例后,即可基于本地內存進行處理,處理方式也有三種。
采用 Esper,用 DSL 語句即可簡單實現滑動窗口。
Storm 1.0 之后提供了滑動窗口的實現。
如果希望自實現滑動窗口(不推薦),實現思路也比較簡單即:循環隊列+自然窗口滑動計數。
循環隊列來解決無限后延的時間里,計數空間重復利用的問題。而此處,我們看到了一個熟悉的名詞——“自然窗口計數”。沒錯,底層仍然采用自然窗口計數,但是區別在于,我們會對自然窗口切分更細的粒度,每次批量超前獲取多個分桶,來進行加和計算。這樣就可以實現滑動窗口的效果,你可以認為,當分桶被細化到 10s、5s 甚至越來越細的時候,計數將趨近于更加準確。
令牌桶的示意圖如下:
而漏桶的示意圖如下:
這個在業內也是鼎鼎大名。基本談起限流算法,這兩個算法必然會被提起,令牌桶可以有流量應對突發流量,漏桶則強調對流量的整型。二者的模型是相反的。令牌桶和漏桶算法在單機限流中較為常見,而在分布式限流中罕見蹤跡。
對于令牌桶來說,你可以采用定時任務去做投遞令牌的動作,也可以采用算法的方式去進行簡單的計算。Guava Ratelimiter 采用的是后者。
令牌桶的優勢之一,在于可以有部分余量用以應對突發流量。但是在實際生產環境中,這不一定是安全的。如果我們的服務沒有做好應對更高突發流量的準備,那么很有可能會引發服務雪崩。所以考慮到這一點,Guava 采用了令牌桶 + 漏桶結合的策略來進行限流。對于默認業務,采用標準令牌桶方式進行“可超支”限速,而對于無法突然應對高峰流量的業務,會采用緩慢提升投放令牌速率(即逐步縮短業務請求等待時間)的方式來進行熱啟動控制,具體見 Guava Ratelimiter 源碼注釋描述,此處不贅述,其效果如下圖所示:
以上的限流手段,有的能應用在單機環境,有的能應用在分布式環境。而在高并發的分布式環境中,我們需要考慮清楚如下幾個問題如何解決。
一旦出現這種問題,則可能導致收集的數據相互污染而導致判斷出錯。所以一方面,在運維層面需要確保機器時鐘能夠按期同步。另一方面,需要有準實時檢測的手段,及時發現時鐘偏差太大或者時鐘回退的機器,基于一定策略篩選出不合格的數據來源,將其刨除出計算范圍并發出警告。
你需要考慮你的限流策略迭代的頻繁程度,推動業務方改造的成本,語言/技術棧異構情況,是否有需要進行立多系統聯合限流的場景,以此來進行決策。如果采用 SDK 方式,你需要做好碰到這幾個棘手問題的心理準備。
而如果采用 Server 方式,你則需要更多考慮高并發下數據堆積,機器資源消耗,以及對業務方性能的影響問題。一般業內采用的是富 SDK 的方式來做,但是對于上述的 SDK 會面臨的幾個問題沒有很好的解決方案。而 ServiceMesh 領軍人物 Istio 采用了 Mixer 來實現 Server 端限流的方式,但是碰到了很嚴重的性能問題。所以這是一個很困難的選擇。
回顧下架構師成長之路之服務治理漫談一篇中所講到的服務治理發展路徑,是不是有點驚人的相似?是不是也許限流的未來,不在 SDK 也不在 Server,而在于 ServiceMesh?我不確定,但我覺得這是一個很好的探索方向。
這是一個很有意思的問題,限流本身是為了“反脆弱”而存在的,但是如果你的分布式復雜拓撲中遍布限流功能,那么以后你每個服務的擴容,新的功能上線,拓撲結構的變更,都有可能會導致局部服務流量的驟增,進一步引發限流導致業務有損問題。 這就是“反脆弱”的本身也有可能會導致“脆弱”的出現。 所以,當你進行大規模限流能力擴張覆蓋的時候,需要謹慎審視你的限流能力和成熟度是否能夠支撐起如此大規模的應用。
我們置身于復雜服務拓撲和各種調用鏈路中,這一方面確實給限流帶來了很大的麻煩,但另一方面,我們是不是可以思考一下,這些復雜度,本身是不是可以帶給我們什么樣的利好?比如:底層服務扛不住,那么是不是可以在更上層的調用方入口進行限流?如此是不是可以給予用戶更友好提示的同時,也可避免鏈路上服務各自限流后帶來的系統級聯處理壓力?微服務的本質是自治沒錯,但是我們是不是可以更好地對各個服務的限流自治能力進行編排,以達到效率、體驗、資源利用的優化?
相信大家都會有自己的答案。這件事情本身的難度是在于決策的準確性,但如果能很好地進行落地實現,則意味著我們的限流從自動化已經逐步轉向了智能化。這也將是更高一層次的挑戰和機遇。
在高并發限流場景下,準確性和實時性理論上不可兼得。在特定的場景中,你需要作出你的選擇,比如前文介紹的基于 Redis ZSet 實現的滑動窗口實時計算方式可以滿足實時性和準確性,但其會帶來很明顯的性能問題。所以我們需要作出我們的權衡,比如犧牲準確性將滑動窗口退化為固定窗口來保障性能;或者犧牲實時性,對滑動窗口多階段去做異步化,分析和決策兩階段分離,來保障性能。這取決于你的判斷。
限流是高可用治理中核心的一環,實現方式也五花八門,每種方式也都有各自的問題,本文只是做了一個簡單的回顧。希望隨著 ServiceMesh、AIOps 等理論的興起,我們對于限流是什么,能做什么,怎么實現,能夠釋放出更大的空間去想象。
首發:https://cloud.tencent.com/developer/article/1408380
往期精彩回顧
推薦 11 個 GitHub 上比較熱門的 Java 項目
分庫分表?如何做到永不遷移數據和避免熱點?
Linux運維寶典:最常用的150個命令匯總
工作發狂:Mybatis 中$和#千萬不要亂用!
分享一些好用的 Chrome 擴展
我爸的電腦中了勒索病毒……
P7 黑客是如何發現女朋友出軌的,痛心的經歷!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。