請求速率限制

今天我們要討論的是如何對 API 進行限流。假設我們有一個 API,多個用戶在訪問它,我們總是有可能遇到某個用戶開始大量請求我們的服務,從而導致服務器超載,阻止其他用戶訪問該服務。

這個用戶可能是一個惡意行爲者,試圖攻擊服務,或者只是由於某人的代碼中存在錯誤。但無論是哪種情況,我們都希望確保能夠消除這種情況的可能性。這種情況會導致其他用戶體驗差,並可能增加成本。因此,如果我們要防止某個特定用戶一次發出過多請求,我們首先需要弄清楚如何在某些服務中實際識別該用戶。

在某些服務中,用戶可能只需通過一個密鑰來訪問 API。這就是在認證系統中出現的情況。我們爲每個用戶提供一個特定的密鑰,授權他們訪問該服務。因爲每個用戶的密鑰不同,我們可以利用該密鑰唯一地識別用戶。

然而,對於公共 API,如果用戶沒有使用密鑰來訪問 API,這可能就不那麼容易了。我們需要通過其他方式來識別用戶。通常情況下,我們會使用請求來源的 IP 地址。然而,在大多數網絡中,我們有一個路由器,它執行網絡地址轉換(NAT),因此實際上訪問 API 的 IP 地址將是該路由器的公共 IP 地址,而不是連接到該網絡的計算機的私有 IP 地址。

例如,如果某人去了一個圖書館、大學或酒店,可能會有數百臺甚至數千臺計算機使用相同的公共 IP 地址。所以,我們不想基於公共 IP 地址來限流,因爲這可能會導致惡意用戶阻止同一網絡上的其他用戶正常訪問該服務。

沒有完美的方法來完全唯一地識別一個設備,但通過將路由器的公共 IP 地址與客戶端的特定信息結合,我們可以做出相當準確的推測。例如,在下面的例子中,我們看到我們不僅使用公共 IP 地址,還結合了客戶端使用的瀏覽器及其版本。在這種情況下,我們有三個客戶端在網絡上,一個使用 Safari,另一個使用 Chrome 版本 101,第三個使用 Chrome 版本 119。

通過將瀏覽器版本與公共 IP 地址結合,我們可以唯一地識別所有訪問我們服務的客戶端,無論它們是否位於網絡地址轉換路由器後面。

爲了進一步增強指紋識別,我們可以利用更多的信息。正如我們剛纔提到的,我們可以使用瀏覽器及其版本,這些信息來自用戶代理(User-Agent)頭。我們也可以使用其他頭部及其獨特屬性,還可以利用一些有趣的東西,比如頭部出現的順序來識別請求的瀏覽器類型。我們還可以使用 TLS 握手中的信息來確定客戶端的類型,因爲不同的瀏覽器、瀏覽器版本和請求庫在 TLS 握手時會請求不同的加密算法。最後,如果我們從瀏覽器訪問 API,我們還可以通過瀏覽器 JavaScript 引擎中的特定漏洞來識別瀏覽器。不同版本和類型的瀏覽器通常會有一些在不同引擎中表現略有不同的隨機 JavaScript 函數,因此我們可以利用這些漏洞來推測是哪種瀏覽器或客戶端在訪問。

這些信息在某種程度上是可以被客戶端操控的。例如,瀏覽器版本只是包含在用戶代理頭部中,任何請求我們的 API 的客戶端都可以輕鬆地修改用戶代理。因此,值得注意的是,這種方法非常容易被繞過。

那麼,瞭解瞭如何識別特定客戶端後,我們來看一下可以用來限制該客戶端發出請求數量的一些方法。最簡單的算法可能是固定窗口算法,顧名思義,它涉及創建一個固定時間窗口,並限制該窗口內的請求次數。

例如,在這個例子中,我們限制每個時間窗口內最多隻能發送四個請求。在這個特定的時間窗口內,四個請求都通過,但如果我們在短時間內發出五個請求,最後一個請求將被丟棄。

由於時間窗口是固定的,這可能在某些邊緣情況下導致一些奇怪的行爲。例如,在下方的例子中,我們看到我們在窗口的末尾發出請求,然後在下一個窗口開始時發出四個請求。這樣,我們就可以在特定時間段內大大超過限流。這也意味着,即使在一段很長的時間內我們沒有發出請求,仍然會有請求被丟棄,因爲在時間窗口開始時就發出了請求。

一個稍微更優的解決方案是使用滑動窗口。與固定時間窗口不同,滑動窗口沒有在固定的時間點開始,而是表示任何特定時間長度內不能有超過四個請求。在這種方法下,我們使用了多個時間窗口,每個時間窗口內的請求次數保持在限制範圍內。即使我們在時間窗口之間的間隙期發出了大量請求,我們仍然會看到在這個特定的時間段內,超過限制的請求會被丟棄。

然而,這種滑動窗口方法在客戶端使用 API 時仍然會產生一些不穩定的行爲。如果我們在開始時發出大量請求,我們可能需要等待直到時間窗口結束,才能再發出請求,然後再次發出大量請求,這時又需要等待。因此,客戶端訪問我們的 API 時,會出現不可預測的延遲。

爲了避免這些不穩定性,我們希望客戶端能夠在請求期間自由地進行一次爆發性請求,然後通過丟棄超出限流的請求來將請求速度降低到一個合理的速率。爲了解決這個問題,常用的做法是漏桶算法。

漏桶算法的類比是:我們有一個裝水的桶,桶底有一個孔,水會以恆定的速度從桶底流出,不管我們往桶裏倒多少水,桶漏出的水流量始終是恆定的,任何溢出的水會被從桶中倒出。回到限流 API 的場景,每次請求就相當於往桶裏倒水,水以恆定的速度從桶裏流出,若桶滿了,我們會丟棄這個請求。漏桶算法給客戶端提供了一個緩衝區,允許它們在短時間內進行爆發性請求,同時在爆發後又能有可預測的行爲。

在這些例子中,我們限制了桶的大小爲 3,所以如果桶中的水量超過 3,就會溢出並丟棄請求。隨着請求的增加,我們觀察到在某些情況下,儘管我們在時間窗口內發出了更多請求,但由於桶的泄漏,最終還是會按預定的速率處理請求,丟棄過多的請求。

總之,限流可以通過多種方式實現,每個 API 和客戶端根據需求有所不同,但幾乎在所有 API 中,添加某種限流機制都是非常重要的,以確保惡意攻擊者無法摧毀你的系統或增加你的成本。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/R4LLAl_PLAlT7B1GlKW_nw?poc_token=HF-DMWejP23BvaGw9ZKV5uswVwt1CUglDUDmaCPl