gRPC 代理

一个在 gRPC 层工作的无状态 etcd 反向代理

gRPC 代理是一个无状态的 etcd 反向代理,工作在 gRPC 层(L7)。该代理旨在减少核心 etcd 集群的总处理负载。为了实现水平扩展,它合并了 watch 和 lease API 请求。为了保护集群免受滥用客户端的影响,它缓存了键范围请求。

gRPC 代理支持多个 etcd 服务器端点。当代理启动时,它会随机选择一个 etcd 服务器端点来使用。这个端点将处理所有请求,直到代理检测到端点故障。如果 gRPC 代理检测到端点故障,它会切换到另一个可用的端点,以隐藏故障对客户端的影响。未来可能会支持其他重试策略,例如加权轮询。

可扩展的 watch API

gRPC 代理将同一键或范围上的多个客户端观察者(c-watchers)合并为一个连接到 etcd 服务器的观察者(s-watcher)。代理将 s-watcher 的所有事件广播给其 c-watchers

假设 N 个客户端监视同一个键,一个 gRPC 代理可以将 etcd 服务器上的监视负载从 N 减少到 1。用户可以部署多个 gRPC 代理以进一步分发服务器负载。

在以下示例中,三个客户端监视键 A。gRPC 代理将这三个观察者合并为一个连接到 etcd 服务器的观察者。

            +-------------+
            | etcd server |
            +------+------+
                   ^ watch key A (s-watcher)
                   |
           +-------+-----+
           | gRPC proxy  | <-------+
           |             |         |
           ++-----+------+         |watch key A (c-watcher)
watch key A ^     ^ watch key A    |
(c-watcher) |     | (c-watcher)    |
    +-------+-+  ++--------+  +----+----+
    |  client |  |  client |  |  client |
    |         |  |         |  |         |
    +---------+  +---------+  +---------+

限制

为了有效地将多个客户端观察者合并为一个观察者,gRPC 代理会在可能的情况下将新的 c-watchers 合并到现有的 s-watcher 中。由于网络延迟或缓冲未传递的事件,这个合并的 s-watcher 可能与 etcd 服务器不同步。当未指定监视修订版本时,gRPC 代理不能保证 c-watcher 将从最新的存储修订版本开始监视。例如,如果客户端从修订版本 1000 的 etcd 服务器开始监视,那么该观察者将从修订版本 1000 开始。如果客户端从 gRPC 代理开始监视,则可能从修订版本 990 开始。

类似的限制也适用于取消操作。当观察者被取消时,etcd 服务器的修订版本可能大于取消响应的修订版本。

这两个限制对于大多数用例来说不应该造成问题。将来可能会有额外的选项,强制观察者绕过 gRPC 代理以获得更准确的修订版本响应。

可扩展的 lease API

为了保持其租约有效,客户端必须至少建立一个 gRPC 流到 etcd 服务器以发送周期性心跳。如果 etcd 工作负载涉及许多客户端的大量租约活动,这些流可能会导致 CPU 利用率过高。为了减少核心集群上的总流数量,代理支持租约流合并。

假设 N 个客户端正在更新租约,单个 gRPC 代理可以将 etcd 服务器上的流负载从 N 减少到 1。部署可以有额外的 gRPC 代理以进一步在多个代理之间分发流。

在以下示例中,三个客户端更新三个独立的租约(L1L2L3)。gRPC 代理将这三个客户端租约流(c-streams)合并为一个连接到 etcd 服务器的单个租约保活流(s-stream)。代理将客户端的租约心跳从 c-streams 转发到 s-stream,然后将响应返回给相应的 c-streams。

          +-------------+
          | etcd server |
          +------+------+
                 ^
                 | heartbeat L1, L2, L3
                 | (s-stream)
                 v
         +-------+-----+
         | gRPC proxy  +<-----------+
         +---+------+--+            | heartbeat L3
             ^      ^               | (c-stream)
heartbeat L1 |      | heartbeat L2  |
(c-stream)   v      v (c-stream)    v
      +------+-+  +-+------+  +-----+--+
      | client |  | client |  | client |
      +--------+  +--------+  +--------+

滥用客户端保护

当不违反一致性要求时,gRPC 代理会缓存请求的响应。这可以保护 etcd 服务器免受紧循环中的滥用客户端的影响。

启动 etcd gRPC 代理

考虑一个具有以下静态端点的 etcd 集群:

名称地址主机名
infra010.0.1.10infra0.example.com
infra110.0.1.11infra1.example.com
infra210.0.1.12infra2.example.com

使用以下命令启动 etcd gRPC 代理以使用这些静态端点:

$ etcd grpc-proxy start --endpoints=infra0.example.com,infra1.example.com,infra2.example.com --listen-addr=127.0.0.1:2379

etcd gRPC 代理启动并监听 2379 端口。它将客户端请求转发到上述三个端点之一。

通过代理发送请求:

$ ETCDCTL_API=3 etcdctl --endpoints=127.0.0.1:2379 put foo bar
OK
$ ETCDCTL_API=3 etcdctl --endpoints=127.0.0.1:2379 get foo
foo
bar

