面試官:如何設計一個可擴展的限流算法?

限流(Rate Limiting,即速率限制)通過限制每個用戶調用API的頻率來防止API被過度使用,這可以防止他們因疏忽或惡意導致的API濫用。在沒有速率限制的情況下,每個用戶可以隨心所欲地請求,這可能會導致“峰值”請求,從而導致其他用戶得不到響應。在啟用速率限制之後,它們的請求將被限制為每秒固定的數量。

面試官:如何設計一個可擴展的限流算法?

在示例圖表中,你可以看到速率限制如何在一段時間內阻塞請求。API最初每分鐘接收4個請求,用綠色表示。當12:02啟用速率限制時,以紅色顯示的其他請求將被拒絕。速率限制對於公共API是非常重要的,因為你想要為每個消費者(API調用者)維護良好的服務質量,即使有些用戶獲取了超出其公平配額的服務。計算密集型的端點特別需要速率限制——特別是通過自動伸縮或AWS Lambda和OpenWhisk等按計算付費服務來提供服務時。你還可能希望對提供敏感數據的API進行評級,因為如果攻擊者在某些不可預見的事件中獲得訪問權限,這可能會限制暴露的數據。實際上有許多不同的方法來實現速率限制,我們將探討不同速率限制算法的優缺點。我們還將探討跨集群擴展時出現的問題。最後,我們將向你展示一個如何使用Kong快速設置速率限制的示例,Kong是最流行的開源API網關。

速度限制算法

有各種各樣的速率限制算法,每一種都有自己的優點和缺點。讓我們回顧一下,這樣你就可以根據自己的需要選擇最好的限流算法。

漏桶算法

漏桶算法(Leaky Bucket,與令牌桶密切相關)是這樣一種算法,它提供了一種簡單、直觀的方法來通過隊列限制速率,你可以將隊列看作一個存儲請求的桶。當一個請求被註冊時,它被附加到隊列的末尾。每隔一段時間處理隊列上的第一項。這也稱為先進先出(FIFO)隊列。如果隊列已滿,則丟棄(或洩漏)其他請求。

面試官:如何設計一個可擴展的限流算法?

這種算法的優點是它可以平滑請求的爆發,並以近似平均的速度處理它們。它也很容易在單個服務器或負載均衡器上實現,並且在有限的隊列大小下對於每個用戶都是內存有效的。然而,突發的訪問量會用舊的請求填滿隊列,並使最近的請求無法被處理。它也不能保證在固定的時間內處理請求。此外,如果為了容錯或增加吞吐量而負載平衡服務器,則必須使用策略來協調和強制它們之間的限制。稍後我們將討論分佈式環境的挑戰。

固定窗口算法

在固定窗口(Fixed Window)算法中,使用n秒的窗口大小(通常使用對人類友好的值,如60秒或3600秒)來跟蹤速率。每個傳入的請求都會增加窗口的計數器。如果計數器超過閾值,則丟棄請求。窗口通常由當前時間戳的層定義,因此12:00:03的窗口長度為60秒,應該在12:00:00的窗口中。

面試官:如何設計一個可擴展的限流算法?

這種算法的優點是,它可以確保處理更多最近的請求,而不會被舊的請求餓死。然而,發生在窗口邊界附近的單個流量突發會導致處理請求的速度增加一倍,因為它允許在短時間內同時處理當前窗口和下一個窗口的請求。另外,如果許多消費者等待一個重置窗口,例如在一個小時的頂部,那麼他們可能同時擾亂你的API。

滑動日誌算法

滑動日誌(Sliding Log)速率限制涉及到跟蹤每個使用者請求的時間戳日誌。這些日誌通常存儲在按時間排序的散列集或表中。時間戳超過閾值的日誌將被丟棄。當新請求出現時,我們計算日誌的總和來確定請求率。如果請求將超過閾值速率,則保留該請求。

面試官:如何設計一個可擴展的限流算法?

