我們一直在限制自己的服務
Mythex 的 API 允許每個網絡地址每分鐘 300 個請求。這個限制是為了保護我們,免受單一用戶或腳本對 API 的大量請求。然而在正式環境中,它開始拒絕我們自己的服務——也就是服務已發佈應用程式的邊緣部分,以及運行 AI 代理的服務——因為它們每一個都只會從少數幾個地址連接 API。現在,帶有我們內部憑證的請求獲豁免,其他所有請求的限制則與以前完全相同。
症狀
API 開始以 429 Too Many Requests 回應我們自己的一些內部呼叫。三種呼叫受到影響,而每一種都與使用 Mythex 的人息息相關:
- 查詢哪個應用程式位於某個自訂網域。 當訪客在自己的網域上開啟已發佈的應用程式時,我們的邊緣會向 API 查詢該網站位於何處。在這裡出現 429,就代表一位訪客無法連到網站。
- 儲存正在運行的代理回合。 代理工作時,其進度會邊進行邊儲存,因此回覆在重新整理或連線中斷後仍會保留。在這裡出現 429,就代表進度沒有被儲存。
- 檢查預覽的存取權限。 你正在建構的應用程式的預覽是受閘門控制的,而閘門會向 API 查詢。在這裡出現 429,就代表預覽無法開啟。
這些都不是人在呼叫 API。全都是我們自己。
發生原因
以地址為單位的速率限制只做一個假設:一個地址大致上就是一位呼叫者。對人來說,這已經夠接近了。對我們自己的基礎設施來說,這個假設完全不成立。
- 負責已發佈應用程式和預覽的邊緣服務,會從幾個共用的輸出地址連接 API。每位訪客造訪每個已發佈的應用程式,以及每次預覽檢查,看起來都像是來自這幾個地址之一。
- 負責運行代理回合的協調器,會從一個容器地址連接 API。每個正在運行的回合,為每位用戶,都共用這個地址。
於是,本來是為一個人而設的預算,被分攤到整個服務的所有流量上。這些服務合起來把它用光了,而限制器只是按設定行事:它拒絕了它們。
修復方法
我們自己的服務本來就能證明自己的身分。每個內部呼叫都帶有一個內部權杖,而 API 內部區段下的每條路由,在做任何事之前都會先檢查該權杖。
速率限制器現在也使用同一項檢查。權杖與內部權杖相符的請求會被列入允許清單,不計入每個地址的預算。其他一切都與以前一樣計算:每個地址每分鐘 300 個請求。
有兩個細節令這不致於變成一個漏洞:
- 錯誤的權杖不會獲得豁免。 允許清單只會匹配真正的內部權杖。使用偽造權杖的請求會被計算並同樣受到限制,而內部路由仍會以
401 Unauthorized拒絕它。 - 限制數字本身沒有改變。 我們沒有為了騰出空間而提高數字。提高數字只會暫時掩蓋問題,流量再次增長時又會再出錯,而且還會削弱限制原本所要保護的情況。
我們把限制器的設定移入一個小函式,並為它撰寫了測試。其中一個測試以內部權杖發送 20 個請求,給一個只允許 5 個請求的限制器,並預期全部 20 個都會成功。另一個測試從同一地址發送普通請求,預期前五個會成功,之後的則會被拒絕。第三個測試以偽造的權杖發送請求,預期它們會以同樣的方式被拒絕。
我們的心得
- 限制本身帶有假設。「一個地址,一位呼叫者」已內建於以地址為單位的限制之中。一旦你自己的服務位於共用地址之後,這個假設就不再成立——而很多服務都是如此:負載平衡器、邊緣網絡、容器、辦公室網絡。
- 以身分而非地址區分自己的流量。 地址只是流量抵達的方式,而不是誰發送了它。我們本來就有一個能證明呼叫屬於我們的憑證;限制器只是沒有去看它。
- 不要靠提高數字來解決。 更大的限制會讓 429 暫時消失,卻保護得更少,而且日後還會再次失敗。
- 留意誰被限制了。 落在你自己服務上的 429,是關於你的限制的一份錯誤報告,而不是限制器正在運作的跡象。
這對在 Mythex 上建構的人意味著什麼
你無需作任何改動。已發佈的應用程式在自訂網域上、代理回合和預覽,都不再依賴 Mythex 其他部分的忙碌程度。你的應用程式自身的流量由你的應用程式自行負責:這裡描述的限制是針對我們的 API,而不是針對你所發佈的應用程式。
如果你運行自己的後端,同樣的陷阱也適用於你自行加入的任何速率限制。我們的指南什麼是 API和AI 建構應用程式的安全檢查清單是很好的起點。至於我們自己的基礎設施妨礙自己的另一個例子,請參閱我們如何把部署的 502 錯誤降至零。