如何實現可伸縮的 etcd API?

etcd 中如何實現可伸縮的 etcd API?使得 etcd 能夠屏蔽內部集羣的信息。本文將會介紹 etcd 中的 gRPC proxy 相關概念和使用分析。

gRPC proxy 是在 gRPC 層(L7)運行的無狀態 etcd 反向代理,旨在**「減少核心 etcd 集羣上的總處理負載」**。gRPC proxy 合併了監視和 Lease API 請求,實現了水平可伸縮性。同時,爲了保護集羣免受濫用客戶端的侵害,gRPC proxy 實現了鍵值對的讀請求緩存。

下面我們將圍繞 gRPC proxy 基本應用、客戶端端點同步、可伸縮的 API、命名空間的實現和其他擴展功能展開介紹。

gRPC proxy 基本應用

首先我們來配置 etcd 集羣,集羣中擁有如下的靜態成員信息:

q9hc8G

使用etcd grpc-proxy start的命令開啓 etcd 的 gRPC proxy 模式,包含上表中的靜態成員:

$ etcd grpc-proxy start --endpoints=http://192.168.10.7:2379,http://192.168.10.8:2379,http://192.168.10.9:2379 --listen-addr=192.168.10.7:12379
{"level":"info","ts":"2020-12-13T01:41:57.561+0800","caller":"etcdmain/grpc_proxy.go:320","msg":"listening for gRPC proxy client requests","address":"192.168.10.7:12379"}
{"level":"info","ts":"2020-12-13T01:41:57.561+0800","caller":"etcdmain/grpc_proxy.go:218","msg":"started gRPC proxy","address":"192.168.10.7:12379"}

可以看到,etcd gRPC proxy 啓動後在192.168.10.7:12379監聽,並將客戶端的請求轉發到上述三個成員其中的一個。通過下述客戶端讀寫命令,經過 proxy 發送請求:

$ ETCDCTL_API=3 etcdctl --endpoints=192.168.10.7:12379 put foo bar
OK
$ ETCDCTL_API=3 etcdctl --endpoints=192.168.10.7:12379 get foo
foo
bar

我們通過 grpc-proxy 提供的客戶端地址進行訪問,proxy 執行的結果符合預期,在使用方法上和普通的方式完全相同。

客戶端端點同步

gRPC 代理是 gRPC 命名的提供者,支持**「在啓動時通過寫入相同的前綴端點名稱」**進行註冊。這樣可以使客戶端將其端點與具有一組相同前綴端點名的代理端點同步,進而實現高可用性。

下面我們來啓動兩個 gRPC 代理,在啓動時指定自定義的前綴___grpc_proxy_endpoint來註冊 gRPC 代理:

$ etcd grpc-proxy start --endpoints=localhost:12379   --listen-addr=127.0.0.1:23790   --advertise-client-url=127.0.0.1:23790   --resolver-prefix="___grpc_proxy_endpoint"   --resolver-ttl=60
{"level":"info","ts":"2020-12-13T01:46:04.885+0800","caller":"etcdmain/grpc_proxy.go:320","msg":"listening for gRPC proxy client requests","address":"127.0.0.1:23790"}
{"level":"info","ts":"2020-12-13T01:46:04.885+0800","caller":"etcdmain/grpc_proxy.go:218","msg":"started gRPC proxy","address":"127.0.0.1:23790"}
2020-12-13 01:46:04.892061 I | grpcproxy: registered "127.0.0.1:23790" with 60-second lease
$ etcd grpc-proxy start --endpoints=localhost:12379 \
>   --listen-addr=127.0.0.1:23791 \
>   --advertise-client-url=127.0.0.1:23791 \
>   --resolver-prefix="___grpc_proxy_endpoint" \
>   --resolver-ttl=60
{"level":"info","ts":"2020-12-13T01:46:43.616+0800","caller":"etcdmain/grpc_proxy.go:320","msg":"listening for gRPC proxy client requests","address":"127.0.0.1:23791"}
{"level":"info","ts":"2020-12-13T01:46:43.616+0800","caller":"etcdmain/grpc_proxy.go:218","msg":"started gRPC proxy","address":"127.0.0.1:23791"}
2020-12-13 01:46:43.622249 I | grpcproxy: registered "127.0.0.1:23791" with 60-second lease

在上面的啓動命令中,將需要加入的自定義端點--resolver-prefix設置爲___grpc_proxy_endpoint。啓動成功之後,我們來驗證下,gRPC 代理在查詢成員時是否列出其所有成員作爲成員列表,執行如下的命令:

ETCDCTL_API=3 etcdctl --endpoints=http://localhost:23790 member list --write-out table

通過下圖,可以看到,通過相同的前綴端點名完成了自動發現所有成員列表的操作。

圖片

同樣地,客戶端也可以通過 Sync 方法自動發現代理的端點,代碼實現如下:

cli, err := clientv3.New(clientv3.Config{
    Endpoints: []string{"http://localhost:23790"},
})
if err != nil {
    log.Fatal(err)
}
defer cli.Close()
// 獲取註冊過的 grpc-proxy 端點
if err := cli.Sync(context.Background()); err != nil {
    log.Fatal(err)
}

