Skip to content

04etcd网关与gRPC-Gateway分别是什么?

etcd 中有 etcd 网关与 gRPC-Gateway 两种类型的网关,很多同学会迷糊:这两种网关有什么区别,各自的用途是什么?这一讲我将通过介绍这两种网关以及相关实践,来解答你的困惑。

etcd 网关模式:构建 etcd 集群的门户

etcd 网关是一个简单的TCP 代理 ,可将网络数据转发到 etcd 集群。网关是无状态且透明的,它既不会检查客户端请求,也不会干扰集群响应,支持多个 etcd 服务器实例,并采用简单的循环策略。etcd 网关将请求路由到可用端点,并向客户端隐藏故障,使得客户端感知不到服务端的故障。后期可能会支持其他访问策略,例如加权轮询。

什么时候使用 etcd 网关模式

我们使用客户端连接到 etcd 服务器时,每个访问 etcd 的应用程序必须知道所要访问的 etcd 集群实例的地址,即用来提供客户端服务的地址:ETCD_LISTEN_CLIENT_URLS。

如果同一服务器上的多个应用程序访问相同的 etcd 集群 ,每个应用程序仍需要知道 etcd 集群的广播的客户端端点地址。如果将 etcd 集群重新配置,拥有不同的端点,那么每个应用程序还需要更新其端点列表。在大规模集群环境下,重新配置的操作既造成了重复又容易出错。

以上问题,都可以通过 etcd 网关来解决:使用 etcd 网关作为稳定的本地端点,对于客户端应用程序 来说,不会感知到集群实例的变化。典型的 etcd 网关配置是使每台运行网关的计算机在本地地址上侦听,并且每个 etcd 应用程序都连接对应的本地网关,发生 etcd 集群实例的变更时,只需要网关更新其端点,而不需要更新每个客户端应用程序的代码实现。

当然也不是所有的场景都适用 etcd 网关,比如下面这两个场景就不适合使用。

  • 性能提升

    etcd 网关不是为提高 etcd 集群性能设计的。它不提供缓存、watch 流合并或批量处理等功能。etcd 团队目前正在开发一种缓存代理,旨在提高集群的可伸缩性。

  • 在集群上运行管理系统

    类似 Kubernetes 的高级集群管理系统本身支持服务发现。应用程序可以使用系统默认的 DNS 名称或虚拟 IP 地址访问 etcd 集群。例如,负责为 Service 提供 Cluster 内部的服务发现和负载均衡的 Kube-proxy 其实等效于 etcd 网关的职能。

总而言之,为了自动传播集群端点更改,etcd 网关在每台机器上都运行,为多个应用提供访问相同的 etcd 集群服务。

etcd 网关模式实践

下面我们基于 etcd 网关模式进行实战演练,有如下的环境:

启动 etcd 网关,以通过 etcd gateway 命令代理这些静态端点:

java
$ etcd gateway start --endpoints=http://192.168.10.7:2379,http://192.168.10.8:2379,http://192.168.10.9:2379
#响应结果如下所示:
{"level":"info","ts":1607794339.7171252,"caller":"tcpproxy/userspace.go:90","msg":"ready to proxy client requests","endpoints":["192.168.10.7:2379","192.168.10.8:2379","192.168.10.9:2379"]}

注意的是,---endpoints是以逗号分隔的、用于转发客户端连接的 etcd 服务器目标列表。默认值为127.0.0.1:2379,不能使用类似如下的配置:

java
 --endpoints=https://127.0.0.1:2379

因为网关并不能决定 TLS

其他常用配置如下:

  • --listen-addr 绑定的接口和端口,用于接受客户端请求,默认配置为 127.0.0.1:23790;

  • --retry-delay 重试连接到失败的端点延迟时间。默认为 1m0s。需要注意的是,值的后面标注单位,类似 123的设置不合法,命令行会出现参数不合法

java
invalid argument "123" for "--retry-delay" flag: time: missing unit in duration 123
  • --insecure-discovery 接受不安全或容易受到中间人攻击的 SRV 记录。默认为 false。

  • --trusted-ca-file 是 etcd 集群的客户端 TLS CA 文件的路径,用于认证端点。

除了使用静态指定 endpoint 的方式,还可以使用 DNS 进行服务发现,进行 DNS SRV 条目设置。你可以基于我们前面讲解的 DNS 发现模式的集群自行尝试。

gRPC-Gateway:为非 gRPC 的客户端提供 HTTP 接口

etcd v3 使用 gRPC 作为消息传输协议。etcd 项目中包括了基于 gRPC 的 Go client 和命令行工具 etcdctl,客户端通过 gRPC 框架与 etcd 集群通讯。对于不支持 gRPC 的客户端语言,etcd 提供 JSON 的 gRPC-Gateway,通过 gRPC-Gateway 提供 RESTful 代理,转换 HTTP/JSON 请求为 gRPC 的 Protocol Buffer 格式的消息。

