我們一直在限制自己的服務
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 個請求。
有兩個細節讓這件事沒有變成漏洞:
- 錯誤的權杖不會被放行。 允許清單只會比對真正的內部權杖。帶有偽造權杖的請求會被計數,並像任何其他請求一樣被限流,而內部路由仍然會以
401 Unauthorized拒絕它。 - 限制數字本身沒有改變。 我們沒有為了騰出空間而提高數字。提高數字會讓 429 暫時消失,卻會掩蓋問題,等流量再次增長時又會失敗;而且會削弱限制本來要防護的那種情況。
我們把限制器的設定移到一個小函式中,並針對它撰寫測試。其中一個測試用內部權杖送出 20 個請求,對一個只允許 5 個的限制器,預期全部 20 個都成功。另一個測試從同一位址送出一般請求,預期前五個成功,之後的則被拒絕。第三個測試送出偽造權杖的請求,預期它們同樣被拒絕。
我們從中學到什麼
- 限制會帶著假設。 「一個位址,一個呼叫者」內建在每位址的限制中。一旦你自己的服務位於共用位址之後,這個假設就不再成立——而且很多服務確實是如此:負載平衡器、邊緣網路、容器、辦公室網路。
- 用身分而非位址來區分自己的流量。 位址只是流量抵達的方式,而不是誰送出的。我們本來就有一個能證明呼叫屬於我們自己的憑證;只是限制器沒有去看它。
- 不要靠提高數字來解決。 更大的限制會讓 429 暫時消失,但保護力會變弱,之後還是會再次失敗。
- 注意是誰被限流。 落在你自己服務上的 429,是一份關於你限制的錯誤報告,而不是限制器正常運作的證明。
這對在 Mythex 上建置的你意味著什麼
你不需要做任何改變。已發佈 App 的自訂網域、代理人回合與預覽,都不再取決於 Mythex 其他部分有多忙碌。你的 App 自己的流量,是你的 App 自己的事:這裡描述的限制是在我們的 API 上,而不是在你發佈的 App 上。
如果你自己運行後端,同樣的陷阱也適用於你加入的任何速率限制。我們的指南 什麼是 API 與 AI 建置 App 的安全檢查清單 是很好的起點。至於我們自己的基礎設施妨礙自己的另一個案例,請參見我們如何把部署的 502 錯誤降到零。