Gubernator 開源:高性能分佈式限速微服務項目

Gubernator 的特性

  • Gubernator 在整個集群中均勻地分佈速率限制請求,這樣用戶就可以添加更多的節點來擴展系統。
  • Gubernator 不依賴於 Memcache 或 Redis 等外部緩存,因此部署時不存在服務依賴。這使得在諸如 kubernetes 或 nomad 的編排系統中能動態增長或縮小集群。
  • Gubernator 在磁盤上不保存狀態,它的配置是由客戶機根據每個請求傳遞給它的。
  • Gubernator 提供了對其 API 的 GRPC 和 HTTP 訪問。可以根據需要限制速率的陪伴服務運行,也可以作為獨立的服務運行。
  • 可以用作庫來實現特定領域的限速服務。
  • 支持對高吞吐量環境進行定製化的一致速率限制服務。
  • Gubernator 是 俄語中 governor 的英文發音 ,聽起來也很酷。

在正式開始討論 Gubernator 的工作原理之前,先來回答大家關心最多的幾個問題:

為什麼不用 Redis 呢?

在評估 Redis 時,我們思考了很多東西:

  1. 即使使用 管道 ,使用基本的 Redis 速率限 制實現也會導致額外的網絡往返。
  2. 我們可以使用 https://redis.io/commands/eval 和一個 LUA 腳本來減少往返,但我們需要為實現的每個算法維護這個腳本。
  3. 每一個獨立的請求將導致至少一次往返於 Redis。再加上至少一次到我們微服務的往返,這意味著每個到我們服務的請求至少要兩次往返。

Redis 的最優解決方案是編寫一個 LUA 腳本來實現速率限制算法。然後,該腳本存儲在 Redis 服務器上,併為每個速率限制請求調用。在這個場景中,大部分工作由 Redis 完成,而我們的微服務基本上是訪問 Redis 的代理。在這種情況下,我們有兩個選擇:

  1. 創建 Gubernator 作為速率限制庫,提供對 Redis 的訪問,這個庫將被所有需要限制速率的服務使用。
  2. 棄用 Redis,並在每個服務都使用瘦 GRPC 客戶機的限速微服務中實現分佈式、緩存和限制算法。
Gubernator 開源:高性能分佈式限速微服務項目

為什麼用微服務?

Mailgun 是一家擅長多種開發語言的公司,我們的代碼大部分都是用 python 和 golang 寫的。如果我們選擇將速率限制實現為一個庫,這至少需要一個 python 和一個 golang 版本的庫。我們以前在內部使用同一個庫的 python 和 golang 版本時也採用過這種方法。根據我們的經驗,跨服務共享庫有以下缺點:

  1. 對庫的 Bug 和特性更新最多可能導致對依賴項的更新。在最壞的情況下,它需要對所有使用庫的服務進行修改,這些服務使用庫中支持的所有語言。
  2. 開發人員很少希望用兩種語言維護或編寫新特性。假如兩個並行的話,通常導致一個版本的庫具有更多的特性,或者比另一個版本維護得更好。

隨著微服務和語言應用數量在我們的生態系統中的不斷增長,這些問題變得越來越複雜,也更糟糕。相比之下,GRPC 和 HTTP 庫很容易為需要訪問 Gubernator 的每種語言創建和維護。

對於微服務,可以添加 bug 更新和新特性,而不會破壞依賴的服務。只要不允許 API 中斷更改,依賴服務就可以選擇新特性,而不需要更新所有依賴服務。

Gubernator 作為微服務的主要特性是,它為進入系統的許多請求創建了一個同步點。在幾微秒內接收到的請求可以被優化並協調成批,從而減少服務在重載下使用的總帶寬和往返延遲。多個服務都運行在單個主機上,並且所有服務都在各自的進程中運行相同的庫,但它們沒有此功能。

為什麼 Gubernator 是無狀態的?

Gubernator 是無狀態的,因為它不需要磁盤空間來操作。不需要任何配置或緩存數據同步到磁盤,這是因為對 Gubernator 的每個請求都包含速率限制的配置。

首先,你可能認為這對每個請求都是不必要的開銷。然而,實際上,速率限制配置僅由 4 個 64 位整數組成。配置由限制、持續時間、算法和行為組成 (有關工作原理的詳細信息,請參閱下面)。正是由於這種簡單的配置,Gubernator 可以用來提供客戶端可以使用的各種速率限制用例。其中一些用例如下:

  1. 入口限制:典型的基於 HTTP 的 402 多請求類型限制。
  2. 流量減少:當 API 處於不佳狀態時,只拒絕新的或未經身份驗證的請求。
  3. 出口限制:用數百萬條消息轟炸外部 SMTP 服務器並非易事。
  4. 隊列處理:知道何時可以立即處理請求,或者應該按照接收請求的順序排隊和處理請求。
  5. API 容量管理:對一個集合 API 系統能夠處理的請求總數設置全侷限制。拒絕或對違反系統正常操作能力的請求進行排隊。