相應地,如果配置的代理沒有配置前綴,gRPC 代理啓動命令如下:

$ ./etcd grpc-proxy start --endpoints=localhost:12379 \
>   --listen-addr=127.0.0.1:23792 \
>   --advertise-client-url=127.0.0.1:23792
# 輸出結果
{"level":"info","ts":"2020-12-13T01:49:25.099+0800","caller":"etcdmain/grpc_proxy.go:320","msg":"listening for gRPC proxy client requests","address":"127.0.0.1:23792"}
{"level":"info","ts":"2020-12-13T01:49:25.100+0800","caller":"etcdmain/grpc_proxy.go:218","msg":"started gRPC proxy","address":"127.0.0.1:23792"}

我們來驗證下 gRPC proxy 的成員列表 API 是不是隻返回自己的advertise-client-url

ETCDCTL_API=3 etcdctl --endpoints=http://localhost:23792 member list --write-out table

通過下圖,可以看到,結果如我們預期:當我們**「沒有配置代理的前綴端點名**「**「時」**」**,獲取其成員列表只會顯示當前節點的信息,也不會包含其他的端點」**。

圖片

可伸縮的 watch API

如果客戶端監視同一鍵或某一範圍內的鍵,gRPC 代理可以將這些客戶端監視程序(c-watcher)合併爲連接到 etcd 服務器的單個監視程序(s-watcher)。當 watch 事件發生時,代理將所有事件從 s-watcher 廣播到其 c-watcher。

假設 N 個客戶端監視相同的 key,則 gRPC 代理可以將 etcd 服務器上的監視負載從 N 減少到 1。用戶可以部署多個 gRPC 代理,進一步分配服務器負載。

如下圖所示,三個客戶端監視鍵 A。gRPC 代理將三個監視程序合併,從而創建一個附加到 etcd 服務器的監視程序。

圖片

爲了有效地將多個客戶端監視程序合併爲一個監視程序,gRPC 代理在可能的情況下將新的 c-watcher 合併爲現有的 s-watcher。由於網絡延遲或緩衝的未傳遞事件,合併的 s-watcher 可能與 etcd 服務器不同步。

「如果「沒有」指定監視版本,gRPC 代理將不能保證 c-watcher 從最近的存儲修訂版本開始監視」。例如,如果客戶端從修訂版本爲 1000 的 etcd 服務器監視,則該監視者將從修訂版本 1000 開始。如果客戶端從 gRPC 代理監視,則可能從修訂版本 990 開始監視。

「類似的限制也適用於取消」。取消 watch 後,etcd 服務器的修訂版可能大於取消響應修訂版。

對於大多數情況,這兩個限制一般不會引起問題,未來也可能會有其他選項強制觀察者繞過 gRPC 代理以獲得更準確的修訂響應。

可伸縮的 lease API

爲了保持客戶端申請租約的有效性,客戶端至少建立一個 gRPC 連接到 etcd 服務器,以定期發送心跳信號。如果 etcd 工作負載涉及很多的客戶端租約活動,這些流可能會導致 CPU 使用率過高。「爲了減少核心集羣上的流總數,gRPC 代理支持將 lease 流合併」

假設有 N 個客戶端正在更新租約,則單個 gRPC 代理將 etcd 服務器上的流負載從 N 減少到 1。在部署的過程中,可能還有其他 gRPC 代理,進一步在多個代理之間分配流。

在下圖示例中,三個客戶端更新了三個獨立的租約(L1、L2 和 L3)。gRPC 代理將三個客戶端租約流(c-stream)合併爲連接到 etcd 服務器的單個租約(s-stream),以保持活動流。代理將客戶端租約的心跳從 c-stream 轉發到 s-stream,然後將響應返回到相應的 c-stream。

圖片

除此之外,gRPC 代理在滿足一致性時會緩存請求的響應。該功能可以保護 etcd 服務器免遭惡意 for 循環中濫用客戶端的攻擊。

命名空間的實現

上面我們講到 gRPC proxy 的端點可以通過配置前綴,自動發現。而當應用程序期望對整個鍵空間有完全控制,etcd 集羣與其他應用程序共享的情況下,爲了使所有應用程序都不會相互干擾地運行,代理可以對**「etcd 鍵空間進行分區」**,以便客戶端大概率訪問完整的鍵空間。

當給代理提供標誌--namespace時,所有進入代理的客戶端請求都將轉換爲**「在鍵上具有用戶定義的前綴」**。普通的請求對 etcd 集羣的訪問將會在我們指定的前綴(即指定的 --namespace 的值)下,而來自代理的響應將刪除該前綴;而這個操作對於客戶端來說是透明的,根本察覺不到前綴。

下面我們給 gRPC proxy 命名,只需要啓動時指定--namespace標識:

