又拍雲張聰:OpenResty 動態流控的幾種姿勢

又拍雲發表於2019-01-23

2019 年 1 月 12 日,由又拍雲、OpenResty 中國社群主辦的 OpenResty × Open Talk 全國巡迴沙龍·深圳站圓滿結束,又拍雲首席架構師張聰在活動上做了《 OpenResty 動態流控的幾種姿勢 》的分享。
OpenResty x Open Talk 全國巡迴沙龍是由 OpenResty 社群、又拍雲發起的,為促進 OpenResty 在技術圈的發展,增進 OpenResty 使用者的交流與學習的系列活動,活動將會陸續在深圳、北京、上海、廣州、杭州、成都、武漢等地舉辦,歡迎大家關注。
張聰,又拍雲首席架構師,多年 CDN 行業產品設計、技術開發和團隊管理相關經驗,個人技術方向集中在 Nginx、OpenResty 等高效能 Web 伺服器方面,國內 OpenResty 技術早期推廣者之一;目前擔任又拍雲內容加速部技術負責人,主導又拍雲 CDN 技術平臺的建設和發展。

 

以下是分享全文:

 

大家下午好,今天我主要和大家分享“在 OpenResty 上如何做動態的流量控制”,將會從以下幾個方面來介紹:

  • Nginx 如何做流控,介紹幾種經典的速率和流量控制的指令和方法;
  • OpenResty 如何動態化做流控;
  • OpenResty 動態流控在又拍雲的業務應用。

又拍雲與 OpenResty 結緣

我目前在又拍雲負責 CDN 的架構設計和開發工作,又拍雲早在 2012 年就開始接觸 OpenResty ,當時我們做調研選型,部分專案考慮用 Lua 來實現,在此之前是基於 Nginx C 模組來做業務開發,一個防盜鏈模組就好幾千行程式碼,改成 Lua 之後大量減少了程式碼,並且整個開發的效率、維護的複雜度都降低了。此外我們通過測試和效能對比,幾乎沒有多的損耗,因為在這一層主要是字串的處理,甚至在 LuaJIT 加速的情況下有很多的呼叫,比我們原先用 C 寫的函式還高效得多。

 

目前又拍雲整個 CDN 代理層系統、對外開放的 API 系統、資料中心的閘道器係統、分散式雲端儲存代理層、邏輯層全部用 ngx_lua 進行了深度的改造,又拍雲內部幾個不同業務的團隊都在 OpenResty 技術棧上有多年的實踐和經驗積累。

