網誌

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

Mythex 的 API 允許每個網絡地址每分鐘 300 個請求。這個限制是為了保護我們,免受單一用戶或腳本對 API 的大量請求。然而在正式環境中,它開始拒絕我們自己的服務——也就是服務已發佈應用程式的邊緣部分,以及運行 AI 代理的服務——因為它們每一個都只會從少數幾個地址連接 API。現在,帶有我們內部憑證的請求獲豁免,其他所有請求的限制則與以前完全相同。

症狀

API 開始以 429 Too Many Requests 回應我們自己的一些內部呼叫。三種呼叫受到影響,而每一種都與使用 Mythex 的人息息相關:

  • 查詢哪個應用程式位於某個自訂網域。 當訪客在自己的網域上開啟已發佈的應用程式時,我們的邊緣會向 API 查詢該網站位於何處。在這裡出現 429,就代表一位訪客無法連到網站。
  • 儲存正在運行的代理回合。 代理工作時,其進度會邊進行邊儲存,因此回覆在重新整理或連線中斷後仍會保留。在這裡出現 429,就代表進度沒有被儲存。
  • 檢查預覽的存取權限。 你正在建構的應用程式的預覽是受閘門控制的,而閘門會向 API 查詢。在這裡出現 429,就代表預覽無法開啟。

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

發生原因

以地址為單位的速率限制只做一個假設:一個地址大致上就是一位呼叫者。對人來說,這已經夠接近了。對我們自己的基礎設施來說,這個假設完全不成立。

  • 負責已發佈應用程式和預覽的邊緣服務,會從幾個共用的輸出地址連接 API。每位訪客造訪每個已發佈的應用程式,以及每次預覽檢查,看起來都像是來自這幾個地址之一。
  • 負責運行代理回合的協調器,會從一個容器地址連接 API。每個正在運行的回合,為每位用戶,都共用這個地址。

於是,本來是為一個人而設的預算,被分攤到整個服務的所有流量上。這些服務合起來把它用光了,而限制器只是按設定行事:它拒絕了它們。

修復方法

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

速率限制器現在也使用同一項檢查。權杖與內部權杖相符的請求會被列入允許清單,不計入每個地址的預算。其他一切都與以前一樣計算:每個地址每分鐘 300 個請求。

有兩個細節令這不致於變成一個漏洞:

  1. 錯誤的權杖不會獲得豁免。 允許清單只會匹配真正的內部權杖。使用偽造權杖的請求會被計算並同樣受到限制,而內部路由仍會以 401 Unauthorized 拒絕它。
  2. 限制數字本身沒有改變。 我們沒有為了騰出空間而提高數字。提高數字只會暫時掩蓋問題,流量再次增長時又會再出錯,而且還會削弱限制原本所要保護的情況。

我們把限制器的設定移入一個小函式,並為它撰寫了測試。其中一個測試以內部權杖發送 20 個請求,給一個只允許 5 個請求的限制器,並預期全部 20 個都會成功。另一個測試從同一地址發送普通請求,預期前五個會成功,之後的則會被拒絕。第三個測試以偽造的權杖發送請求,預期它們會以同樣的方式被拒絕。

我們的心得

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

這對在 Mythex 上建構的人意味著什麼

你無需作任何改動。已發佈的應用程式在自訂網域上、代理回合和預覽,都不再依賴 Mythex 其他部分的忙碌程度。你的應用程式自身的流量由你的應用程式自行負責:這裡描述的限制是針對我們的 API,而不是針對你所發佈的應用程式。

如果你運行自己的後端,同樣的陷阱也適用於你自行加入的任何速率限制。我們的指南什麼是 API和AI 建構應用程式的安全檢查清單是很好的起點。至於我們自己的基礎設施妨礙自己的另一個例子,請參閱我們如何把部署的 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 简体中文 繁體中文(台灣)