除了上面提到的用例,無配置設計對微服務的設計和部署有重要的影響:

  1. 部署時不用配置同步。當使用 Gubernator 的服務被部署時,不用預先部署到 Gubernator 的速率限制配置。
  2. 使用 Gubernator 的服務擁有其問題空間的速率極限域模型。這使得 Gubernator 無法獲得領域特定的知識,因此 Gubernator 可以專注於它最擅長的事情——速率限制!

在這些問題之外,下面就從 Gubernator 的工作原理開始,討論更多關於 Gubernator 的內容。

Gubernator 的工作原理

Gubernator 開源:高性能分佈式限速微服務項目

Gubernator 被設計成一個分佈式的對等點集群,它利用了內存中所有當前活動速率限制的緩存,因為不用將數據同步到磁盤。由於大多數基於網絡的速率限制持續時間只有幾秒鐘,因此在重啟或計劃停機期間丟失內存緩存並不是什麼大問題。對於 Gubernator,我們選擇性能而不是精度,因為在緩存丟失的情況下,一小部分流量在短時間內 (通常是幾秒鐘) 超過請求是可以接受的。

Gubernator 開源:高性能分佈式限速微服務項目

當向 Gubernator 發出速率限制請求時,將鍵入該請求並應用一致的哈希算法來確定哪個對等點將是速率限制請求的所有者。為速率限制選擇單個所有者可以使計數的原子增量非常快,並且避免了在對等集群中一致地分佈計數所涉及的複雜性和延遲。

儘管簡單且性能良好,但是這種設計可能會受到一大堆請求的影響,因為一個協調器可能要處理成千上萬個請求,而且速度有限。

為了解決這個問題,客戶機可以請求 Behaviour=BATCHING,它允許對等點在指定的窗口內接受多個請求 (缺省值為 500 微秒),並將請求批處理為單個對等點請求,從而極大地減少了通過網絡向單個 Gubernator 對等點發送請求的總數。

為了確保集群中的每個對等點準確地計算速率限制鍵的正確散列,必須以及時和一致的方式將集群中的對等點列表分發給集群中的每個對等點。目前,Gubernator 支持使用 etcd 或 kubernetes 端點 API 來發現 Gubernator 對等點。

Gubernator 操作

當客戶機或服務向 Gubernator 發出請求時,客戶機將為每個請求提供速率限制配置。然後,速率限制配置與當前速率限制狀態一起存儲在速率限制所有者的本地緩存中。存儲在本地緩存中的速率限制及其配置僅在速率限制配置的指定持續時間內存在。

在持續時間過期之後,如果在此期間沒有再次請求速率限制,則從緩存中刪除它。對相同名稱和 unique_key 對的後續請求將在緩存中重新創建配置和速率限制,這個循環將重複。另一方面,具有不同配置的後續請求將覆蓋以前的配置並立即應用新配置。

通過 GRPC 發送的速率限制請求示例如下所示:

 複製代碼

rate_limits:
# Scopes the request to a specific rate limit
- name:requests_per_sec
# A unique_key that identifies this rate limit request
unique_key:account_id=123|source_ip=172.0.0.1
# The number of hits we are requesting
hits:1
# The total number of requests allowed for this rate limit
limit:100
# The duration of the rate limit in milliseconds
duration:1000
# The algorithm used to calculate the rate limit
# 0 = Token Bucket
# 1 = Leaky Bucket
algorithm:0
# The behavior of the rate limit in gubernator.
# 0 = BATCHING (Enables batching of requests to peers)
# 1 = NO_BATCHING (Disables batching)
# 2 = GLOBAL (Enable global caching for this rate limit)
behavior:0

下面是一個例子:

 複製代碼

rate_limits:
# The status of the rate limit. OK = 0, OVER_LIMIT = 1
- status:0,
# The current configured limit

limit:10,
# The number of requests remaining
remaining:7,
# A unix timestamp in milliseconds of when the rate limit will reset,
# or if OVER_LIMIT is set it is the time at which the rate limit
# will no longer return OVER_LIMIT.
reset_time:1551309219226,
# Additional metadata about the request the client might find useful
metadata:
# This is the name of the node that owns this request
"owner":"api-n03.staging.us-east-1.mailgun.org:9041"

