Skip to content

06集群配置:如何动态重配置etcd集群?

今天我和你分享的主题与 etcd 集群动态调整相关。

etcd 集群部署之后,动态调整集群是经常发生的情况,比如增加 etcd 节点、移除某个 etcd 节点,或者是更新 etcd 节点的信息。这些情况都需要我们动态调整 etcd 集群。

这一讲我将介绍 etcd 如何进行常见的集群运行时重配置操作,etcd 运行时重配置命令的设计以及需要注意的内容。

这部分内容官方文档有所提及,我在写这篇内容时也和编辑有过沟通,但我认为还是有必要再讲一下的,因为集群运行时重配置是一个风险比较高的操作,仅仅阅读官方文档恐怕难以完全理解如何实践操作。通过这一讲你可以学习到集群运行时重配置的详细实战及讲解,这也是官方文档不能带给你的。

集群运行时重配置

集群运行时重配置的前提条件是只有在大多数集群成员都在正常运行时,etcd 集群才能处理重配置请求

从两个成员的集群中删除一个成员是不安全的,因为两个成员的集群中的大多数也是两个,如果在删除过程中出现故障,集群就可能无法运行,需要从多数故障中重新启动。因此 etcd 官方建议:生产环境的集群大小始终大于两个节点

使用场景介绍

集群的动态重新配置一般的使用场景,如下图所示:

集群动态调整的场景图

如上所述场景中的大多数,都会涉及添加或移除成员。这些操作一般都会使用到 etcd 自带的 etcdctl 命令行工具,命令如下所示:

java
member add          已有集群中增加成员
member remove       移除已有集群中的成员
member update       更新集群中的成员
member list         集群成员列表

除了使用 etcdctl 修改成员,还可以使用 etcd v3 gRPC members API

下面我基于 etcdctl 具体介绍 etcd 集群如何进行更新成员、删除成员和增加新成员等运维操作。

更新成员

首先我们来看更新成员的实践。更新成员有两种情况:client URLspeer URLs

我们回想下这两个配置的功能:

  • client URLs 用于客户端的 URL,也就是对外服务的 URL;

  • peer URLs 用作监听 URL,用于与其他节点通讯。

更新 client URLs

为了更新成员的 client URLs,只需要使用更新后的 client URL 标记(即 --advertise-client-urls)或者环境变量来重启这个成员(ETCD_ADVERTISE_CLIENT_URLS)。重启后的成员将自行发布更新后的 URL,错误更新的 client URL 将不会影响 etcd 集群的健康。

更新 peer URLs

要更新成员的 peer URLs,首先通过成员命令更新它,然后重启成员,因为更新 peer URL 修改了集群范围配置并能影响 etcd 集群的健康。

当我们要更新某个成员的 peer URL,需要找到该目标成员的 ID,使用 etcdctl 列出所有成员:

powershell
//设置环境变量
$ ENDPOINTS=http://localhost:22379
// 查询所有的集群成员
$ etcdctl  --endpoints=$ENDPOINTS  member list -w table
+------------------+---------+--------+------------------------+------------------------+------------+
|        ID        | STATUS  |  NAME  |       PEER ADDRS       |      CLIENT ADDRS      | IS LEARNER |
+------------------+---------+--------+------------------------+------------------------+------------+
| 8211f1d0f64f3269 | started | infra1 | http://127.0.0.1:12380 | http://127.0.0.1:12379 |      false |
| 91bc3c398fb3c146 | started | infra2 | http://127.0.0.1:22380 | http://127.0.0.1:22379 |      false |
| fd422379fda50e48 | started | infra3 | http://127.0.0.1:32380 | http://127.0.0.1:32379 |      false |
+------------------+---------+--------+------------------------+------------------------+------------+

在这个例子中,我们启动了三个节点的 etcd 集群。更新 8211f1d0f64f3269 成员 ID 并修改它的 peer URLs 值为 http://127.0.0.1:2380

java
$ etcdctl   --endpoints=http://localhost:12379    member update 8211f1d0f64f3269  --peer-urls=http://127.0.0.1:2380

Member 8211f1d0f64f3269 updated in cluster ef37ad9dc622a7c4

可以看到,集群中 8211f1d0f64f3269 对应的成员信息更新成功。更新之后,集群的成员列表如下所示:

集群列表

随后使用新的配置重启 infra 1,即可完成 etcd 集群成员的 peer URLs 更新。

删除成员

基于上面三个节点的集群,假设我们要删除 ID 为 8211f1d0f64f3269 的成员,可使用 remove 命令来执行成员的删除:

java
$ etcdctl --endpoints=$ENDPOINTS member remove 8211f1d0f64f3269
Member 8211f1d0f64f3269 removed from cluster ef37ad9dc622a7c4