$ ./etcd grpc-proxy start --endpoints=localhost:12379 \
>   --listen-addr=127.0.0.1:23790 \
>   --namespace=my-prefix/
{"level":"info","ts":"2020-12-13T01:53:16.875+0800","caller":"etcdmain/grpc_proxy.go:320","msg":"listening for gRPC proxy client requests","address":"127.0.0.1:23790"}
{"level":"info","ts":"2020-12-13T01:53:16.876+0800","caller":"etcdmain/grpc_proxy.go:218","msg":"started gRPC proxy","address":"127.0.0.1:23790"}

此時對代理的訪問會在 etcd 羣集上自動地加上前綴,對於客戶端來說沒有感知。我們通過 etcdctl 客戶端進行嘗試:

$ ETCDCTL_API=3 etcdctl --endpoints=localhost:23790 put my-key abc
# OK
$ ETCDCTL_API=3 etcdctl --endpoints=localhost:23790 get my-key
# my-key
# abc
$ ETCDCTL_API=3 etcdctl --endpoints=localhost:2379 get my-prefix/my-key
# my-prefix/my-key
# abc

上述三條命令,首先通過代理寫入鍵值對,然後讀取。爲了驗證結果,第三條命令通過 etcd 集羣直接讀取,不過需要加上代理的前綴,兩種方式得到的結果完全一致。因此,「使用 proxy 的命名空間即可實現 etcd 鍵空間分區」,對於客戶端來說非常便利。

其他擴展功能

gRPC 代理的功能非常強大,除了上述提到的客戶端端點同步、可伸縮 API、命名空間功能,還提供了指標與健康檢查接口和 TLS 加密中止的擴展功能。

指標與健康檢查接口

gRPC 代理爲--endpoints定義的 etcd 成員公開了/health和 Prometheus 的/metrics接口。我們通過瀏覽器訪問這兩個接口:

訪問 metrics 接口的結果

訪問 health 接口的結果

通過代理訪問/metrics端點的結果如上圖所示,其實和普通的 etcd 集羣實例沒有什麼區別,同樣也會結合一些中間件進行統計和頁面展示,如 Prometheus 和 Grafana 的組合。

除了使用默認的端點訪問這兩個接口,另一種方法是定義一個附加 URL,該 URL 將通過 --metrics-addr 標誌來響應/metrics/health端點。命令如下所示:

$ ./etcd grpc-proxy start \
  --endpoints http://localhost:12379 \
  --metrics-addr http://0.0.0.0:6633 \
  --listen-addr 127.0.0.1:23790 \

在執行如上啓動命令時,會有如下的命令行輸出,提示我們指定的 metrics 監聽地址爲 http://0.0.0.0:6633。

{"level":"info","ts":"2021-01-30T18:03:45.231+0800","caller":"etcdmain/grpc_proxy.go:456","msg":"gRPC proxy listening for metrics","address":"http://0.0.0.0:6633"}

TLS 加密的代理

通過使用 gRPC 代理 etcd 集羣的 TLS,可以給沒有使用 HTTPS 加密方式的本地客戶端提供服務,實現 etcd 集羣的 TLS 加密中止,即未加密的客戶端與 gRPC 代理通過 HTTP 方式通信,gRPC 代理與 etcd 集羣通過 TLS 加密通信。下面我們進行實踐:

$ etcd --listen-client-urls https://localhost:12379 --advertise-client-urls https://localhost:2379 --cert-file=peer.crt --key-file=peer.key --trusted-ca-file=ca.crt --client-cert-auth

上述命令使用 HTTPS 啓動了單個成員的 etcd 集羣,然後確認 etcd 集羣以 HTTPS 的方式提供服務:

# fails
$ ETCDCTL_API=3 etcdctl --endpoints=http://localhost:2379 endpoint status
# works
$ ETCDCTL_API=3 etcdctl --endpoints=https://localhost:2379 --cert=client.crt --key=client.key --cacert=ca.crt endpoint status

顯然第一種方式不能訪問。

接下來通過使用客戶端證書連接到 etcd 端點https://localhost:2379,並在 localhost:12379 上啓動 gRPC 代理,命令如下:

$ etcd grpc-proxy start --endpoints=https://localhost:2379 --listen-addr localhost:12379 --cert client.crt --key client.key --cacert=ca.crt --insecure-skip-tls-verify

啓動後,我們通過 gRPC 代理寫入一個鍵值對測試:

$ ETCDCTL_API=3 etcdctl --endpoints=http://localhost:12379 put abc def
# OK

可以看到,使用 HTTP 的方式設置成功。

回顧上述操作,我們通過 etcd 的 gRPC 代理實現了代理與實際的 etcd 集羣之間的 TLS 加密,而本地的客戶端通過 HTTP 的方式與 gRPC 代理通信。因此這是一個簡便的調試和開發手段,你在生產環境需要謹慎使用,以防安全風險。

小結

本文我們主要介紹了 etcd 中的 gRPC proxy。gRPC 代理用於支持多個 etcd 服務器端點,當代理啓動時,它會隨機選擇一個 etcd 服務器端點來使用,該端點處理所有請求,直到代理檢測到端點故障爲止。如果 gRPC 代理檢測到端點故障,它將切換到其他可用的端點,對客戶端繼續提供服務,並且隱藏了存在問題的 etcd 服務端點。

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