客户端端点同步和名称解析

代理支持通过写入用户定义的端点来注册其端点以进行发现。这有两个目的。首先,它允许客户端针对一组代理端点进行同步以实现高可用性。其次,它是 etcd gRPC 命名 的端点提供者。

通过提供用户定义的前缀来注册代理:

$ etcd grpc-proxy start --endpoints=localhost:2379 \
  --listen-addr=127.0.0.1:23790 \
  --advertise-client-url=127.0.0.1:23790 \
  --resolver-prefix="___grpc_proxy_endpoint" \
  --resolver-ttl=60

$ etcd grpc-proxy start --endpoints=localhost:2379 \
  --listen-addr=127.0.0.1:23791 \
  --advertise-client-url=127.0.0.1:23791 \
  --resolver-prefix="___grpc_proxy_endpoint" \
  --resolver-ttl=60

代理将列出所有成员以供成员列表使用:

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

+----+---------+--------------------------------+------------+-----------------+
| ID | STATUS  |              NAME              | PEER ADDRS |  CLIENT ADDRS   |
+----+---------+--------------------------------+------------+-----------------+
|  0 | started | Gyu-Hos-MBP.sfo.coreos.systems |            | 127.0.0.1:23791 |
|  0 | started | Gyu-Hos-MBP.sfo.coreos.systems |            | 127.0.0.1:23790 |
+----+---------+--------------------------------+------------+-----------------+

这使客户端能够通过 Sync 自动发现代理端点:

cli, err := clientv3.New(clientv3.Config{
    Endpoints: []string{"http://localhost:23790"},
})
if err != nil {
    log.Fatal(err)
}
defer cli.Close()

// fetch registered grpc-proxy endpoints
if err := cli.Sync(context.Background()); err != nil {
    log.Fatal(err)
}

请注意,如果代理配置没有解析器前缀,

$ etcd grpc-proxy start --endpoints=localhost:2379 \
  --listen-addr=127.0.0.1:23792 \
  --advertise-client-url=127.0.0.1:23792

对 grpc-proxy 的成员列表 API 将返回其自己的 advertise-client-url

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

+----+---------+--------------------------------+------------+-----------------+
| ID | STATUS  |              NAME              | PEER ADDRS |  CLIENT ADDRS   |
+----+---------+--------------------------------+------------+-----------------+
|  0 | started | Gyu-Hos-MBP.sfo.coreos.systems |            | 127.0.0.1:23792 |
+----+---------+--------------------------------+------------+-----------------+

命名空间

假设某个应用程序期望完全控制整个键空间,但 etcd 集群与其他应用程序共享。为了让所有应用程序互不干扰地运行,代理可以对 etcd 键空间进行分区,使客户端看起来拥有完整的键空间访问权限。当代理被赋予 --namespace 标志时,所有进入代理的客户端请求都会被转换为在键上带有用户定义的前缀。对 etcd 集群的访问将在该前缀下进行,而代理的响应将去掉该前缀;对于客户端来说,就好像根本没有前缀一样。

要命名空间化代理,请使用 --namespace 启动它:

$ etcd grpc-proxy start --endpoints=localhost:2379 \
  --listen-addr=127.0.0.1:23790 \
  --namespace=my-prefix/

现在对代理的访问在 etcd 集群上会被透明地加上前缀:

$ 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

TLS 终止

通过 gRPC 代理终止来自安全 etcd 集群的 TLS,从而提供未加密的本地端点。

要尝试这一点,请使用客户端 https 启动一个单成员的 etcd 集群:

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

确认客户端端口正在提供 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 &

最后,通过 http 在代理中插入一个键来测试 TLS 终止:

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

指标和健康检查

gRPC 代理为由 --endpoints 定义的 etcd 成员暴露 /health 和 Prometheus /metrics 端点。可以通过 --metrics-addr 标志定义一个附加 URL,该 URL 将同时响应 /metrics/health 端点。

$ etcd grpc-proxy start \
  --endpoints https://localhost:2379 \
  --metrics-addr https://0.0.0.0:4443 \
  --listen-addr 127.0.0.1:23790 \
  --key client.key \
  --key-file proxy-server.key \
  --cert client.crt \
  --cert-file proxy-server.crt \
  --cacert ca.pem \
  --trusted-ca-file proxy-ca.pem

已知问题

代理的主要接口同时服务于 HTTP2 和 HTTP/1.1。如果代理如上例所示设置为 TLS,则使用 cURL 等客户端针对监听接口发出请求时,需要显式设置协议为 HTTP/1.1 以返回 /metrics/health。通过使用 --metrics-addr 标志,辅助接口将不需要此要求。

 $ curl --cacert proxy-ca.pem --key proxy-client.key --cert proxy-client.crt https://127.0.0.1:23790/metrics --http1.1

最后更新于 2025 年 6 月 3 日:递归地将 v3.6 的内容复制到 v3.7(a90b2a6)