又拍雲開放了一個 upyun-resty 的倉庫(https://github.com/upyun/upyun-resty),我們內部孵化的開源專案以及對社群的補丁修復等都會發布在這個倉庫。大家如果對又拍雲這塊的工作感興趣可以關注這個倉庫,我們今年還會陸續把內部使用非常成熟的一些庫放出來,包括今天講的兩個與限速有關的 Lua 庫也已經開源出來了。

什麼是流控以及為什麼要做流控

1、什麼是流控

今天的主題,首先是針對應用層,尤其是 7 層的 HTTP 層,在業務流量進來的時候如何做流量的疏導和控制。我個人對“流控”的理解(針對應用層):

(1) 流控通常意義下是通過一些合理的技術手段對入口請求或流量進行有效地疏導和控制,從而使得有限資源的上游服務和整個系統能始終在健康的設計負荷下工作,同時在不影響絕大多數使用者體驗的情況下,整個系統的“利益”最大化。

因為後端資源有限,無論考慮成本、機器或者系統本身的瓶頸,不可能要求上游系統能夠承受突發的流量,而需要在前面做好流量的控制和管理。

有時候我們不得不犧牲少數的使用者體驗,拒絕部分請求來保證絕大多數的請求正常地服務,其實沒有完美能夠解決所有問題的方案,所以這個在流量控制中要結合我們對業務的理解需要學會做取捨。

(2) 流控有時候也是在考慮安全和成本時的一個手段。

除了上面的通用場景,流控也在安全和成本上做控制。比如敏感賬號的登入頁面,密碼失敗次數多了就禁掉它,不允許反覆暴力嘗試;比如我們的上游頻寬有限,需要確保傳輸的頻寬在較低的水平中進行,不要把線路跑滿,因為跑滿有可能涉及到一些成本超支的問題等。

2、為什麼要流控

針對上面的描述,下面介紹一些流控跟速率限制的方法。

(1)為了業務資料安全,針對關鍵密碼認證請求進行有限次數限制,避免他人通過字典攻擊暴力破解。

為了資料安全,我們會對一些敏感的請求嘗試訪問做累計次數的限制,比如一定時間內你輸錯了三次密碼,接下來的幾個小時內就不讓你來嘗試了,這是一種很常見的手段。如果沒有這樣的保護,攻擊者會不斷試你的密碼,呼叫這個敏感的介面,最終可能會讓他試出來,所以這是需要保護的。

(2)在保障正常使用者請求頻率的同時,限制非正常速率的惡意 DDoS 攻擊請求,拒絕非人類訪問。

我們需要保障一個 API 服務正常的請求流量,但是拒絕完全惡意的 DDoS 的攻擊、大量非人類訪問。這也是在最前面這層需要做的事情,否則這些請求串進上游,很多後端的伺服器肯定是抗不住的。

(3)控制上游應用在同一時刻處理的使用者請求數量,以免出現併發資源競爭導致體驗下降。

我們需要控制上游只能同時併發處理幾個任務或幾個請求,此時關心的是“同時”,因為它可能有內部的資源競爭,或者有一些衝突,必須保證這個服務“同時”只能滿足幾個使用者的處理。

(4)上游業務處理能力有限,如果某一時刻累計未完成任務超過設計最大容量,會導致整體系統出現不穩定甚至持續惡化,需要時刻保持在安全負荷下工作。

當我們整個上游系統的彈性伸縮能力還不錯,它會有一個設計好的最大容量空間,即最多累計能夠承受多大量的請求流入。如果超過它最大可處理範圍效能就會下降。例如一個任務系統每小時能夠完成 10 萬個任務,如果一個小時內任務沒有堆積超過 10 萬,它都能夠正常處理;但某一個小時出現了 20 萬請求,那它處理能力就會下降,它原本一小時能處理 10 萬,此時可能只能處理 5 萬或 2 萬甚至更少,效能變得很差,持續惡化,甚至最終導致崩潰。

因此,我們需要對這樣的流量進行疏導,確保後端系統能夠健康地執行,如果它每小時最多隻能跑 10 萬的任務,那麼無論多大的任務量,每小時最多都應只讓它跑 10 萬的量,而不是因為量超過了,反而最後連 10 萬都跑不到。

(5)叢集模式下,負載均衡也是流控最基礎的一個環節,當然也有些業務無法精確進行前置負載均衡,例如圖片處理等場景就容易出現單點資源瓶頸,此時需要根據上游節點實時負載情況進行主動排程。

在做流量管理時,負載均衡是很基礎的。如果一個叢集基本負載均衡都沒做好,流量還是偏的,上游某個節點很容易在叢集中出現單點,這時去做流量控制就有點不合適。流量控制,首先在叢集模式下要先做好負載均衡,在流量均衡的情況下再去做流量控制,識別惡意的流量。而不要前面的負載均衡都沒做好,流量都集中在某一臺機器上,那你在這一臺上去做控制,吃力不討好。

(6)在實際的業務運營中,往往出於成本考慮,還需要進行流量整形和頻寬控制,包括下載限速和上傳限速,以及在特定領域例如終端裝置音視訊播放場景下,根據實際位元速率進行鍼對性速率限制等。

出於成本的考慮,我們會對一些流量進行控制。比如下載限速是一個很常見的場景,終端使用者尤其是移動端,在進行視訊的播放,按正常的位元速率播放已經足夠流暢。如果是家庭頻寬,下載速度很快,開啟沒一會兒就把電影下載完成了,但實際上沒有必要,因為電影播放已經足夠流暢,一下子把它下載完浪費了很多流量。特別地對於音視訊的內容提供商,他覺得浪費了流量而且使用者體驗差不多,所以此時一般會對這些檔案進行下載限速。

經典的 Nginx 方式實現流量控制

 

Nginx 大家都非常熟悉,特別是資料中心或後端的服務,不管是什麼語言寫的,可能你也不明白為什麼要這麼做,但前面套一個 Nginx 總是讓人放心一點,因為在這麼多年的發展中,Nginx 已經默默變成一個非常基礎可靠的反頂流量最外層入口的在向代理伺服器,基本上很多開發者甚至感知不到它的存在,只知道運維幫忙前面架了一個轉發的服務。

所以,如果我們要做流量管理,應該儘量往前做,不要等流量轉發到後面,讓應用服務去做可能已經來不及了,應用服務只需要關心業務,這些通用的事情就讓上層的代理伺服器完成。

1、Nginx 請求速率限制

(1)limit_req 模組

△ ngx_http_limit_req_module

limit_req 是 Nginx 最常用的限速的模組,上圖是一個簡單的配置,它基於來源 IP 作為唯一的 Key,針對某個唯一的來源 IP 做速率控制,這裡的速率控制配置是 5r/s( 1 秒內允許 5 個請求進來),基於這個模組的實現,再解釋一下 5r/s,即每隔 200ms 能夠允許進來一個請求,每個請求的間隔必須大於 200ms,如果小於 200ms 它就會幫你拒絕。

使用起來很簡單,配置一個共享記憶體,為了多個 worker 能共享狀態。具體可以在 location 做這樣的配置,配完之後就會產生限速的效果。

 
 

△ limit_req 請求示意圖

上圖可以更加直觀地瞭解這個機制,圖中整個灰色的時間條跨度是 1s,我按 200ms 切割成了五等份,時間條上面的箭頭代表一個請求。0s 的時候第一次請求過來直接轉發到後面去了;第二次間隔 200ms 過來的請求也是直接轉發到上游;第三個請求也一樣;第四個請求(第一個紅色箭頭)在 500ms 左右過來,它跟前一個請求的時間間隔只有 100ms,此時模組就發揮作用,幫你拒絕掉,後面也是類似的。

總結一下 limit_req 模組的特點:

  • 針對來源IP,限制其請求速率為 5r/s。
  • 意味著,相鄰請求間隔至少 200ms,否則拒絕。

但實際業務中,偶爾有些突增也是正常的。

這樣簡單地使用,很多時候在實際的業務中是用得不舒服的,因為實際業務中很多場景是需要有一些偶爾的突增的,這樣操作會過於敏感,一超過 200ms 就彈,絕大多數系統都需要允許偶爾的突發,而不能那麼嚴格地去做速率限制。

(2)brust 功能引數

這樣就引出了 limit_req 模組中的一個功能引數 brust(突發),為了方便演示,這裡設定 brust=4,表示在超過限制速率 5r/s 的時候,同時最多允許額外有 4 個請求排隊等候,待平均速率迴歸正常後,佇列最前面的請求會優先被處理。

 

△ brust=4,limit_req 請求示意圖

在 brust 引數的配合下,請求頻率限制允許一定程度的突發請求。設定為 4 次後,表示在超過 5r/s 的瞬間,本來要直接彈掉的請求,現在系統允許額外有 4 個位置的排隊等候,等到整體的平均速率迴歸到正常後,排隊中的 4 個請求會挨個放進去。對於上游的業務服務,感知到的始終是 200ms 一個間隔進來一個請求,部分提前到達的請求在 Nginx 這側進行排隊,等到請求可以進來了就放進來,這樣就允許了一定程度的突發。

如上圖,時間條上面第四個請求跟第三個,間隔明顯是小於 200ms ,按原來的設定應該就直接拒絕了,但現在我們允許一定程度的突發,所以第四個請求被排隊了,等時間慢慢流轉到 600ms 的時候,就會讓它轉發給後端,實際它等待了 100ms。下面也是挨個進來排隊,第五個請求進來它排隊了 200ms,因為前面的時間片已經被第四個請求佔用了,它必須等到下一個時間片才能轉發。可以看到上游服務接收到的請求間隔永遠是恆定的 200ms。

在已經存在 4 個請求同時等候的情況下,此時“立刻”過來的請求就會被拒絕。上圖中可以看到從第五個請求到第九個請求,一共排隊了 5 個請求,第十個請求才被拒絕。因為時間一直是在流動的,它整體是一個動態排隊的過程,解決了一定程度的突發,當然太多突發了還是會處理的。

雖然允許了一定程度的突發,但有些業務場景中,排隊導致的請求延遲增加是不可接受的,例如上圖中突發佇列隊尾的那個請求被滯後了 800ms 才進行處理。對於一些敏感的業務,我們不允許排隊太久,因為這些延時根本就不是在進行有效處理,它只是等候在 Nginx 這側,這時很多業務場景可能就接受不了,這樣的機制我們也需要結合新的要求再優化。但是如果你對延時沒有要求,允許一定的突發,用起來已經比較舒服了。

(3)nodealy 功能引數

limit_req 模組引入了 nodelay 的功能引數,配合 brust 引數使用。nodelay 引數配合 brust=4 就可以使得突發時需要等待的請求立即得到處理,與此同時,模擬一個插槽個數為 4 的“令牌”佇列(桶)。

△ brust=4 配合 nodelay 的 limit_req 請求示意圖

本來突發的請求是需要等待的,有了 nodelay 引數後,原本需要等待的 4 個請求一旦過來就直接轉發給後端,落到後端的請求不會像剛剛那樣存在嚴格的 200ms 間隔,在比較短的時間內就會落下去,它實際上沒有在排隊,請求進來直接往上游就轉發,不過後續超出佇列突發的請求仍然是會被限制的。

為了能夠比較好理解這個場景,引入一個虛擬“桶”。從抽象的角度描述下這個過程,該“令牌”桶會每隔 200ms 釋放一個“令牌”,空出的槽位等待新的“令牌”進來,若桶槽位被填滿,隨後突發的請求就會被拒絕。

本來第六到第九這 4 個請求是排隊等候在 Nginx 一側,現在它們沒有等待直接下去了,可以理解為我們拿出了 4 個虛擬的令牌放入一個“桶”,4 個令牌模擬這 4 個請求在排隊。“桶”每隔 200ms 就會釋放出一個令牌,而一旦它釋放出一個,新的虛擬令牌就可以過來,如果它還沒釋放出,“桶”是滿的,這時請求過來還是會被拒絕。總而言之就是真實的請求沒有在排隊,而是引入了 4 個虛擬的令牌在排隊,在它滿的情況下是不允許其它請求進來。

如此,可以保證這些排隊的請求不需要消耗在無謂的等待上,可以直接進去先處理,而對於後面超過突發值的請求還是拒絕的。這樣就達到了折中,對於上游,它需要更短的時間間隔來處理請求,當然這需要結合業務來考慮,這裡只是提供了一種方式和特定的案例。

總結,在這個模式下,在控制請求速率的同時,允許了一定程度的突發,並且這些突發的請求由於不需要排隊,它能夠立即得到處理,改善了延遲體驗。

(4)delay 功能引數

Nginx 最新的版本 1.15.7 增加了 delay 引數,**支援 delay=number 和 brust=number 引數配合使用。 **delay 也是一個獨立的引數,它支援 number(數量)的配置,和突發的數量配置是一樣的,這兩個引數解決的問題更加細緻,通用場景中遇到的可能會少一點。

這個功能引數是這樣描述的:在有些特定場景下,我們既需要保障正常的少量關聯資源能夠快速地載入,同時也需要對於突發請求及時地進行限制,而 delay 引數能更精細地來控制這類限制效果。

比如網站的頁面,它下面有 4-6 個 JS 、CSS 檔案,載入頁面時需要同時快速地載入完這幾個檔案,才能確保整個頁面的渲染沒有問題。但如果同時超過十個併發請求在這個頁面上出現,那可能就會是非預期的突發,因為一個頁面總共才 4-6 個資源,如果刷一下同時過來的是 12 個請求,說明使用者很快地刷了多次。在這種情況下,業務上是要控制的,就可以引入了 delay 引數,它能夠更精細地來控制限制效果。

在上面的例子中,一個頁面併發載入資源載入這個頁面的時候會跑過來 4-6 個請求,某個使用者點一下頁面,服務端收到的是關於這個頁面的 4-6 個併發請求,返回給它;如果他很快地點了兩下,我們覺得需要禁止他很快地刷這個頁面刷兩次,就需要把超過併發數的這部分請求限制掉,但 burst 設定太小又擔心有誤傷,設定太大可能就起不到任何效果。

此時,我們可以配置一個策略,整體突發配置成 12,超過 12 個肯定是需要拒絕的。而在 12 範圍內,我們希望前面過來的 4-6 個併發請求能夠更快地載入,不要進行無效地等待,這裡設定 delay=8 ,佇列中前 8 個等候的請求會直接傳給上游,而不會排隊,而第 8 個之後的請求仍然會排隊,但不會被直接拒絕,只是會慢一些,避免在這個尺度內出現一些誤傷,同時也起到了一定限制效果(增大時延)。

上面 4 點都是講 Nginx 怎麼進行請求速率限制,簡單總結一下,速率就是針對連續兩個請求間的請求頻率的控制,包括允許一定程度的突發,以及突發排隊是否需要延後處理的優化,還有後面提到的 delay 和 brust 的配合使用。

2、Nginx 併發連線數限制

 

△ ngx_http_limit_conn_module

Nginx 有一個模組叫 limit_conn,在下載的場景中,會出現幾個使用者同時在下載同一個資源,對於處理中的請求,該模組是在讀完請求頭全部內容後才開始計數,比如同時允許線上 5 人下載,那就限制 5 個,超過的 503 拒絕。特別地,在 HTTP/2 和 SPDY 協議下,每一個併發請求都會當作一個獨立的計數項。

3、Nginx 下載頻寬限制

 

△ ngx_http_core_module

在 ngx_http_core_module 模組裡面有 limit_rate_after 和 limit_rate 引數,這個是下載頻寬限制。如上圖,意思是在下載完前面 500KB 資料後,對接下來的資料以每秒 20KB 速度進行限制,這個在檔案下載、視訊播放等業務場景中應用比較多,可以避免不必要的浪費。例如視訊播放,第一個畫面能夠儘快看到,對使用者體驗來說很重要,如果使用者第一個頁面看不到,那他的等待忍耐程度是很差的,所以這個場景下前面的幾個位元組不應該去限速,在看到第一個畫面之後,後面畫面是按照一定視訊位元速率播放,所以沒必要下載很快,而且快了也沒用,它照樣是流暢的,但卻多浪費了流量資源,如果使用者看到一半就關掉,整個視訊下載完成,對於使用者和內容提供商都是資源浪費。

OpenResty 動態流控

相比 Nginx ,OpenResty 具有很多的優勢。

  • 我們需要更加豐富的流控策略!Nginx 只有經典的幾種。
  • 我們需要更加靈活的配置管理!限速的策略配置規則是多樣化的,我們需要更加靈活。
  • 我們需要在 Nginx 請求生命週期的更多階段進行控制!前面提到的的 limit_req 模組,它只能在 PREACCESS 階段進行控制,我們可能需要在 SSL 的解除安裝過程中對握手的連線頻率進行控制,我們也可能需要在其它任意階段進行請求頻率控制,那 Nginx 這個模組就做不到了。
  • 我們需要跨機器進行狀態同步!

請求速率限制 / 併發連線數限制

OpenResty 官方有一個叫做 lua-resty-limit-traffic 的模組,裡面有三種限速的策略。

(1) resty.limit.req 模組

 

△ lua-resty-limit-traffic (resty.limit.req)

resty.limit.req 模組的設計與 NGINX limit_req 實現的效果和功能一樣,當然它用 Lua 來表達限速邏輯,可以在任何的程式碼裡面去引入,幾乎可以在任意上下⽂中使⽤。

(2)resty.limit.conn 模組

功能和 NGINX limit_conn 一致,但 Lua 版本允許突發連線進行短暫延遲等候。

(3)resty.limit.count 模組

 

△ lua-resty-limit-traffic (resty.limit.count)

第三個是 resty.limit.count 模組,請求數量限制,這個目前 Nginx 沒有,用一句話概括這個模組,就是在單位時間內確保累計的請求數量不超過一個最大的值。比如在 1 分鐘之內允許累計有 100 個請求,累計超過 100 就拒絕。這個模組和 Github API Rate Limiting(https://developer.github.com/v3/#rate-limiting)的介面設計類似,也是一個比較經典的限制請求的方式。

跨機器速率限制

 

△ lua-resty-redis-ratelimit (resty.redis.ratelimit)

有了 OpenResty,可以做一些更加有意思的事情。比如我們有多臺機器,想把限制的狀態共享,又拍雲之前開放了一個簡單的模組叫 lua-resty-redis-ratelimit(resty.redis.ratelimit),顧名思義就是把這個狀態扔到 Redis 儲存。它和 Nginx limit req 以及 resty.limit.req 一樣,都是基於漏桶演算法對平均請求速率進行限制。不同的是,該模組將資訊儲存在 Redis 從而實現多 Nginx 例項狀態共享。

藉助於 Redis Lua Script 機制 ,Redis 有一個支援寫 Lua 指令碼的功能,這個指令碼能夠讓一些操作在 Redis 執行的時候保證原子性,依賴這個機制,我們把一次狀態的變更用 Lua Script 就能夠完全原子性地在 Redis 裡面做完。

同時,該模組支援在整個叢集層⾯禁⽌某個非法⽤使用者一段時間,可實現全域性自動拉⿊功能。因為是全域性共享,一旦全網有一個客戶觸發了設定的請求頻率限制,我們可以在整個叢集內瞬間把他拉黑幾個小時。

當然這個模組是有代價的,而且代價也比較大,因為 Nginx 和 Redis 互動需要網路 IO,會帶來一定延遲開銷,僅適合請求量不大,但需要非常精確限制全域性請求速率或單位統計時間跨度非常大的場景。

當然,這個模組也可以做一些自己的優化,不一定所有的狀態都需要跟 Redis 同步,可以根據自己的業務情況做一些區域性計算,然後定時做全域性同步,犧牲一些精確性和及時性,這些都可以去抉擇,這邊只是多提供了一個手段。

知識點-漏桶演算法

 

△ 漏桶演算法示意圖

前面提到的多個模組都是基於漏桶演算法的思想達到頻率限速的效果,如上圖,一個水桶,水滴一滴一滴往下滴,我們希望水往下滴的速度儘可能是恆定的,這樣下游能夠承載的處理能力是比較健康的,不要一下子桶就漏了一個大洞衝下去,希望它均衡地按序地往下滴,同時前面會有源源不斷的水進來。

 

 

這個漏桶演算法思想的核心就是上圖中這個簡單的公式,我們怎麼把請求的 5r/s,即每 200ms 一個請求的頻次限制代到這個公式呢?

首先,在具體實現中,一般定義最小速率為 0.001r/s,即最小的請求刻度是 0.001 個請求,為了直觀計算,我們用 1 個水滴(假設單位t)來表達 0.001 個請求,那麼 rate=5r/s 相當於 5000t/s。

前面提到該演算法是計算兩個相鄰請求的頻率,所以要計算當前請求和上一個請求的時間間隔,假設是 100 ms,單位是毫秒,下面公式中除以 1000 轉換成秒等於 0.1s,即 0.1s 能夠往下滴 500 個水滴,因為速率是 5000t/s,時間過去了 0.1 秒,當然只滴下去 500 滴水。

500 水滴下去的同時,速率一直是恆定的,但是同時又有請求進來,因為新的請求進來才會去計算這個公式,所以後面加了 1000,1000 個水滴代表當前這一個請求。就可以計算出當前桶的剩餘水滴數。

excess 表示上一次超出的水滴數(延遲通過),一開始是 0 。特別地,如果 excess<0,說明這個桶空了,就會把 excess 重置為 0 ;如果 excess>0,說明這個桶有水滴堆積,這時水滴的流入速度比它的流出速度快了,返回 BUSY,表示繁忙。通過這樣動態的標記就可以把這個速率給控制起來。

前面提到的突發,只要把這裡的 0 換成 4 ,就是允許一定程度的突發了。

令牌桶限速

令牌桶和漏桶從一些特殊的角度(特別是從效果)上是有一些相似的,但是它們在設計思想上有比較明顯的差異。

 

△ 令牌桶

令牌桶是指令牌以一定的速率往桶裡進令牌,進來的請求是恆定的速率來補充這個桶,只要桶沒有滿就可以一直往裡面放,如果是補充滿了就不會再補充了。每處理一個請求就從令牌桶拿出一塊,如果沒有令牌可以拿那麼請求就無法往下走。

 

△ lua-resty-limit-rate (resty.limit.rate)

lua-resty-limit-rate(resty.limit.rate)是又拍雲最近開源的一個庫,基於令牌桶實現。

上圖是個簡化的演示,首先申請兩個令牌桶,一個是全域性的令牌桶,一個是針對某個使用者的令牌桶,因為系統內肯定有很多使用者呼叫,全域性是一個桶,每個使用者是一個桶,可以做一個組合的設定。如果全域性的桶沒有滿,單個使用者超過了使用者單獨的頻次限制,我們一般會允許其突發,後端對於處理 A 使用者、B 使用者的消耗一般是相同的,只是業務邏輯上分了 A 使用者和 B 使用者。

因此,整體容量沒有超過限制,單個使用者即便超過了他的限制配置,也允許他突發。只有全域性桶拿不出令牌,此時再來判斷每個使用者的桶,看是否可以拿出令牌,如果它拿不出來了就拒絕掉。此時整體系統達到瓶頸,為了使用者體驗,我們不可能無差別地去彈掉任意使用者的請求,而是挑出當前突發較大的使用者將其請求拒絕而保障其他正常的使用者請求不受任何影響,這是基於使用者體驗的角度來考慮限速的方案配置。

相比 limit.req 基於漏桶的設計,令牌桶的思想更關注容量的變化,而非相鄰請求間的速率的限制,它適合有一定彈性容量設計的系統,只有在全域性資源不夠的時候才去做限制,而非兩個請求之間頻率超了就限制掉,速率允許有較⼤大的波動。

相比 limit.count 對單位視窗時間內累計請求數量進行限制,該模組在特定配置下,也能達到類似效果,並且能避免在單位時間視窗切換瞬間導致可能雙倍的限制請求情況出現。 limit.count 模組在單位時間內,比如在 1 分鐘內限制 100 次,在下一個 1 分鐘統計時,上一個 1 分鐘統計的計數是清零的,固定的時間視窗在切換的時候,在這個切換的瞬間,可能前 1 分鐘的最後 1 秒上來了 99 個請求,下一個 1 分鐘的第 1 秒上來 99 個請求,在這 2 秒內,它超過了設計的單位時間最多 100 個請求的限制,它的切換瞬間會有一些邊界的重疊。而基於令牌桶後,因為它的流入流出有一個桶的容量在保護,所以它切換是比較平滑的,流入速度和流出速度中間有一個緩衝。

除了請求速率限制(一個令牌一個請求),還能夠對位元組傳輸進行流量整形,此時,一個令牌相當於一個位元組。因為流量都是由一個個位元組組成的。如果把位元組變成令牌,那流量的流出流入也可以通過令牌桶來給流量做一些整形。整形就是流量按你期望設計的形狀頻寬(單位時間內的流量)進行傳輸。

OpenResty 動態流控在又拍雲的業務應用

  • 海外代理進行上傳流量整形,避免跑滿傳輸線路頻寬(流量整形);
  • 某 API 請求基於令牌桶針對不同賬戶進行請求速率控制(令牌桶應用);
  • CDN 特性:IP 訪問限制,支援階梯策略升級(IP訪問限制);
  • CDN 特性:位元速率適配限速

又拍雲和 KONG

 

 

KONG 是一個非常著名的 OpenResty 的應用,又拍雲在 2018 年在閘道器層引入了 KONG ,內部也維護了一個 KONG 的 Fork 版本,做了一些外掛的改造和適配。

流量整形

我們在 KONG 上怎麼去做流量呢?因為香港到國內資料中心的傳輸線路價格非常昂貴,我們購買線路頻寬是有一定限制的。但是我們在這條線路傳輸有很多 API ,如果有一個 API 突發流量,就會影響到其他,所以我們在 KONG 上做了改造。

 

KONG 的設計不允許管控請求的 socket 位元組流,也是用 Nginx 的核心模組來轉發位元組流,我們需要去管控所有從 req socket 進來的位元組流,因為要做位元組流限制,所以我們這裡用純 Lua 接管了。

Lua 接管之後,可以看到每 8192 個位元組,都會拿 8192 個令牌,如果能拿出來,就讓這 8192 個位元組往後端傳;如果拿不出來,說明當時已經往後傳太多位元組了,就讓它等一等,起到一些限制效果。

令牌桶應用

 

我們在某一個 API 系統中用令牌桶怎麼做策略的限制呢?上圖是一個簡單的配置示例,我們針對全域性有一個桶,一個令牌的新增速度是 40r/s,令牌的容量是 12000,每次是 4 個令牌一起新增,這是全域性桶的策略;每個使用者空間的策略是:桶的容量是 6000,每次 2 個令牌一起新增,它的限制大概是 10r/s ;對於一些特殊的操作,比如 delete,我們會限制得更加嚴格一點,引入了第三個,專門針對 delete 操作的桶。

所以這裡可以有好多桶來配合,全域性的,區域性的以及特殊的操作,大家的限制等級都不太一樣,策略都可以靈活去配置。

 

△ 限制效果圖

上圖是我們實際的限制效果,藍色部分是通過令牌桶遮蔽掉的,綠色的是健康的,這部分被彈的,看業務資料的話,不是任意空間被彈掉,它被彈的時候都是那麼幾個空間被彈掉,會比較集中那幾個空間,特別出頭的被彈掉。而不是說一大堆的空間,甚至請求流量很小的,你隨機去彈幾個。肯定要挑出那些搗亂的把它彈掉,從而保護整個後端的請求能維持在一個健康的水位下。

IP 訪問限制

 

△ IP 訪問限制

又拍雲的產品中有一個 IP 訪問的限制的功能,針對單位時間內的 IP 進行頻率的保護。當你的網站或者靜態資源被一些惡意的 IP 瘋狂下載,浪費你很多流量的時候是有幫助的。而且我們支援階梯的配置,達到第一個階梯禁止多少時間,如果繼續達到第二個階梯,階梯升級禁用的力度就會更大。

位元速率適配限速

 

△ 位元速率適配限速

針對視訊播放,我們需要對位元速率進行適配。這個功能可以動態讀取 MP4 的後設資料,讀到它的位元速率情況,從而做出相應的下載頻寬控制的策略,使得這個檔案在播放的時候看到的是很流暢的,使用者體驗沒有受到任何影響,但是不會因為客戶端網速較快而多浪費流量資源。這是下載頻寬限速,結合實際應用的一個例子。

 

分享視訊及PPT可前往:

OpenResty 動態流控的幾種姿勢 - 又拍雲

相關文章