这里你需要注意的是,在 HTTP 请求体中的 JSON 对象,其包含的 key 和 value 字段都被定义成了 byte 数组,因此必须在 JSON 对象中,使用 base64 编码对内容进行处理。为了方便,我在下面例子将会使用 curl 发起 HTTP 请求,其他的 HTTP/JSON 客户端(如浏览器、Postman 等)都可以进行这些操作。

etcd 版本与 gRPC-Gateway 接口对应的关系

gRPC-Gateway 提供的接口路径自 etcd v3.3 已经变更:

  • etcd v3.2 及之前的版本只能使用 [CLIENT-URL]/v3alpha/* 接口;

  • etcd v3.3 使用 CLIENT-URL/v3alpha/*;

  • etcd v3.4 使用 CLIENT-URL/v3beta/,且废弃了[CLIENT-URL]/v3alpha/;

  • etcd v3.5 只使用 CLIENT-URL/v3beta/。

通过上面的接口与 etcd 版本的对应关系,你可以看到,即使是 v3 版本下的 API,gRPC-Gateway 提供的接口路径在内部细分的版本下也有不同,所以需要注意当前正在使用的 etcd 版本。

下面我们将基于 etcd 提供的 gRPC-Gateway 接口进行键值对读写、watch、事务和安全认证的实践。

键值对读写操作

我们先来看看键值对的读写,分别使用接口 /v3/kv/range 和 /v3/kv/put 进行读写 keys。我们将键值对象写入 etcd:

java
$ curl -L http://localhost:2379/v3/kv/put \
  -X POST -d '{"key": "Zm9v", "value": "YmFy"}'
# 输出结果如下:
{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"16","raft_term":"9"}}

可以看到,我们通过 HTTP 请求成功写入了一对键值对,其中键为 Zm9v,值为 YmFy。键值对经过了 base64 编码,实际写入的键值对为 foo:bar。

接着,我们通过 /v3/kv/range 接口,来读取刚刚写入的键值对:

java
$ curl -L http://localhost:2379/v3/kv/range \
  -X POST -d '{"key": "Zm9v"}'
# 输出结果如下:
{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"16","raft_term":"9"},"kvs":[{"key":"Zm9v","create_revision":"13","mod_revision":"16","version":"4","value":"YmFy"}],"count":"1"}

通过 range 接口,获取 "Zm9v" 对应的值,完全符合我们的预期。

当我们想要获取前缀为指定值的键值对时,可以使用如下请求:

java
$ curl -L http://localhost:2379/v3/kv/range \
  -X POST -d '{"key": "Zm9v", "range_end": "Zm9w"}'
# 输出结果如下:
{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"16","raft_term":"9"},"kvs":[{"key":"Zm9v","create_revision":"13","mod_revision":"16","version":"4","value":"YmFy"}],"count":"1"}

我们在请求中指定了 key 的范围为 Zm9v-Zm9w。结果只返回一个键值对,符合预期。通过接口 /v3/kv/range 和 /v3/kv/put,我们可以方便地读写键值对。

watch 键值

键值对的 watch 也是 etcd 中经常用到的功能。etcd 中提供了 /v3/watch 接口来监测 keys,我们来 watch 刚刚写入的 "Zm9v",请求如下所示:

java
$ curl -N http://localhost:2379/v3/watch \
  -X POST -d '{"create_request": {"key":"Zm9v"} }' &
# 输出结果如下:
{"result":{"header":{"cluster_id":"12585971608760269493","member_id":"13847567121247652255","revision":"1","raft_term":"2"},"created":true}}

我们创建了一个监视 key 为 Zm9v 的请求,etcd 服务端返回了创建成功的结果。

另外发起一个请求,用以更新该键值,请求如下所示:

java
$ curl -L http://localhost:2379/v3/kv/put \
  -X POST -d '{"key": "Zm9v", "value": "YmFy"}' >/dev/null 2>&1

我们在 watch 请求的执行页面,可以看到如下结果:

当写入键值后,触发了监测事件的发生,控制台输出了时间的细节。HTTP 请求客户端与 etcd 服务端建立长连接,当监听的键值对发生变更时,便会将事件通知给客户端。

etcd 事务的实现

事务用于完成一组操作,通过对比指定的条件,成功的情况下执行相应的操作,否则回滚。在 gRPC-Gateway 中提供了 API接口,通过 /v3/kv/txn 接口发起一个事务。

我们先来对比指定键值对的创建版本,如果成功则执行更新操作。

为了获取创建版本,我们在执行前,先查询该键值对的信息:

java
# 查询键值对的版本
$ curl -L http://localhost:2379/v3/kv/range   -X POST -d '{"key": "Zm9v"}'
#响应结果
{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"20","raft_term":"9"},"kvs":[{"key":"Zm9v","create_revision":"13","mod_revision":"20","version":"8","value":"YmFy"}],"count":"1"}
# 事务,对比指定键值对的创建版本
$ curl -L http://localhost:2379/v3/kv/txn \
  -X POST \
  -d '{"compare":[{"target":"CREATE","key":"Zm9v","createRevision":"13"}],"success":[{"requestPut":{"key":"Zm9v","value":"YmFy"}}]}'
 #响应结果
 {"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"20","raft_term":"9"},"succeeded":true,"responses":[{"response_put":{"header":{"revision":"20"}}}]}

接着发起事务,用以设置键值,compare 是断言列表,拥有多个联合的条件,这里的条件是当 createRevision 的值为 13 时(我们在上面请求查询到该键值的创建版本为 13),表示符合条件,因此事务可以成功执行。

下面是一个对比指定键值对版本的事务,HTTP 请求实现如下:

java
# 事务,对比指定键值对的版本
$ curl -L http://localhost:2379/v3/kv/txn \
  -X POST \
  -d '{"compare":[{"version":"8","result":"EQUAL","target":"VERSION","key":"Zm9v"}],"success":[{"requestRange":{"key":"Zm9v"}}]}'
 #响应结果
{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"6","raft_term":"3"},"succeeded":true,"responses":[{"response_range":{"header":{"revision":"6"},"kvs":[{"key":"Zm9v","create_revision":"2","mod_revision":"6","version":"4","value":"YmF6"}],"count":"1"}}]}

上述命令获取指定版本的键值,可以看到 compare 中 target 的枚举值为 VERSION。通过比较,发现键 Zm9v 对应的 version 确实是 8,因此执行查询结果,返回 Zm9v 对应的正确值 YmF6。

HTTP 请求的安全认证

HTTP 的方式访问 etcd 服务端,需要考虑安全的问题,gRPC-Gateway 中提供的 API 接口支持开启安全认证。通过 /v3/auth 接口设置认证,实现认证的流程如下图所示:

java
# 创建 root 用户
$ curl -L http://localhost:2379/v3/auth/user/add \
-X POST -d '{"name": "root", "password": "123456"}'
#响应结果
{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"20","raft_term":"9"}}
# 创建 root 角色
curl -L http://localhost:2379/v3/auth/role/add \
  -X POST -d '{"name": "root"}'
#响应结果	{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"20","raft_term":"9"}}
# 为 root 用户授予角色
curl -L http://localhost:2379/v3/auth/user/grant \
  -X POST -d '{"user": "root", "role": "root"}'
#响应结果{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"20","raft_term":"9"}}
# 开启权限
$ curl -L http://localhost:2379/v3/auth/enable -X POST -d '{}'
#响应结果 {"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"20","raft_term":"9"}}

如上的请求中,我们首先创建了 root 用户和角色,将 root 角色赋予到 root 用户,这样就可以开启用户的权限。接下来就是进行身份验证,并进行 HTTP 访问。流程如下图所示:

使用 /v3/auth/authenticate API 接口对 etcd 进行身份验证以获取身份验证令牌:

java
# 获取 root 用户的认证令牌
$ curl -L http://localhost:2379/v3/auth/authenticate \
  -X POST -d '{"name": "root", "password": "123456"}'
#响应结果
{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"21","raft_term":"9"},"token":"DhRvXkWhOkINVQXI.57"}

请求获取到 token 的值为 DhRvXkWhOkINVQXI.57。

接下来,设置请求的头部 Authorization 为刚刚获取到的身份验证令牌,以使用身份验证凭据设置 key 值:

java
$ curl -L http://localhost:2379/v3/kv/put \
  -H 'Authorization : DhRvXkWhOkINVQXI.57' \
  -X POST -d '{"key": "Zm9v", "value": "YmFy"}'
#响应结果  	{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"21","raft_term":"9"}}

可以看到,上述请求设置成功 Zm9v 对应的 YmFy。如果 token 不合法,会出现 401 的错误,如下所示:

etcd gRPC-Gateway 中提供的 API 接口还有诸如 /v3/auth/role/delete、/v3/auth/role/get 等其他接口,限于篇幅,我只介绍了其中常用的几个,其他的接口可以参见:rpc.swagger.json

小结:etcd 网关与 gRPC-Gateway 的对比

通过上面的讲解与实践,我们来总结下这两种网关的用途和使用场景:

  • etcd 网关通常用于 etcd 集群的门户,是一个简单的 TCP 代理,将客户端请求转发到 etcd 集群,对外屏蔽了 etcd 集群内部的实际情况,在集群出现故障或者异常时,可以通过 etcd 网关进行切换;

  • gRPC-Gateway 则是对于 etcd 的 gRPC 通信协议的补充,有些语言的客户端不支持 gRPC 通信协议,此时就可以使用 gRPC-Gateway 对外提供的 HTTP API 接口。通过 HTTP 请求,实现与 gRPC 调用协议同样的功能。

总的来说,etcd 网关与 gRPC-Gateway 是 etcd 中两个不同的功能,不能混为一谈,只有当你理解了它们各自的功能,才能在适当的场景使用。

本讲主要内容如下:

学完这一讲,你对 etcd 网关与 gRPC-Gateway 有什么使用方面的见解或者实践经历?欢迎在留言区和我分享。下一讲我们将会学习如何实现可伸缩的 etcd API,即 etcd gRPC 代理的模式。