可以看到已经成功执行移除集群中 8211f1d0f64f3269 对应的成员 etcd 1,检查下成员列表进行确认:

此时目标成员将会自行关闭服务,并在日志中打印出移除信息:

java
13:14:54 etcd1 | {"level":"warn","ts":"2020-10-18T13:14:54.368+0800","caller":"rafthttp/peer_status.go:68","msg":"peer became inactive (message send to peer failed)","peer-id":"fd422379fda50e48","error":"failed to dial fd422379fda50e48 on stream Message (the member has been permanently removed from the cluster)"}
13:14:54 etcd1 | {"level":"warn","ts":"2020-10-18T13:14:54.368+0800","caller":"etcdserver/server.go:1084","msg":"server error","error":"the member has been permanently removed from the cluster"}

这种方式可以安全地移除 leader 和其他成员。如果是移除 leader 的场景,新 leader 被选举时集群将处于不活动状态(inactive),且持续时间通常由选举超时时间和投票过程决定

添加新成员

当我们新起节点时,需要加入现有的 etcd 集群中。添加新成员的过程有如下两个步骤:

  1. 通过 HTTP members API 添加新成员到集群,gRPC members API 或者 etcdctl member add 命令;

  2. 使用新的集群配置启动新成员,包括更新后的成员列表(现有成员加上新成员):

java
$ etcdctl --endpoints=http://localhost:22379 member add infra4 --peer-urls=http://localhost:2380
Member 574399926694aee9 added to cluster ef37ad9dc622a7c4
ETCD_NAME="infra4"
ETCD_INITIAL_CLUSTER="infra4=http://localhost:2380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://localhost:2380"
ETCD_INITIAL_CLUSTER_STATE="existing"

如上的命令使用 etcdctl 指定 name 和 advertised peer URLs 来添加新的成员到集群。我们新增了名为 infra 4 的节点,其启动标志了 --peer-urls=http://localhost:2380

通过命令行的输出,可以看到添加成员执行成功。成员 574399926694aee9 添加到集群 ef37ad9dc622a7c4,并在下方输出了集群现有的信息,这些信息很重要。

下面步骤就是基于新的集群配置启动刚刚添加的成员,我们主要是直接使用 etcd 启动的方式:

java
etcd --name infra4 --listen-client-urls http://127.0.0.1:2379 --advertise-client-urls http://127.0.0.1:2379 --listen-peer-urls http://127.0.0.1:2380 --initial-advertise-peer-urls http://127.0.0.1:2380 --initial-cluster-token ef37ad9dc622a7c4 --initial-cluster-state existing --initial-cluster 'infra4=http://127.0.0.1:2380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --enable-pprof --logger=zap --log-outputs=stderr

虽然在启动命令中指定了集群的成员、集群的标志、集群状态等信息,但是会出现如下的报错:

shell
Members:[&{ID:18d3ac4dcf19552b RaftAttributes:{PeerURLs:[http://localhost:2380] IsLearner:false} Attributes:{Name: ClientURLs:[]}} &{ID:91bc3c398fb3c146 RaftAttributes:{PeerURLs:[http://127.0.0.1:22380] IsLearner:false} Attributes:{Name:infra2 ClientURLs:[http://127.0.0.1:22379]}} &{ID:fd422379fda50e48 RaftAttributes:{PeerURLs:[http://127.0.0.1:32380] IsLearner:false} Attributes:{Name:infra3 ClientURLs:[http://127.0.0.1:32379]}}] RemovedMemberIDs:[]}: unmatched member while checking PeerURLs (\"http://127.0.0.1:32380\"(resolved from \"http://127.0.0.1:32380\") != \"http://127.0.0.1:2380\"(resolved from \"http://127.0.0.1:2380\"))","stacktrace":"go.etcd.io/etcd/etcdmain.startEtcdOrProxyV2\n\t/tmp/etcd-release-3.4.5/etcd/release/etcd/etcdmain/etcd.go:271\ngo.etcd.io/etcd/etcdmain.Main\n\t/tmp/etcd-release-3.4.5/etcd/release/etcd/etcdmain/main.go:46\nmain.main\n\t/tmp/etcd-release-3.4.5/etcd/release/etcd/main.go:28\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:200"}

根据报错可以知道,这种方式使得启动的新节点也是集群的方式,peer URLs 不匹配,导致了启动报错。

我们需要知道 etcdctl 添加成员时已经给出关于新成员的集群信息,并打印出成功启动它需要的环境变量。因此使用关联的标记为新的成员启动 etcd 进程:

java
$ export ETCD_NAME="infra4"
$ export ETCD_INITIAL_CLUSTER="infra4=http://localhost:2380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380"
$ export ETCD_INITIAL_CLUSTER_STATE=existing
$ etcd --listen-client-urls http://localhost:2379 --advertise-client-urls http://localhost:2379 --listen-peer-urls http://localhost:2380 --initial-advertise-peer-urls http://localhost:2380

如上述的命令执行完成,新成员将作为集群的一部分运行并立即开始同步集群的其他成员。如果添加多个成员,官方推荐的做法是每次配置单个成员,并在添加更多新成员前验证它正确启动

我们此时查看集群的状态如下:

shell
$ etcdctl --endpoints=http://localhost:22379   member list -w table
+------------------+---------+--------+------------------------+------------------------+------------+
|        ID        | STATUS  |  NAME  |       PEER ADDRS       |      CLIENT ADDRS      | IS LEARNER |
+------------------+---------+--------+------------------------+------------------------+------------+
| 18d3ac4dcf19552b | started | infra4 | http://localhost:2380  | http://localhost:2379  |      false |
| 91bc3c398fb3c146 | started | infra2 | http://127.0.0.1:22380 | http://127.0.0.1:22379 |      false |
| fd422379fda50e48 | started | infra3 | http://127.0.0.1:32380 | http://127.0.0.1:32379 |      false |
+------------------+---------+--------+------------------------+------------------------+------------+

除此之外,如果添加新成员到一个节点的集群,在新成员启动前集群无法继续工作 ,因为它需要两个成员作为 galosh 才能在一致性上达成一致。这种情况仅会发生 在 etcdctl member add影响集群和新成员成功建立连接到已有成员的时间内

运行时重配置的设计及注意点

我上面介绍了 etcd 集群重配置的常见操作。运行时重配置是分布式系统中难点之一,也很容易出错,我们需要了解运行时重配置命令的设计和注意点。

两阶段配置变更设计

在 etcd 中,出于安全考虑,每个 etcd 集群节点进行运行时重配置都必须经历两个阶段:通知集群新配置、加入新成员

上面介绍的几种集群操作都是按照这两个步骤进行的。以添加新成员为例,两阶段描述如下。

  • 阶段一:通知集群新配置

将成员添加到 etcd 集群中,需要通过调用 API 将新成员添加到集群中。当集群同意配置修改时,API 调用返回。

  • 阶段二:加入新成员

要将新的 etcd 成员加入现有集群,需要指定正确的initial-cluster并将initial-cluster-state设置为 existing。成员启动时,它首先与现有集群通信,并验证当前集群配置是否与initial-cluster中指定的预期配置匹配。当新成员成功启动时,集群已达到预期的配置。

将过程分为两个独立的阶段,运维人员需要了解集群成员身份的变化,这实际上为我们提供了更大的灵活性,也更容易理解这个过程。

我们通过上面的实践可以发现,进行集群运行重配置时,每一阶段都会确认集群成员的数量和状态,当第一阶段没有问题时才会进行下一阶段的操作。这是为了第一阶段的状态不正常时,我们可以及时进行修正,从而避免因为第一阶段的配置问题,导致集群进入无序和混乱的状态。

集群重配置注意点

我们在前面进行了集群运行时重配置的介绍与实践,但有两点你在重配置时要特别注意。

  • 集群永久失去它的大多数成员,需要从旧数据目录启动新集群来恢复之前的状态

集群永久失去它的大多数成员的情况下,完全有可能从现有集群中强制删除发生故障的成员来完成恢复。但是,etcd 不支持该方法,因为它绕过了不安全的常规共识提交阶段。

如果要删除的成员实际上并没有挂掉或通过同一集群中的不同成员强行删除,etcd 最终会得到具有相同 clusterID 的分散集群。这种方式将会导致后续很难调试和修复。

  • 运行时重配置禁止使用公用发现服务

公共发现服务应该仅用于引导集群。成功引导集群后,成员的 IP 地址都是已知的。若要将新成员加入现有集群,需使用运行时重新配置 API

如果依靠公共发现服务会存在一些问题,如公共发现服务自身存在的网络问题、公共发现服务后端是否能够支撑访问负载等。

通过以上的介绍你知道了 etcd 公共发现服务的种种问题。如果要使用运行时重配置的发现服务,你最好选择构建一个私有服务

小结

这一讲我主要介绍了 etcd 运行时重配置集群的常见操作以及 etcd 是如何设计运行时重配置、使用的注意点。

本讲内容如下:

分布式系统中,运行时集群重配置是一个难点。运行时重配置会涉及集群的稳定性和可用性,因此需要慎之又慎,尽可能避免运行时集群重配置。如果你必须重配置 etcd 集群,你需要遵循两阶段配置变更的思想,平稳可靠地进行重配置操作。

关于动态重配置,你有什么经验和踩坑的经历,欢迎你在留言区和我分享。