Global Behavior

由於 Gubernator 速率限制是由集群中的單個對等點哈希和處理的,所以適用於數據中心中的每個請求的速率限制將導致單個對等點處理整個數據中心的速率限制請求。

例如,考慮 name=requests_per_datacenter 和 unique_id=us-east-1 的速率限制。現在,假設對每個進入 us-east-1 數據中心的 HTTP 請求都使用這個速率限制向 Gubernator 發出請求。這可能是每秒數十萬個請求,甚至可能是數百萬個請求,這些請求都由集群中的一個對等點哈希並處理。由於這個潛在的可伸縮性問題,Gubernator 引入了一個名為 GLOBAL 的可配置 behavior。

當速率限制配置為 behavior=GLOBAL 時,從客戶機接收到的速率限制請求將不會轉發給擁有它的對等方。相反,它將從接收請求的對等方處理的內部緩存中得到響應。Hits 速率限制的點擊率將由接收對等點批量處理,並異步發送到擁有該點擊率的對等點,在該對等點上,點擊率將被總計並得出 OVER_LIMIT。然後,擁有節點的節點有責任用速率限制的當前狀態更新集群中的每個節點,這樣,節點內部緩存就會定期從所有者那裡獲得最新速率限制狀態的更新。

Global Behavior 的其他影響

由於 Hits 是批量處理並異步轉發給擁有它的對等點的,所以對客戶機的即時響應將不包括最精確的 remaining 計數。只有在對所有者對等點的異步調用完成並且擁有對等點有時間更新集群中的所有對等點之後,該計數才會得到更新。因此,使用 GLOBAL 允許更大的集群規模,但要以一致性為代價。如果集群足夠大,使用 GLOBAL 可以增加每速率限制請求的通信量。 GLOBAL 應該只用於與傳統的非 GLOBAL 行為不兼容的高容量速率限制。

Gubernator 性能

在我們的生產環境中,每向我們的 API 發送一個請求,我們就向 Gubernator 發送兩個速率限制請求來評估速率限制;一個用於對 HTTP 請求進行評級,另一個用於對用戶在特定時間內也可以發送電子郵件的收件人數量進行評級。在這種設置下,一個 Gubernator 節點每秒處理超過 2000 個請求,大多數批量響應在 1 毫秒內返回。

轉發給擁有節點的對等請求通常在 30 微秒內響應。

  • NOTE
  • The
  • above
  • graphs
  • only report
  • the slowest
  • request
  • within the
  • 1 second
  • sample
  • time. So
  • you are
  • seeing the
  • slowest
  • requests
  • that
  • Gubernator
  • fields to
  • clients.

由於許多面向公眾的 API 都是用 python 編寫的,所以我們在一個節點上運行許多 python 解釋器實例。這些 python 實例將本地請求轉發給 Gubernator 實例,然後 Gubernator 實例將請求批處理並轉發給擁有節點的節點。

Gubernator 允許用戶選擇非批處理行為,這將進一步減少客戶機速率限制請求的延遲。但是,由於吞吐量需求,我們的生產環境使用默認的 500 微秒窗口使用 Behaviour=BATCHING。在生產中,我們觀察到在 API 使用高峰期間,批處理大小為 1000。其他不具有相同高流量需求的用戶可以禁用批處理,並以吞吐量為代價降低延遲。

Gubernator 用作庫

如果使用 Golang,可以使用 Gubernator 作為庫。如果你希望在頂部實現一個公司特有模型的速率限制服務,這是非常有用的。我們在 Mailgun 內部有一項名為“ratelimits”的服務,專門跟蹤每個賬戶的限額。通過這種方式,你可以利用 Gubernator 的強大功能和速度,同時可以分層業務邏輯,並將特定領域的問題集成到速率限制服務中。

Gubernator 開源:高性能分佈式限速微服務項目

使用庫後,你的服務將成為集群的正式成員,與獨立的 Gubernator 服務器一樣參與一致的散列和緩存。你所需要做的就是提供 GRPC 服務器實例,並告訴 Gubernator 集群中的對等點位於何處。

結論

使用 Gubernator 作為通用的速率限制服務,允許我們依賴於微服務體系結構,而不會損害服務獨立性和公共速率限制解決方案所需的重複工作。我們希望通過開源這個項目,與其他人共同合作,也讓他們從中受益。

"


分享到:


相關文章: