部落格

我們一直在限制自己的服務

Mythex 的 API 允許來自單一網路位址的每分鐘 300 個請求。這個限制的存在,是為了保護我們不被單一個人或腳本灌爆。在正式環境中,它開始拒絕我們自己的服務——負責提供已發佈 App 的邊緣服務,以及執行 AI 代理人的服務——因為它們每一個都只從少數幾個位址連向 API。攜帶我們內部憑證的請求現在可以豁免,其他所有請求的限制則與以前完全相同。

症狀

API 開始以 429 Too Many Requests 回應我們部分的內部呼叫。被波及的有三類呼叫,而每一類都與使用 Mythex 的人息息相關:

  • 查詢某個自訂網域上住的是哪個 App。 當訪客在自己的網域上開啟已發佈的 App 時,我們的邊緣會詢問 API 該網站位於何處。在這裡收到 429,就表示一位訪客無法連到網站。
  • 儲存正在執行的代理人回合。 代理人工作時,進度會邊做邊儲存,使回覆在重新整理或連線中斷後仍然保留。在這裡收到 429,就表示進度沒有被儲存。
  • 檢查預覽的存取權。 你正在建置的 App 的預覽是受閘門保護的,而這道閘門會向 API 詢問。在這裡收到 429,就表示預覽無法開啟。

這些都不是人在呼叫 API。全部都是我們自己。

為什麼會這樣

每位址的速率限制只做了一個假設:一個位址大致就是一個呼叫者。對一般人來說,這大致成立。但對我們自己的基礎設施來說,這完全錯誤。

  • 為已發佈 App 與預覽提供服務的邊緣服務,從 少數幾個共用的對外位址 連向 API。每位訪客造訪的每個已發佈 App,以及每次預覽檢查,看起來都來自這幾個位址之一。
  • 編排器——執行代理人回合的服務——從 單一容器位址 連向 API。每個正在執行的回合、為每位使用者服務的,都共用這一個位址。

因此,本來是為一個人設計的配額,被整個服務的所有流量瓜分了。這些服務合起來把它用完,而限制器則照它的設定做事:拒絕了它們。

修正方式

我們自己的服務本來就能證明它們的身分。每個內部呼叫都帶有一個內部權杖,而 API 內部區段下的每條路由,在做任何事之前都會先檢查這個權杖。

速率限制器現在使用同樣的檢查。權杖與內部權杖相符的請求會被列入允許清單,不計入每位址的配額。其他所有請求的計算方式則與以前完全相同:每位址每分鐘 300 個請求。

有兩個細節讓這件事沒有變成漏洞:

  1. 錯誤的權杖不會被放行。 允許清單只會比對真正的內部權杖。帶有偽造權杖的請求會被計數,並像任何其他請求一樣被限流,而內部路由仍然會以 401 Unauthorized 拒絕它。
  2. 限制數字本身沒有改變。 我們沒有為了騰出空間而提高數字。提高數字會讓 429 暫時消失,卻會掩蓋問題,等流量再次增長時又會失敗;而且會削弱限制本來要防護的那種情況。

我們把限制器的設定移到一個小函式中,並針對它撰寫測試。其中一個測試用內部權杖送出 20 個請求,對一個只允許 5 個的限制器,預期全部 20 個都成功。另一個測試從同一位址送出一般請求,預期前五個成功,之後的則被拒絕。第三個測試送出偽造權杖的請求,預期它們同樣被拒絕。

我們從中學到什麼

  • 限制會帶著假設。 「一個位址,一個呼叫者」內建在每位址的限制中。一旦你自己的服務位於共用位址之後,這個假設就不再成立——而且很多服務確實是如此:負載平衡器、邊緣網路、容器、辦公室網路。
  • 用身分而非位址來區分自己的流量。 位址只是流量抵達的方式,而不是誰送出的。我們本來就有一個能證明呼叫屬於我們自己的憑證;只是限制器沒有去看它。
  • 不要靠提高數字來解決。 更大的限制會讓 429 暫時消失,但保護力會變弱,之後還是會再次失敗。
  • 注意是誰被限流。 落在你自己服務上的 429,是一份關於你限制的錯誤報告,而不是限制器正常運作的證明。

這對在 Mythex 上建置的你意味著什麼

你不需要做任何改變。已發佈 App 的自訂網域、代理人回合與預覽,都不再取決於 Mythex 其他部分有多忙碌。你的 App 自己的流量,是你的 App 自己的事:這裡描述的限制是在我們的 API 上,而不是在你發佈的 App 上。

如果你自己運行後端,同樣的陷阱也適用於你加入的任何速率限制。我們的指南 什麼是 API 與 AI 建置 App 的安全檢查清單 是很好的起點。至於我們自己的基礎設施妨礙自己的另一個案例,請參見我們如何把部署的 502 錯誤降到零。

Mythex · Pricing · 用 AI 打造任何東西 · 網站與應用程式範本 · Mythex 比較 · AI 應用程式開發詞彙表 · 免費的 AI 應用程式開發工具 · English

English አማርኛ العربية Български বাংলা Bosanski Català Čeština Dansk Deutsch Ελληνικά Español Español (España) Eesti فارسی Suomi Français Français (Canada) ગુજરાતી हिन्दी Hrvatski Magyar Հայերեն Bahasa Indonesia Íslenska Italiano 日本語 ქართული Қазақ тілі ಕನ್ನಡ 한국어 Lietuvių Latviešu Македонски മലയാളം Монгол मराठी Bahasa Melayu မြန်မာ Norsk bokmål Nederlands ਪੰਜਾਬੀ Polski Português (Brasil) Português (Portugal) Română Русский Slovenčina Slovenščina Soomaali Shqip Srpski Svenska Kiswahili தமிழ் తెలుగు ไทย Tagalog Türkçe Українська اردو Tiếng Việt 简体中文 繁體中文(香港)