Envoy 自定義授權和限流示例

最近, 我所工作的一個團隊選擇 Envory 作爲一個系統構建的核心組件。關於它的介紹以及包含或圍繞它構建的開源工具數量之多給我留下了深刻的印象, 但實際上並沒有進行任何深入的探討。

我尤其對它如何作爲邊緣代理感到好奇, 本質上 Envoy 是作爲一個更現代和可編程的組件, 在過去我一直使用 nginx 來實現。最終的成果是實現這個設計的一個小型的容器化的 playground。

整個請求的生命週期有以下幾個階段:

一個客戶端發送資源請求到 Envory(作爲網關) 利用EnvoryExternal Authorizer接口: 驗證調用, 如果無效則拒絕 設置用來限流的自定義的請求頭 根據路由, 提供不同的信息以用於限流 使用Ratelimiter接口, 應用限流, 如果超出限制則拒絕 最終, 請求通過後端, 返回一個response給客戶端 憑藉有限的經驗, 可以說的是, Envory沒有辜負我的期望。同時我發現雖然官方文檔很完整, 但有些方面又太簡潔, 這是我想要編寫這篇文章的原因之一, 即很難找到這種模式的完整示例, 因此如果你正在閱讀這篇文章, 想必可以節省你的一些精力。

在文章的其餘部分, 我會一步步的介紹各個部分的工作方式。

Docker 環境

在這裏我使用了docker-compose, 因爲它提供了圍繞構建和運行一堆容器的簡單編排, 並且有統一良好的的日誌輸出。這裏我們講創建五個容器:

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 錯誤

你還需要ratelimit 一個本地的副本。子模塊在這裏會很好, 但是作爲簡單驗證, 直接

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的配置示例, 需要一點時間去理解。對於簡單的 “單個節點” 後端, 它有一些相當重要的樣板配置。它的功能也非常強大。我們可以定義如何查找節點, 應該如何負載均衡, 多個羣集, 多個羣集中的多個負載平衡器以及其中的多個節點。完全雖然可以簡化此定義, 但是這個版本也可以運行的很好。很好且一致的是, 在定義任一集羣時, 集羣定義是相同的

同樣有幫助的是custers(或者整個配置) 通過容易管理的數據結構定義, 實際上它被定義爲protobufs。這意味着當你使用YAML文件配置Envoy時, 或者在運行時通過配置界面, 可以相當一致地完成Envoy的管理。既然已經定義好了backend, 那麼是時候讓它獲得一些流量了, 這是通過routes來完成的。

route_config:
  name: local_route
  virtual_hosts:
  - name: local_service
    domains: ["*"]
    routes:
    - match: { prefix: "/" }
      route: 
    cluster: backend

再說一次, 配置具有相當不錯的數據結構來表示 “將所有流量發送到我定義的稱爲backendcluster”, 正如我們將看到的, 當需要添加條件限流時, 它提供了類似的有用位置來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=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"

定義諸如CheckRequestCheckResponse之類的東西。這允許根據我們需要做的事情構造並返回正確的響應。舉個例子, 下面是成功的路徑:

// 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服務來使限流策略配置化。以下定義了兩個路由:

- 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_keyratelimitkey。當有 value 提供時, 最終將是一個靜態路徑 (如slowpath), 該條目適用於所有請求。如果沒有 value 提供, 它將使用密鑰和提供的值來構建複合密鑰。這裏也可以定義嵌套結構,二級層次結構爲ratelimitkey/path

因此, 此配置應做兩件事:

就是這樣!

結合在自定義授權服務代碼中設置所需的任何請求頭值的能力, 這最終成爲對幾乎所有所需內容進行限流的絕佳方法。一些有趣的選項包括:

測試與驗證

將所有內容組合在一起很有趣, 但是我想確保它能夠正常工作。我最終產出了一個奇怪的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顯然是經過精心設計和真正出色的軟件, 我真的很高興能夠在嘗試隱藏堆棧的同時嘗試按想要的方式進行構建並以自己想要的方式運行。對於需要自定義請求處理的任何用例 (尤其是批量較大的用例), 都值得您考慮。

本文提到的鏈接

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/-bRKQFNCphZBXE4Cjk02yg