Envoy 自定義授權和限流示例
最近, 我所工作的一個團隊選擇 Envory 作爲一個系統構建的核心組件。關於它的介紹以及包含或圍繞它構建的開源工具數量之多給我留下了深刻的印象, 但實際上並沒有進行任何深入的探討。
我尤其對它如何作爲邊緣代理感到好奇, 本質上 Envoy 是作爲一個更現代和可編程的組件, 在過去我一直使用 nginx 來實現。最終的成果是實現這個設計的一個小型的容器化的 playground。
整個請求的生命週期有以下幾個階段:
一個客戶端發送資源請求到 Envory(作爲網關) 利用Envory
的External Authorizer
接口: 驗證調用, 如果無效則拒絕 設置用來限流的自定義的請求頭 根據路由, 提供不同的信息以用於限流 使用Ratelimiter
接口, 應用限流, 如果超出限制則拒絕 最終, 請求通過後端, 返回一個response
給客戶端 憑藉有限的經驗, 可以說的是, Envory
沒有辜負我的期望。同時我發現雖然官方文檔很完整, 但有些方面又太簡潔, 這是我想要編寫這篇文章的原因之一, 即很難找到這種模式的完整示例, 因此如果你正在閱讀這篇文章, 想必可以節省你的一些精力。
在文章的其餘部分, 我會一步步的介紹各個部分的工作方式。
Docker 環境
在這裏我使用了docker-compose
, 因爲它提供了圍繞構建和運行一堆容器的簡單編排, 並且有統一良好的的日誌輸出。這裏我們講創建五個容器:
-
envory
, 毫無懸念, 單純的...envory
-
redis
, 用來存儲限流服務的數據 -
extauth
, 這是一個自定義的 Go 應用, 可實現Envoy的gRPC規範
以進行外部授權 -
ratelimit
, envoy 官方的開源限流服務, 該服務實現了Envoy gRPC限流規範
-
backend
, 一個自定義的 Go 應用, 它本質上是一個“ hello world”
, 並且還會打印它收到的請求頭, 以便於進行故障排查
docker-compose
還創建了一個network(envorymesh)
用以上面的所有服務的網絡共享, 並且對外暴露了幾個端口。其中最重要的是8010
端口 (或者localhost:8010
, 對大多數docker machines
來說), 它是公共的 HTTP 端點。
爲了讓它跑起來,
# 原作者倉庫:git clone https://github.com/jbarratt/envoy_ratelimit_example
git clone https://github.com/cluas/envoy_ratelimit_example # 添加go mod支持 修復api v3 錯誤
你還需要ratelimi
t 一個本地的副本。子模塊在這裏會很好, 但是作爲簡單驗證, 直接
git clone https://github.com/envoyproxy/ratelimit.git
完成以上步驟後, 運行docker-compose up
。第一次啓動會比較耗時, 因爲它要從頭構建所有內容。
你可以使用簡單的 curl, 來確保整個環境工作正常, 同時展示所有調用的軌跡
作爲集成真實身份認證提供方的替代方案, 所有的bearer tokens
只要是 3 個字符長度的這裏都認爲是有效的 授權服務設置請求頭 (X-Ext-Auth-Ratelimit
), 該請求頭可用於唯一的每個token
限流 在每個envoy
配置中, Authorization
請求頭被剝離, 因此敏感的身份信息不會被推送到後端
$ curl -v -H "Authorization: Bearer foo" http://localhost:8010/
> GET / HTTP/1.1
> Authorization: Bearer foo
>
< HTTP/1.1 200 OK
< date: Tue, 21 May 2019 00:23:12 GMT
< content-length: 270
< content-type: text/plain; charset=utf-8
< x-envoy-upstream-service-time: 0
< server: envoy
Oh, Hello!
# The backend got these headers with the request
X-Request-Id: 6c03f5f4-e580-4d8f-aee1-7e62ba2c9b30
X-Ext-Auth-Ratelimit: LCa0a2j/xo/5m0U8HTBBNBNCLXBkg7+g+YpeiGJm564=
X-Envoy-Expected-Rq-Timeout-Ms: 15000
X-Forwarded-Proto: http
定義後端服務
backend 是一個運行在容器中的非常簡單的 Go 應用。它在 envoy.yaml 配置文件中多次顯示 (命名爲 backend)。首先, 將其定義爲 “cluster”(儘管作爲單個容器, 它並不是集羣的一部分)。
clusters:
- name: backend
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: round_robin
load_assignment:
cluster_name: backend
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: backend
port_value: 8123
這是一個envoy
的配置示例, 需要一點時間去理解。對於簡單的 “單個節點” 後端, 它有一些相當重要的樣板配置。它的功能也非常強大。我們可以定義如何查找節點, 應該如何負載均衡, 多個羣集, 多個羣集中的多個負載平衡器以及其中的多個節點。完全雖然可以簡化此定義, 但是這個版本也可以運行的很好。很好且一致的是, 在定義任一集羣時, 集羣定義是相同的
-
“服務” 通過
envoy
代理 -
“輔助服務” 通過
envoy
的filter
聯繫, 比如授權和限流服務
同樣有幫助的是custers
(或者整個配置) 通過容易管理的數據結構定義, 實際上它被定義爲protobufs
。這意味着當你使用YAML
文件配置Envoy
時, 或者在運行時通過配置界面, 可以相當一致地完成Envoy
的管理。既然已經定義好了backend
, 那麼是時候讓它獲得一些流量了, 這是通過routes
來完成的。
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: backend
再說一次, 配置具有相當不錯的數據結構來表示 “將所有流量發送到我定義的稱爲backend
的cluster
”, 正如我們將看到的, 當需要添加條件限流時, 它提供了類似的有用位置來hook
其他配置。
自定義外部授權者
Envoy
具有用於外部授權的內置過濾器模塊。
http_filters:
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
grpc_service:
envoy_grpc:
cluster_name: ext-authz
timeout: 0.25s
transport_api_version: V3
這個配置片段表示去調用一個gRPC
服務, 該服務運行在名爲extauth
的集羣上 (與上面的 backend 定義相同)。我對 2 個近期的發展感到非常高興, 這讓 Go 應用非常容易構建——Go modules 和 Docker 多階段構建。使用 Alpine 和 Go 應用程序的二進制文件構建一個瘦容器, 只需要 Dockerfile 的這一小片段。是的, 非常好用。
FROM golang:latest as builder
COPY . /ext-auth-poc
WORKDIR /ext-auth-poc
ENV GO111MODULE=on
RUN CGO_ENABLED=0 GOOOS=linux go build -o ext-auth-poc
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /ext-auth-poc .
CMD ["./ext-auth-poc"]
好的, 我們如何構建該應用程序?對於 Go 服務, Envoy
使事情變得非常簡單明瞭。簡單地自定義授權服務代碼
譯者注:原倉庫不支持 v3,會報錯,建議使用譯者 fork 的倉庫測試
其中的定義可從 Envoy 存儲庫中獲取, 例如
auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
定義諸如CheckRequest
和CheckResponse
之類的東西。這允許根據我們需要做的事情構造並返回正確的響應。舉個例子, 下面是成功的路徑:
// inject a header that can be used for future rate limiting
func (a *AuthorizationServer) Check(ctx context.Context, req *auth.CheckRequest) (*auth.CheckResponse, error) {
...
// valid tokens have exactly 3 characters. #secure.
// Normally this is where you'd go check with the system that knows if it's a valid token.
if len(token) == 3 {
return &auth.CheckResponse{
Status: &status.Status{
Code: int32(rpc.OK),
},
HttpResponse: &auth.CheckResponse_OkResponse{
OkResponse: &auth.OkHttpResponse{
Headers: []*core.HeaderValueOption{
{
Header: &core.HeaderValue{
Key: "x-ext-auth-ratelimit",
Value: tokenSha,
},
},
},
},
},
}, nil
}
}
在請求週期的時間點上編寫任意代碼的能力非常強大, 因爲在此處添加請求頭可用於各種決策, 包括路由和限流。
限流
限流可以通過任何實現限流器接口的服務來完成。值得慶幸的是, envoy 官方提供了一個非常不錯的工具, 它具有簡單但功能強大的配置–對於許多用例來說, 使用起來可能綽綽有餘。(envoyproxy/ratelimit) 就像使用外部授權器一樣, 有一些Envoy
配置可以啓用外部限流服務。先定義集羣, 然後啓用envoy.filters.http.ratelimit filter
。
- name: envoy.filters.http.ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
domain: backend
request_type: external
stage: 0
rate_limited_as_resource_exhausted: true
failure_mode_deny: false
rate_limit_service:
grpc_service:
envoy_grpc:
cluster_name: ratelimit
timeout: 0.25s
transport_api_version: V3
爲了讓Envoy
能夠做到限流, 你必須告訴它要限制什麼。超級有用的一點是可以根據不同的路由設置不同的限流。Envoy
還可以通過將鍵 / 值對發送到ratelimiter
服務來使限流策略配置化。以下定義了兩個路由:
-
/slowpath
, 通過generic_key:slowpath
發送 -
/(其餘路由)
, 通過ratelimitkey:$x-ext-auth-ratelimit
和path:$path
—其中帶有$
的值是這些請求頭具有的任何值
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/slowpath" }
route:
cluster: backend
rate_limits:
- stage: 0
actions:
- {generic_key: {"descriptor_value": "slowpath"}}
- match: { prefix: "/" }
route:
cluster: backend
rate_limits:
- stage: 0
actions:
- {request_headers: {header_name: "x-ext-auth-ratelimit", descriptor_key: "ratelimitkey"}}
- {request_headers: {header_name: ":path", descriptor_key: "path"}}
當你使用 envoyproxy 的ratelimiter
時, 實際配置非常優雅。
---
domain: backend
descriptors:
- key: generic_key
value: slowpath
rate_limit:
requests_per_unit: 1
unit: second
- key: ratelimitkey
descriptors:
- key: path
rate_limit:
requests_per_unit: 2
unit: second
一切都在backdend
域內發生 (任意字符串, 在啓用速率限制過濾器時定義)。尋找 2 個描述符, generic_key
和ratelimitkey
。當有 value 提供時, 最終將是一個靜態路徑 (如slowpath
), 該條目適用於所有請求。如果沒有 value 提供, 它將使用密鑰和提供的值來構建複合密鑰。這裏也可以定義嵌套結構,二級層次結構爲ratelimitkey/path
。
因此, 此配置應做兩件事:
-
全侷限制 slowpath 爲每秒 1 個請求
-
使所有用戶每秒每路徑有 2 個請求
就是這樣!
結合在自定義授權服務代碼中設置所需的任何請求頭值的能力, 這最終成爲對幾乎所有所需內容進行限流的絕佳方法。一些有趣的選項包括:
-
用戶或帳戶信息
-
特定的身份驗證信息 (使用了哪個 API 密鑰 / 令牌)
-
源 IP
-
隨用戶變化的信息, 使諸如讓客戶爲不同的速率限制付費或發出臨時優先權之類的事情
-
計算的欺詐 / 風險分類
測試與驗證
將所有內容組合在一起很有趣, 但是我想確保它能夠正常工作。我最終產出了一個奇怪的go test files
, 但是我會嘗試它。簡而言之, 這是一個 Go 的表驅動測試, 加上vegeta
作爲基礎庫, 以驗證所有授權和限流是否按預期工作。首先, 建立一些具有各種特徵的 vegeta 目標。下面這個是使用有效的 API 密鑰進行調用/test
, 另外還有 5 種用於調用路徑, 要使用的密鑰以及密鑰是否有效的各種組合。
// An authenticated path
authedTargetA := vegeta.Target{
Method: "GET",
URL: "http://localhost:8010/test",
Header: http.Header{
"Authorization": []string{"Bearer foo"},
},
}
鑑於此, 然後可以運行一些測試, 如下所示:
testCases := []struct {
desc string
okPct float64
targets []vegeta.Target
}{
{"single authed path, target 2qps", 0.20, []vegeta.Target{authedTargetA}},
{"2 authed paths, single user, target 4qps", 0.40, []vegeta.Target{authedTargetA, authedTargetB}},
{"1 authed paths, dual user, target 4qps", 0.40, []vegeta.Target{authedTargetA, otherAuthTarget}},
{"slow path, target 1qps", 0.1, []vegeta.Target{slowTarget}},
{"unauthed, target 0qps", 0.0, []vegeta.Target{unauthedTarget}},
}
每個測試都有一個描述, 一個預期的 “成功百分比” 以及一個或多個要運行的目標的混合。所有測試均以每秒 10 個查詢的速度運行, 因此, 如果速率限制應爲每秒 2 個查詢, 則預期成功率爲 20%。(0.20)。因此, 要使用vegeta
實際運行測試:
func runTest(okPct float64, tgts ...vegeta.Target) (ok bool, text string) {
rate := vegeta.Rate{Freq: 10, Per: time.Second}
duration := 10 * time.Second
targeter := vegeta.NewStaticTargeter(tgts...)
attacker := vegeta.NewAttacker()
var metrics vegeta.Metrics
for res := range attacker.Attack(targeter, rate, duration, "test") {
metrics.Add(res)
}
metrics.Close()
if closeEnough(metrics.Success, okPct) {
return true, fmt.Sprintf("Got %0.2f which was close enough to %0.2f\n", metrics.Success, okPct)
}
return false, fmt.Sprintf("Error: Got %0.2f which was too far from %0.2f\n", metrics.Success, okPct)
}
最終, 這真是令人興奮。一個簡單的測試定義實際上可以測試各種速率限制方案實際上限制了速率。測試確實會運行 10 秒鐘。我使用不同的速率進行了測試, 並且由於開始跟蹤速率需要花費一些時間, 因此在進行較短的測試時, 數據就變得更加模糊。整個測試套件運行起來非常容易:
# 使用原作者倉庫此處可能報錯,建議使用譯者fork的倉庫
$ cd vegeta && make test
cd loadtest && go test -v
=== RUN TestEnvoyStack
--- PASS: TestEnvoyStack (50.10s)
=== RUN TestEnvoyStack/single_authed_path,_target_2qps
--- PASS: TestEnvoyStack/single_authed_path,_target_2qps (10.02s)
=== RUN TestEnvoyStack/2_authed_paths,_single_user,_target_4qps
--- PASS: TestEnvoyStack/2_authed_paths,_single_user,_target_4qps (10.03s)
=== RUN TestEnvoyStack/1_authed_paths,_dual_user,_target_4qps
--- PASS: TestEnvoyStack/1_authed_paths,_dual_user,_target_4qps (10.02s)
=== RUN TestEnvoyStack/slow_path,_target_1qps
--- PASS: TestEnvoyStack/slow_path,_target_1qps (10.02s)
=== RUN TestEnvoyStack/unauthed,_target_0qps
--- PASS: TestEnvoyStack/unauthed,_target_0qps (10.01s)
PASS
測試結果非常令人滿意。在短短的 50 秒內, 所有 5 種情況均以相當可靠的方式進行了測試。
最後的想法
Envoy
顯然是經過精心設計和真正出色的軟件, 我真的很高興能夠在嘗試隱藏堆棧的同時嘗試按想要的方式進行構建並以自己想要的方式運行。對於需要自定義請求處理的任何用例 (尤其是批量較大的用例), 都值得您考慮。
本文提到的鏈接
-
容器化的 playground: https://github.com/jbarratt/envoy_ratelimit_example
-
譯者 fork 倉庫: https://github.com/Cluas/envoy_ratelimit_example
-
ratelimit: https://github.com/envoyproxy/ratelimit
-
vegeta: https://github.com/tsenart/vegeta
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/-bRKQFNCphZBXE4Cjk02yg