如何保障服務器承受億級流量(12)【限流】

前面的文章中我們介紹過秒殺系統(tǒng)的架構方案,其中涉及了限流的相關內容,因篇幅有限,當時并沒有將這部分內容展開討論,在這篇文章里就著重聊聊限流的相關知識。

為了方便理解內容,我們還是先從業(yè)務場景入手。

一、業(yè)務場景

在秒殺活動中,總計有 100 個特價商品,且每個商品的價格都非常低,活動計劃于 10 月 10 日晚上 10 點 10 分 0 秒開啟。

當時,我們的服務器架構圖如下,所有客戶端的 API 請求先進入 1 個 Nginx 層,再由 Nginx 層轉發(fā)至網關層(Java,使用 Spring Cloud Zuul),最后轉發(fā)至后臺服務1(Java)。

file

預測到秒殺開始那一瞬間會有海量用戶涌入,致使系統(tǒng)無法處理所有用戶請求,為保障服務器承受住大流量,我們只能通過限流的方式將部分流量放入后臺服務中。

那什么是限流呢?一說到限流,有些人總喜歡把它與熔斷混在一起談,其實它們是有區(qū)別的。

熔斷一般發(fā)生在服務調用方,比如服務 A 需要調用服務 B,調用幾次后發(fā)現(xiàn)服務 B 出現(xiàn)了問題且無法再調用,此時服務 A 必須立馬觸發(fā)熔斷,在一段時間內不再調用服務 B。

限流一般發(fā)生在服務被調用方,且主要在網關層做限流操作。比如一個電商網站 1 秒鐘內后臺服務只能處理 10 萬個請求,這時突然涌入了 100 萬個請求,我們該怎么辦?此時,我們可以把 90% 的請求全部拋棄且不做處理,然后專心處理 10% 的請求,以此保證至少 10 萬人能正常操作。(這個比例看起來有點夸張,但是在實際秒殺場景中,就算我們把 99% 的流量拋棄掉都不要緊。)

再回到這一講的業(yè)務場景中,這次我們的訴求是在某個層級通過限流的方式將秒殺活動的交易 TPS 控制在 100 筆/秒(因為秒殺活動總計 100 個庫存,也就是說最終的交易只有 100 筆,而我們希望 100 筆交易在 1 秒內完成),此時我們應該怎么做呢?這就需要使用到限流的一些常用算法了。

二、限流算法

據我了解,市面上關于限流的算法總共分為固定時間窗口計數、滑動時間窗口計數、漏桶、令牌桶這 4 種,下面我們分別進行說明。

(一)固定時間窗口計數算法

假設我們的訴求是每 5 秒鐘,后臺服務處理 500 個請求(以 5 秒為單位方便舉例),那么每 5 秒鐘我們就需要一個時間窗口來統(tǒng)計請求,為了方便理解,我們梳理了如下一個業(yè)務表。


file

此時固定時間窗口計算算法看起來可以滿足我們的訴求了,不過它會出現(xiàn)一個問題,我們細細分析下。

假設 1 秒至4 秒有 200 個請求過來,5 秒時有 300 個請求過來,6 秒至 9 秒有 499 個請求過來,10 秒時有 1 個請求過來,通過計算得知:1 秒至 5 秒總計 500 個請求,6 秒至 10 秒也是總計 500 個請求。

通過這種算法統(tǒng)計后,我們發(fā)現(xiàn)流量確實沒有超出閾值。再仔細看看 5 秒~9 秒這個區(qū)間的請求數,它已經達到了 300+499=799 個,也就是說 5 秒~9 秒的請求數超標了 299 個,服務器明顯扛不住了。

因此,固定時間窗口計數算法在現(xiàn)實中并不實用。

(二)滑動時間窗口計數算法

假設我們的訴求是在 1 秒內后臺服務處理 100 個請求,滑動時間窗口計數算法是每 100 毫秒設置一個時間區(qū)間,每個時間區(qū)間統(tǒng)計該區(qū)間內的請求數量,然后每 10 個時間區(qū)間合并計算請求總數,請求數超出最大數量時就把多余的請求數據拋棄。當時間節(jié)點進入到下一個區(qū)間(比如第 11 個區(qū)間),我們便不再統(tǒng)計第 1 個區(qū)間的請求數量,而是將第 2 個區(qū)間~第 11 個區(qū)間的請求數量進行合并計算出一個總數,并以此類推,如下圖所示。


file

雖然滑動時間窗口計數算法并不能保證每秒的統(tǒng)計請求數 100% 精準,但是可以大大減少單位時間內請求數超出閾值且檢測不出來的概率。比如請求都堆積在前 100ms 的尾端與后 100ms 的首端,有可能出現(xiàn)請求數超出最大數量且不被發(fā)現(xiàn)的情況。

當然,我們可以將這個區(qū)間分得更細,比如設置 10 毫秒為一個區(qū)間。因為區(qū)間分得越細,計算數據就越精密,不過資源損耗也越多。

這個算法目前看來似乎已經能滿足我們的需求了。不過,我們的場景是這樣的:庫存中只有 100 個商品,如果我們想把 TPS 控制在每秒 100 筆,將滑動時間窗口設置為 1 秒就行,即被分成 10 個區(qū)間,每個區(qū)間 100 毫秒,此時就會出現(xiàn)在第 1 個 100ms 請求已經超出了 100 個的情況,也就是說商品已經被秒光。

這時問題就來了,什么人能在 100ms 內完成點擊購買、下單、提交訂單整個流程?我只能說機器人可以做到,也就是說秒殺商品基本是給機器人準備的,這并不是我們想要的結果。

(三)漏桶算法

關于漏桶算法的實現(xiàn)思路,我們先通過一張圖進行說明,這樣就能立馬理解了。


file

從上圖中,我們可以看到漏桶算法的實現(xiàn)思路分為 3 個步驟:

  1. 任意請求進來后直接進入漏桶排隊;
  2. 以特定的速率處理漏桶隊列里面的請求;
  3. 超出漏桶負載范圍的新請求直接拋棄掉,無法進入排隊隊列。

結合上方在 1 秒內控制 100 個請求的例子,我們可以把輸出速率設置為 100r/s(即每 10ms 處理一次請求),再把桶的大小設置為 100 。

因為漏桶算法是按照先進先出的原則處理請求,所以會出現(xiàn)最終被處理的請求還是前面那 100 個,這就與滑動時間窗口計數方法遇到的問題一樣了。

那如果我們把桶的大小設置為 1,不就可以達到我們的目的了。不過我們還有其他考慮,之后會進一步說明。

(四)令牌桶算法

令牌桶算法的實現(xiàn)思路是這樣的:

  1. 按照特定的速率產生 tokens 并存放在令牌桶中,如果令牌桶滿了,新的令牌不再產生;
  2. 新進來的請求如果需要處理,則消耗桶中的一個令牌;
  3. 如果桶中有令牌,直接消耗一個;
  4. 如果桶中沒有令牌,進入一個隊伍中等待新的令牌;
  5. 如果等待令牌的隊伍滿了,新請求就會直接被拋棄掉。
file

再結合上方在 1 秒內控制 100 個請求的例子,如果使用令牌桶算法,我們需要先把令牌的產生速率設置為 100/s,等待令牌的排隊隊列設為 0,這樣就能夠滿足我們的秒殺限流的訴求了。

那令牌桶數量到底設置為多少呢?如果設置為 100,假設令牌在秒殺前已經產生,那么秒殺開始時請求數已經是 100 了,前 100 個請求就會被放行,也就是說機器人又搶到了所有商品。

此時,我們可以設置令牌桶數量為 10,這樣可以保證頂多 10 個機器人搶到商品。

三、方案實現(xiàn)

搞清楚限流的常見算法后,我們可以進行方案實現(xiàn)了,不過需要考慮以下 4 個問題。

(一)使用令牌桶還是漏桶模式?

剛剛提到令牌桶算法與漏桶算法都可以滿足我們的訴求,但是做限流時,我們希望這個算法不僅可以用于秒殺功能,還可以用于其他限流場景。

而使用漏桶算法存在一個缺陷:比如服務器空閑時,理論上服務器可以直接處理一次洪峰,但是漏桶的機制是請求處理速率恒定,因此,前期服務器資源只能根據恒定的漏水速率逐步處理請求,無法用于其他限流場景。

要是使用令牌桶算法就不存在這個問題了,因為我們可以把令牌桶一下子裝滿。因此,針對這個問題,我們最終使用的是令牌桶。

(二)在 Nginx 中實現(xiàn)限流還是在網關層中實現(xiàn)限流?

在上述業(yè)務場景中,最終我們決定在網關層實現(xiàn)限流的原因有兩點。

  • Nginx 中有一個限流插件,它可以對單個用戶的請求數做限制,不過它基于漏桶算法;
  • 當時我們希望可以動態(tài)調整限流的相關配置,因為我們對 Nginx+Lua 不熟悉,以至于配置管理沒辦法直接操作 Nginx 中的數據。

(三)使用分布式限流還是單機限流?

如果使用單機限流的方式,我們需要提前算好服務器的數量,然后把 100/s 的 TPS 平分到各個服務器上進行一層換算。

如果使用分布式限流的方式,比如我們把令牌桶的數據存放在 Redis 中,即每次請求都需要訪問 Redis,因秒殺開始時,下單的請求數往往很大,Redis 未必能承受住如此大的 QPS。

兩害相權取其輕,最終我們決定使用單機限流的方式。

(四)使用哪個開源技術?

最終,我們使用開源庫 Google-Guava 中的 RateLimiter 的相關類來實現(xiàn)限流,它是基于令牌桶算法的實現(xiàn)庫。

在使用開源技術之前,我們需要先定義一個 Zuul 的 filter,再使用 Guava 的 RateLimiter 對提交訂單的 API 請求進行過濾。

在使用 RateLimiter 的過程中,我們需要設置如下 3 個配置項。

  • permitsPerSecond:每秒允許的請求數。
  • warmupPeriod:令牌桶多久滿。
  • tryAcquire 的超時時間:當令牌桶為空時,可以等待新的令牌多久。

針對以上配置項,我們是這樣配置的:

  • permitsPerSecond 設置為 100/10,100 代表想達到的 TPS,10 是代表網關節(jié)點 10 臺;
  • warmupPeriod 設置為 100ms;
  • tryAcquire 的超時時間設置為 0,即拿不到令牌的請求直接拋棄掉,它無須等待。

關于以上配置內容我們詳細展開說明下:permitsPerSecond 為 10,說明 1 秒可以產生 10 個令牌。warmupPeriod=100ms,代表從開始到令牌桶塞滿需要 100ms,即令牌桶的大小是 1,如果我們有 10 臺網關服務器,那么總令牌桶的大小就是 10。(前面我們提到過,為防止搶到物品的都是機器人,我們需要把令牌桶設置為 10。)

四、限流方案注意事項

在做限流方案時,我曾經遇到過不少的坑,這一講我把相關的注意事項總結如下,希望對你有所幫助。

(一)限流返回給客戶端的錯誤代碼

為了給用戶帶來好的體驗,用戶界面上盡量別出現(xiàn)錯誤,因此限流后被拋棄的請求應該返回一個特制的 HTTP CODE,供客戶端進行特殊處理。

(二)實時監(jiān)控

在實際工作中,我們最好對限流日志隨時做好記錄并實時統(tǒng)計,這樣有助于我們實時監(jiān)控限流情況,一旦出現(xiàn)意外,可以及時處理。

(三)實時配置

因為限流功能還需要應用到秒殺以外的場景,所以最好在配置中心就可以實現(xiàn)對令牌桶的動態(tài)管理+實時設置,這樣也方便我們管理其他的限流場景。

(四)秒殺以外的場景的限流配置

在這次秒殺活動中,我們可以簡單換算出控制在 100 的 TPS,而在平時的限流場景中,TPS 或 QPS(其他場景可能不使用 TPS)需要根據實際的壓力測試結果來計算限流的正確配置。

五、聯(lián)系我

微信公眾號:服務端技術精選

個人博客網站:http://www.jiangyi.cool

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容