該算法的優點是不受固定窗口邊界條件的限制,速率限制將嚴格執行。此外,因為滑動日誌是針對每個消費者進行跟蹤的,所以不會出現對固定窗口造成挑戰的踩踏效應。但是,為每個請求存儲無限數量的日誌可能非常昂貴。它的計算成本也很高,因為每個請求都需要計算使用者先前請求的總和,這些請求可能跨越一個服務器集群。因此,它不能很好地處理大規模的流量突發或拒絕服務攻擊。

滑動窗口

這是一種將固定窗口算法的低處理成本與改進的滑動日誌邊界條件相結合的混合方法。與固定窗口算法一樣,我們跟蹤每個固定窗口的計數器。接下來,我們根據當前的時間戳計算前一個窗口請求率的加權值,以平滑突發的流量。例如,如果當前窗口通過了25%,那麼我們將前一個窗口的計數加權為75%。跟蹤每個鍵所需的相對較少的數據點允許我們在大型集群中擴展和分佈。

面試官:如何設計一個可擴展的限流算法?

我們推薦使用滑動窗口方法,因為它可以靈活地調整速率限制,並且具有良好的性能。速率窗口是它向API消費者提供速率限制數據的一種直觀方式。它還避免了漏桶的飢餓問題和固定窗口實現的突發問題。

分佈式系統中的速率限制

同步策略如果希望在使用多個節點的集群時實施全局速率限制,則必須設置策略來實施該限制。如果每個節點都要跟蹤自己的速率限制,那麼當請求被髮送到不同節點時,使用者可能會超過全局速率限制。實際上,節點的數量越大,用戶越有可能超過全侷限制。執行此限制的最簡單方法是在負載均衡器中設置粘性會話,以便每個使用者都被精確地發送到一個節點。缺點包括缺乏容錯性和節點過載時的縮放問題。允許更靈活的負載平衡規則的更好解決方案是使用集中的數據存儲,如Redis或Cassandra。這將存儲每個窗口和消費者的計數。這種方法的兩個主要問題是增加了向數據存儲發出請求的延遲,以及競爭條件(我們將在下面討論)。

面試官:如何設計一個可擴展的限流算法?

競態條件

集中式數據存儲的最大問題之一是高併發請求模式中的競爭條件。當你使用一種簡單的“get-then-set”方法時,就會發生這種情況,在這種方法中,你檢索當前速率限制計數器,增加它的值,然後將其推回到數據存儲中。這個模型的問題是,在執行一個完整的讀遞增存儲週期時,可能會出現額外的請求,每個請求都試圖用無效的(較低的)計數器值存儲遞增計數器。這允許使用者發送非常高的請求率來繞過速率限制控制。

面試官:如何設計一個可擴展的限流算法?

避免這個問題的一種方法是在有問題的密鑰周圍放置一個“鎖”,防止任何其他進程訪問或寫入計數器。這將很快成為一個主要的性能瓶頸,而且伸縮性不好,特別是在使用諸如Redis之類的遠程服務器作為備份數據存儲時。更好的方法是使用“先設置後獲取”的心態,依賴於原子操作符,它們以一種非常高性能的方式實現鎖,允許你快速增加和檢查計數器值,而不讓原子操作成為障礙。

性能優化

使用集中式數據存儲的另一個缺點是,在檢查速率限制計數器時增加了延遲。不幸的是,即使是檢查像Redis這樣的快速數據存儲,也會導致每個請求增加毫秒的延遲。為了以最小的延遲確定這些速率限制,有必要在內存中進行本地檢查。這可以通過放鬆速率檢查條件和使用最終一致的模型來實現。例如,每個節點可以創建一個數據同步週期,該週期將與中央數據存儲同步。每個節點定期將每個使用者的計數器增量和它看到的窗口推送到數據存儲,數據存儲將自動更新這些值。然後,節點可以檢索更新後的值來更新其內存版本。集群內節點之間的這種收斂→發散→再收斂的循環最終是一致的。

面試官:如何設計一個可擴展的限流算法?

節點聚合的週期速率應該是可配置的。當流量分佈在群集中的多個節點上時(例如,當坐在一個輪詢調度平衡器後面時),較短的同步間隔將導致較少的數據點分散,而較長的同步間隔將對數據存儲施加較小的讀/寫壓力,更少的開銷在每個節點上獲取新的同步值。

使用Kong快速設置速率限制

Kong是一個開源的API網關,它使構建具有速率限制的可伸縮服務變得非常容易。它被全球超過300,000個活動實例使用。它可以完美地從單個的Kong節點擴展到大規模的、跨越全球的Kong集群。Kong位於API前面,是上游API的主要入口。在處理請求和響應時,Kong將執行你決定添加到API中的任何插件。

面試官:如何設計一個可擴展的限流算法?

Kong的速率限制插件是高度可配置的。它提供了為每個API和消費者定義多個速率限制窗口和速率的靈活性。它支持本地內存、Redis、Postgres和Cassandra備份數據存儲。它還提供了各種數據同步選項,包括同步和最終一致的模型。你可以在其中一臺開發機器上快速安裝Kong來測試它。我最喜歡的入門方式是使用AWS雲形成模板,因為只需幾次單擊就可以獲得預先配置好的開發機器。只需選擇一個HVM選項,並將實例大小設置為使用t2.micro,這些對於測試都是負擔得起的。然後ssh到新實例上的命令行進行下一步。

在Kong上添加API

下一步是使用Kong的admin API在Kong上添加一個API。我們將使用httpbin作為示例,它是一個針對API的免費測試服務。get URL將把我的請求數據鏡像成JSON。我們還假設Kong在本地系統上的默認端口上運行。

<code>curl -i -X POST \\
--url http://localhost:8001/apis/ \\
--data 'name=test' \\
--data 'uris=/test' \\
--data 'upstream_url=http://httpbin.org/get'
/<code>

現在Kong意識到每個發送到“/test”的請求都應該代理到httpbin。我們可以向它的代理端口上的Kong發出以下請求來測試它:

<code>curl http://localhost:8000/test
{
"args": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "curl/7.51.0",
"X-Forwarded-Host": "localhost"
},
"origin": "localhost, 52.89.171.202",
"url": "http://localhost/get"
}
/<code>

它還是好的!Kong已經接收了請求並將其代理到httpbin,httpbin已將我的請求頭和我的原始IP地址進行了鏡像。

添加基本的速率限制

讓我們繼續,通過使用社區版的限速插件[1]添加限速功能來保護它不受過多請求的影響,每個消費者每分鐘只能發出5個請求:

<code>curl -i -X POST http://localhost:8001/apis/test/plugins/ \\
-d "name=rate-limiting" \\
-d "config.minute=5"
/<code>

如果我們現在發出超過5個請求,Kong會回覆以下錯誤信息:

<code>curl http://localhost:8000/test
{
"message":"API rate limit exceeded"
}
/<code>

看上去不錯!我們在Kong上添加了一個API,並且僅在兩個HTTP請求中向Kong的admin API添加了速率限制。它默認使用固定的窗口來限制IP地址的速率,並使用默認的數據存儲在集群中的所有節點之間進行同步。有關其他選項,包括每個用戶的速率限制或使用其他數據存儲(如Redis),請參閱文檔[1]。

企業版Kong,更好的性能

企業版[2]的速率限制增加了對滑動窗口算法的支持,以更好地控制和性能。滑動窗口可以防止你的API在窗口邊界附近重載,如上面的部分所述。對於低延遲,它使用計數器的內存表,可以使用異步或同步更新跨集群進行同步。這提供了本地閾值的延遲,並且可以跨整個集群擴展。第一步是安裝企業版的Kong。然後,可以配置速率限制、以秒為單位的窗口大小和同步計數器值的頻率。它真的很容易使用,你可以得到這個強大的控制與一個簡單的API調用:

<code>curl -i -X POST http://localhost:8001/apis/test/plugins \\
-d "name=rate-limiting" \\
-d "config.limit=5" \\
-d "config.window_size=60" \\
-d "config.sync_rate=10"
/<code>

企業還增加了對Redis Sentinel的支持,這使得Redis高可用性和更強的容錯能力。你可以閱讀更多的企業速率限制插件文檔。


分享到:


相關文章: