使用 Istio 可以很方便地實現速率限制。本文介紹了速率限制的使用場景,使用 memquota\redisquota adapter 實現速率限制的方法,通過配置 rule 實現有條件的速率限制,以及速率限制的原理。
1 使用場景
在許多場景下都需要對服務進行速率限制。
一種常見的場景是防止來自外部服務的過度調用(如爬蟲)。
另一種常見的場景是調用某些收費的外部服務,但是提供了免費配額,可以使用速率限制確保只使用免費的配額。
2 環境准備
-
在 Kubernetes 集群上部署 Istio
-
部署 Bookinfo 示例應用
-
配置 Bookinfo 應用各個微服務的 destinationrule 和 virtualservice
3 使用 Istio 實現速率限制
1. 創建 memquota 或 redisquota 類型的 handler
一個請求到達某個服務時,一般會發生兩次對 Mixer 的調用,一次是前置檢查,一次是遙測報告。每一次調用,Mixer 都需要調用一個或多個適配器。因此需要定義處理速率限制的 handler,可以在 memquota 和 redisquota 這兩種類型的 handler 中選擇一種:
上面的 memquota handler 定義了三種不同的速率限制模式:
-
如果沒有 override 能夠匹配,默認每秒限制 500 次請求;
-
如果請求的 destination 是 reviews,每 5 秒限制 1 次請求;
-
如果請求的 destination 是 productpage,每 5 秒限制 1 次請求。
然后創建 memquota handler:
上面使用的是 memquota handler,memquota handler 綁定在 Mixer 進程上,沒有持久化,無 HA 能力,因此並不適合生產使用。可以用 redisquota handler 替代 memquota handler 實現持久化:
上面定義了一個 redisquota handler,定義的三種速率限制模式和上文的 memquota handler相同。
其中 rateLimitAlgorithm 字段可以選擇 ROLLING_WINDOW 或者 FIXED_WINDOW,兩種算法的介紹見下文“速率限制的原理”一節。
然后創建該 redisquota handler:
2. 創建 quota template 的 instance
不同的適配器需要不同的數據塊作為輸入來進行處理。例如日志適配器需要日志輸入,指標適配器需要指標輸入,認證適配器需要憑據輸入。適配器在請求時消費的數據是由 Mixer 的 template 來描述的,通過 quota template 可以定義 memquota\redisquota handler 需要的 dimension 數據。
編輯 quota_instance.yaml 內容如下:
上面的 quota template 定義了四種可以被 memquota\redisquota 使用的 dimension,可以 override 匹配到某些屬性的請求。比如 destination 會被置為 destination.labels["app"], destination.service.host 和 "unknown" 中第一個不為空的值。
然后執行如下命令創建 quota template 的 instance:
3. 創建 quota rule
Rule 負責通知 Mixer,哪個 instance 應該在什么時候發送給哪個 handler。
編輯 quota_rule.yaml 內容如下:
上面這條 rule 告訴 Mixer 使用上面創建的 handler.memquota\handler.redisquota handler,並將上面創建的 requestcount.quota instance 構建的對象傳遞給該 handler。
然后執行如下命令創建上述 rule:
4. 創建 QuotaSpec
QuotaSpec 對象用於定義每次請求消費的 quota instance 的數量。
編輯 quota_spec.yaml 內容如下:
在上面的 QuotaSpec 中,我們定義每次請求消費 1 個 quota instance。
然后執行如下命令創建上述 QuotaSpec:
5. 創建 QuotaSpecBinding
QuotaSpecBinding 對象用於將上面創建的 QuotaSpec 綁定到需要應用限流的服務上。通過給不同的服務綁定不同的 QuotaSpec,可以實現對於調用代價較高的服務,每個請求消費較多的 quota instance。
編輯 quota_spec_binding.yaml 內容如下:
上面的 QuotaSpecBinding 將 productpage 綁定到名為 request-count 的 QuotaSpec 上。需要注意的是,如果服務的 namespace 和 QuotaSpecBinding 不同,需要指定 namespace。
然后執行如下命令創建上述 QuotaSpec:
6. 在瀏覽器中刷新 productpage 頁面
因為限制了 productpage 只允許每 5 秒 2 個請求,所以如果持續刷新頁面,會看到如下的 RESOURCE_EXHAUSTED:Quota is exhausted for: requestcount。
4 有條件的速率限制
上面只使用了 dimensions 來定義速率限制的條件,還可以在 rule 中使用任意屬性來定義規則。
例如,在某些場景下,我們不需要對已經登錄的用戶進行速率限制。在 bookinfo 示例中,我們通過設置 cookie user=<username> 來識別已經登錄的用戶。
修改 quota_rule.yaml 內容如下:
然后執行如下命令修改 rule:
修改后的 rule 使得當且僅當 user=<username> cookie 為空時才應用 memquota 或者 redisquota 適配器,從而保證了已登錄的用戶免受速率限制。
以“kokokobe”用戶登錄,並持續刷新頁面,每次都可以刷出下圖所示頁面。
登出之后持續刷新頁面,會再一次看到下圖所示的 RESOURCE_EXHAUSTED:Quota is exhausted for: requestcount。
5 速率限制的原理
速率限制有 Token Bucket、Fixed Window、Rolling Window 等常見的算法實現。
Token Bucket 算法每經過固定的時間間隔會產生一個 token,如果此時 bucket 沒有滿,則產生的 token 可以被放入 bucket 中。當請求到來時,如果此時 bucket 中有足夠多的 token,則可以同時取走多個;如果此時 bucket 中 token 不夠,則拒絕服務。
Fixed Window 算法每個時間間隔對應一個計數器,每當有請求到來,如果此時計數器未達到配額的限定值,則計數器加 1,否則拒絕服務。當進入下一個時間間隔時,計數器失效被重置。該算法的缺點在於不能保證在任意的時間間隔內,速率都被限制在配額以下。即如果請求集中在計數器失效的時間點附近,則在該時間點附近的時間間隔內,速率最大能達到配額的兩倍。
Rolling Window 算法通過對上一個時間間隔請求數和當前時間間隔已處理的請求數進行加權,實現了對任意時間間隔的速率的估算。
圖片來自
https://blog.cloudflare.com/counting-things-a-lot-of-different-things/
如上圖所示,在上一分鍾內處理了 42 個請求,當前這一分鍾已經過去了 15 秒,處理了 18 個請求,則當前這一分鍾的速率可以估算為:
rate = 42 * ((60-15)/60) + 18 = 42 * 0.75 + 18 = 49.5
如果使用 memquota adapter,默認使用亞秒級分辨率的 rolling window 實現速率限制。
如果使用 redisquota adapter,可以配置使用 Rolling Window 算法或者 Fixed Window 算法。
如果在該時間間隔內的請求數超過了 maxAmount,Mixer 會返回 RESOURCE_EXHAUSTED 信息給 Envoy,Envoy 再返回 HTTP 429 StatusTooManyRequests 給調用者。