将 etcd 从 3.2 升级到 3.3

关于将 etcd 从 3.2 升级到 3.3 的流程、检查清单和注意事项

通常情况下,从 etcd 3.2 升级到 3.3 可以实现零停机、滚动升级:

  • 逐个停止 etcd v3.2 进程,并将其替换为 etcd v3.3 进程
  • 在所有进程都运行 v3.3 版本后,集群即可使用 v3.3 中的新功能

开始升级之前,请通读本指南其余部分以做好准备。

升级检查清单

注意:从 v2 迁移且没有 v3 数据时,如果 etcd 从现有快照恢复但不存在 v3 的 ETCD_DATA_DIR/member/snap/db 文件,etcd 服务器 v3.2+ 将会 panic。这种情况发生在服务器已从 v2 迁移但之前没有 v3 数据时。这也防止了意外的 v3 数据丢失(例如 db 文件可能已被移动)。etcd 要求只有在存在 v3 数据的情况下才能进行后续的 v3 版本迁移。在 v3.0 服务器包含 v3 数据之前,请勿升级到更新的 v3 版本。

注意: 如果启用了认证并使用租约(lease TTL 较小),则有很大概率遇到问题,导致数据不一致。强烈建议先升级到 3.2.31+ 以修复此问题,然后再升级到 3.3。此外,在升级过程中,如果无权限用户向 3.3 节点发送 LeaseRevoke 请求,仍可能导致数据损坏,因此最好确保您的环境中不存在此类异常调用。详情请参见 #11691

3.3 版本中的主要破坏性变更

etcd --auto-compaction-retention 标志的值类型更改为 string

修改 --auto-compaction-retention 标志以支持字符串值,并具有更精细的粒度。由于现在 --auto-compaction-retention 接受字符串值,etcd 配置 YAML 文件中的 auto-compaction-retention 字段也必须更改为 string 类型。以前,--config-file etcd.config.yaml 可以包含 auto-compaction-retention: 24 字段,现在必须改为 auto-compaction-retention: "24"auto-compaction-retention: "24h"。如果配置为 --auto-compaction-mode periodic --auto-compaction-retention "24h",则 --auto-compaction-retention 参数的时间持续值必须对 Go 中的 time.ParseDuration 函数有效。

# etcd.config.yaml
+auto-compaction-mode: periodic
-auto-compaction-retention: 24
+auto-compaction-retention: "24"
+# Or
+auto-compaction-retention: "24h"

etcdserver.EtcdServer.ServerConfig 更改为 *etcdserver.EtcdServer.ServerConfig

etcdserver.EtcdServer 将其成员字段的类型从 *etcdserver.ServerConfig 更改为 etcdserver.ServerConfig。同时,etcdserver.NewServer 现在接受 etcdserver.ServerConfig,而不是 *etcdserver.ServerConfig

升级前后对比(例如 k8s.io/kubernetes/test/e2e_node/services/etcd.go

import "github.com/coreos/etcd/etcdserver"

type EtcdServer struct {
	*etcdserver.EtcdServer
-	config *etcdserver.ServerConfig
+	config etcdserver.ServerConfig
}

func NewEtcd(dataDir string) *EtcdServer {
-	config := &etcdserver.ServerConfig{
+	config := etcdserver.ServerConfig{
		DataDir: dataDir,
        ...
	}
	return &EtcdServer{config: config}
}

func (e *EtcdServer) Start() error {
	var err error
	e.EtcdServer, err = etcdserver.NewServer(e.config)
    ...

新增 embed.Config.LogOutput 结构体

请注意,该字段在 v3.4 中已重命名为 embed.Config.LogOutputs,类型为 []string。更多细节请参见 v3.4 升级指南

embed.Config 中添加了 LogOutput 字段:

package embed

type Config struct {
 	Debug bool `json:"debug"`
 	LogPkgLevels string `json:"log-package-levels"`
+	LogOutput string `json:"log-output"`
 	...

此前 gRPC 服务器的警告日志记录在 etcdserver 中。

WARNING: 2017/11/02 11:35:51 grpc: addrConn.resetTransport failed to create client transport: connection error: desc = "transport: Error while dialing dial tcp: operation was canceled"; Reconnecting to {localhost:2379 <nil>}
WARNING: 2017/11/02 11:35:51 grpc: addrConn.resetTransport failed to create client transport: connection error: desc = "transport: Error while dialing dial tcp: operation was canceled"; Reconnecting to {localhost:2379 <nil>}

从 v3.3 开始,默认禁用 gRPC 服务器日志。

注意,embed.Config.SetupLogging 方法在 v3.4 版本中已被弃用。详情请参阅 v3.4 升级指南

import "github.com/coreos/etcd/embed"

cfg := &embed.Config{Debug: false}
cfg.SetupLogging()

embed.Config.Debug 字段设置为 true 以启用 gRPC 服务器日志。

更改了 /health 端点的响应

此前,[endpoint]:[client-port]/health 返回的是手动序列化的 JSON 值。3.3 版本现在定义了 etcdhttp.Health 结构体。

请注意,在 v3.3.0-rc.0、v3.3.0-rc.1 和 v3.3.0-rc.2 版本中,etcdhttp.Health 包含布尔类型的 "health""errors" 字段。为了向后兼容,我们已将 "health" 字段恢复为 string 类型,并移除了 "errors" 字段。更详细的健康信息将在独立的 API 中提供。

$ curl http://localhost:2379/health
{"health":"true"}

更改了 gRPC 网关 HTTP 端点(将 /v3alpha 替换为 /v3beta

之前

curl -L http://localhost:2379/v3alpha/kv/put \
  -X POST -d '{"key": "Zm9v", "value": "YmFy"}'

之后

curl -L http://localhost:2379/v3beta/kv/put \
  -X POST -d '{"key": "Zm9v", "value": "YmFy"}'

/v3alpha 端点的请求将重定向到 /v3beta,且 /v3alpha 将在 3.4 版本中被移除。

更改了最大请求大小限制

3.3 版本现在允许为服务器端和客户端配置自定义请求大小限制。在之前的版本(v3.2.10、v3.2.11)中,客户端响应大小仅限于 4 MiB。

服务器端请求限制可通过 --max-request-bytes 参数进行配置:

# limits request size to 1.5 KiB
etcd --max-request-bytes 1536

# client writes exceeding 1.5 KiB will be rejected
etcdctl put foo [LARGE VALUE...]
# etcdserver: request is too large

或者配置 embed.Config.MaxRequestBytes 字段:

import "github.com/coreos/etcd/embed"
import "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"

// limit requests to 5 MiB
cfg := embed.NewConfig()
cfg.MaxRequestBytes = 5 * 1024 * 1024

// client writes exceeding 5 MiB will be rejected
_, err := cli.Put(ctx, "foo", [LARGE VALUE...])
err == rpctypes.ErrRequestTooLarge

若未指定,服务器端限制默认为 1.5 MiB

客户端请求限制必须基于服务器端限制进行配置。

# limits request size to 1 MiB
etcd --max-request-bytes 1048576
import "github.com/coreos/etcd/clientv3"

cli, _ := clientv3.New(clientv3.Config{
    Endpoints: []string{"127.0.0.1:2379"},
    MaxCallSendMsgSize: 2 * 1024 * 1024,
    MaxCallRecvMsgSize: 3 * 1024 * 1024,
})


// client writes exceeding "--max-request-bytes" will be rejected from etcd server
_, err := cli.Put(ctx, "foo", strings.Repeat("a", 1*1024*1024+5))
err == rpctypes.ErrRequestTooLarge


// client writes exceeding "MaxCallSendMsgSize" will be rejected from client-side
_, err = cli.Put(ctx, "foo", strings.Repeat("a", 5*1024*1024))
err.Error() == "rpc error: code = ResourceExhausted desc = grpc: trying to send message larger than max (5242890 vs. 2097152)"


// some writes under limits
for i := range []int{0,1,2,3,4} {
    _, err = cli.Put(ctx, fmt.Sprintf("foo%d", i), strings.Repeat("a", 1*1024*1024-500))
    if err != nil {
        panic(err)
    }
}
// client reads exceeding "MaxCallRecvMsgSize" will be rejected from client-side
_, err = cli.Get(ctx, "foo", clientv3.WithPrefix())
err.Error() == "rpc error: code = ResourceExhausted desc = grpc: received message larger than max (5240509 vs. 3145728)"

若未指定,客户端发送限制默认为 2 MiB(1.5 MiB + gRPC 开销字节),接收限制默认为 math.MaxInt32。更多详情请参见 clientv3 godoc

更改了原始 gRPC 客户端包装函数的签名

3.3 版本更改了 clientv3 gRPC 客户端包装函数的签名。此更改是为了支持在消息大小限制上使用自定义 grpc.CallOption

更改前后对比

-func NewKVFromKVClient(remote pb.KVClient) KV {
+func NewKVFromKVClient(remote pb.KVClient, c *Client) KV {

-func NewClusterFromClusterClient(remote pb.ClusterClient) Cluster {
+func NewClusterFromClusterClient(remote pb.ClusterClient, c *Client) Cluster {

-func NewLeaseFromLeaseClient(remote pb.LeaseClient, keepAliveTimeout time.Duration) Lease {
+func NewLeaseFromLeaseClient(remote pb.LeaseClient, c *Client, keepAliveTimeout time.Duration) Lease {

-func NewMaintenanceFromMaintenanceClient(remote pb.MaintenanceClient) Maintenance {
+func NewMaintenanceFromMaintenanceClient(remote pb.MaintenanceClient, c *Client) Maintenance {

-func NewWatchFromWatchClient(wc pb.WatchClient) Watcher {
+func NewWatchFromWatchClient(wc pb.WatchClient, c *Client) Watcher {

更改了 clientv3 Snapshot API 的错误类型

此前,clientv3 Snapshot API 返回的是原始的 [grpc/*status.statusError] 类型错误。v3.3 现在将这些错误转换为相应的公共错误类型,以与其他 API 保持一致。

之前

import "context"

// reading snapshot with canceled context should error out
ctx, cancel := context.WithCancel(context.Background())
rc, _ := cli.Snapshot(ctx)
cancel()
_, err := io.Copy(f, rc)
err.Error() == "rpc error: code = Canceled desc = context canceled"

// reading snapshot with deadline exceeded should error out
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
defer cancel()
rc, _ = cli.Snapshot(ctx)
time.Sleep(2 * time.Second)
_, err = io.Copy(f, rc)
err.Error() == "rpc error: code = DeadlineExceeded desc = context deadline exceeded"

之后

import "context"

// reading snapshot with canceled context should error out
ctx, cancel := context.WithCancel(context.Background())
rc, _ := cli.Snapshot(ctx)
cancel()
_, err := io.Copy(f, rc)
err == context.Canceled

// reading snapshot with deadline exceeded should error out
ctx, cancel = context.WithTimeout(context.Background(), time.Second)
defer cancel()
rc, _ = cli.Snapshot(ctx)
time.Sleep(2 * time.Second)
_, err = io.Copy(f, rc)
err == context.DeadlineExceeded

更改了 etcdctl lease timetolive 命令的输出

此前,对已过期的租约执行 lease timetolive LEASE_ID 命令时,剩余秒数会显示为 -1s。3.3 版本现在会输出更清晰的消息。

之前

lease 2d8257079fa1bc0c granted with TTL(0s), remaining(-1s)

之后

lease 2d8257079fa1bc0c already expired

更改了 golang.org/x/net/context 的导入

clientv3 已弃用 golang.org/x/net/context。如果项目在其他代码中(例如 etcd 生成的协议缓冲区代码)引入了 golang.org/x/net/context 并导入了 github.com/coreos/etcd/clientv3,则需要 Go 1.9+ 才能编译。

之前

import "golang.org/x/net/context"
cli.Put(context.Background(), "f", "v")

之后

import "context"
cli.Put(context.Background(), "f", "v")

更改了 gRPC 依赖

3.3 版本现在要求使用 grpc/grpc-go v1.7.5

弃用 grpclog.Logger

grpclog.Logger 已被 grpclog.LoggerV2 取代。clientv3.Logger 现在是 grpclog.LoggerV2

之前

import "github.com/coreos/etcd/clientv3"
clientv3.SetLogger(log.New(os.Stderr, "grpc: ", 0))

之后

import "github.com/coreos/etcd/clientv3"
import "google.golang.org/grpc/grpclog"
clientv3.SetLogger(grpclog.NewLoggerV2(os.Stderr, os.Stderr, os.Stderr))

// log.New above cannot be used (not implement grpclog.LoggerV2 interface)
弃用 grpc.ErrClientConnTimeout

以前,客户端拨号超时时会返回 grpc.ErrClientConnTimeout 错误。3.3 版本改为返回 context.DeadlineExceeded(参见 #8504)。

之前

// expect dial time-out on ipv4 blackhole
_, err := clientv3.New(clientv3.Config{
    Endpoints:   []string{"http://254.0.0.1:12345"},
    DialTimeout: 2 * time.Second
})
if err == grpc.ErrClientConnTimeout {
	// handle errors
}

之后

_, err := clientv3.New(clientv3.Config{
    Endpoints:   []string{"http://254.0.0.1:12345"},
    DialTimeout: 2 * time.Second
})
if err == context.DeadlineExceeded {
	// handle errors
}

更改了官方容器镜像仓库

etcd 现在使用 gcr.io/etcd-development/etcd 作为主容器镜像仓库,quay.io/coreos/etcd 作为备用仓库。

之前

docker pull quay.io/coreos/etcd:v3.2.5

之后

docker pull gcr.io/etcd-development/etcd:v3.3.0

升级到 >= v3.3.14

v3.3.14 不得不包含了一些来自 3.4 的功能,同时尽量减少客户端负载均衡器实现之间的差异。此版本修复了 “当第一个 etcd 服务器不可用时,kube-apiserver 1.13.x 拒绝工作”(kubernetes#72102) 的问题。

grpc.ErrClientConnClosing 已在 gRPC >= 1.10 中被弃用

import (
+	"go.etcd.io/etcd/clientv3"

	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
)

_, err := kvc.Get(ctx, "a")
-if err == grpc.ErrClientConnClosing {
+if clientv3.IsConnCanceled(err) {

// or
+s, ok := status.FromError(err)
+if ok {
+  if s.Code() == codes.Canceled

新的客户端负载均衡器 使用异步解析器将端点传递给 gRPC 拨号函数。因此,v3.3.14 或更高版本需要使用 grpc.WithBlock 拨号选项,以等待底层连接建立完成。

import (
	"time"
	"go.etcd.io/etcd/clientv3"
+	"google.golang.org/grpc"
)

+// "grpc.WithBlock()" to block until the underlying connection is up
ccfg := clientv3.Config{
  Endpoints:            []string{"localhost:2379"},
  DialTimeout:          time.Second,
+ DialOptions:          []grpc.DialOption{grpc.WithBlock()},
  DialKeepAliveTime:    time.Second,
  DialKeepAliveTimeout: 500 * time.Millisecond,
}

请参阅 CHANGELOG 查看完整的变更列表。

服务器升级检查清单

升级要求

要将现有的 etcd 部署升级到 3.3 版本,运行中的集群必须是 3.2 或更高版本。如果低于 3.2,请先升级到 3.2,然后再升级到 3.3。

此外,为确保平滑的滚动升级,运行中的集群必须处于健康状态。在继续操作前,请使用 etcdctl endpoint health 命令检查集群健康状况。

准备工作

在升级 etcd 之前,务必先在预演环境中测试依赖 etcd 的服务,然后再将升级部署到生产环境。

开始之前,请先备份 etcd 数据。如果升级过程中出现问题,可以使用此备份回滚到现有的 etcd 版本。请注意,snapshot 命令仅备份 v3 数据。对于 v2 数据,请参见 v2 数据存储的备份方法

混合版本

升级期间,etcd 集群支持混合版本的成员,并以最低公共版本的协议运行。只有当所有成员都升级到 3.3 版本后,集群才被视为已升级。内部而言,etcd 成员之间会相互协商以确定整体集群版本,该版本控制着报告的版本和所支持的功能。

限制

注意:如果集群仅有 v3 数据而无 v2 数据,则不受此限制影响。

如果集群提供的 v2 数据集大于 50MB,则每个新升级的成员可能需要最多两分钟才能追上现有集群的进度。可通过检查最近快照的大小来估算总数据量。换句话说, safest 的做法是在每次升级成员之间等待 2 分钟。

对于更大的总数据量(100MB 或以上),这个一次性过程可能需要更长时间。对于如此大规模的大型 etcd 集群的管理员,可以在升级前联系etcd 团队,我们将很乐意提供升级流程建议。

Downgrade

如果所有成员都已升级到 v3.3,集群将升级至 v3.3,且从此完成状态降级是不可能的。然而,如果有任何一个成员仍为 v3.2,则集群及其操作仍保持为“v3.2”状态,此时可以从这种混合集群状态恢复为在所有成员上使用 v3.2 的 etcd 二进制文件。

备份所有 etcd 成员的数据目录,以便即使集群已完成升级后仍能进行降级。

升级流程

本示例演示如何升级在本地机器上运行的由 3 个成员组成的 v3.2 etcd 集群。

1. 检查升级要求

集群是否健康并运行 v3.2.x 版本?

$ ETCDCTL_API=3 etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
localhost:2379 is healthy: successfully committed proposal: took = 6.600684ms
localhost:22379 is healthy: successfully committed proposal: took = 8.540064ms
localhost:32379 is healthy: successfully committed proposal: took = 8.763432ms

$ curl http://localhost:2379/version
{"etcdserver":"3.2.7","etcdcluster":"3.2.0"}

2. 停止现有的 etcd 进程

每当一个 etcd 进程停止时,集群中的其他成员会记录预期的错误日志。这是正常现象,因为集群成员之间的连接已(临时)中断。

14:13:31.491746 I | raft: c89feb932daef420 [term 3] received MsgTimeoutNow from 6d4f535bae3ab960 and starts an election to get leadership.
14:13:31.491769 I | raft: c89feb932daef420 became candidate at term 4
14:13:31.491788 I | raft: c89feb932daef420 received MsgVoteResp from c89feb932daef420 at term 4
14:13:31.491797 I | raft: c89feb932daef420 [logterm: 3, index: 9] sent MsgVote request to 6d4f535bae3ab960 at term 4
14:13:31.491805 I | raft: c89feb932daef420 [logterm: 3, index: 9] sent MsgVote request to 9eda174c7df8a033 at term 4
14:13:31.491815 I | raft: raft.node: c89feb932daef420 lost leader 6d4f535bae3ab960 at term 4
14:13:31.524084 I | raft: c89feb932daef420 received MsgVoteResp from 6d4f535bae3ab960 at term 4
14:13:31.524108 I | raft: c89feb932daef420 [quorum:2] has received 2 MsgVoteResp votes and 0 vote rejections
14:13:31.524123 I | raft: c89feb932daef420 became leader at term 4
14:13:31.524136 I | raft: raft.node: c89feb932daef420 elected leader c89feb932daef420 at term 4
14:13:31.592650 W | rafthttp: lost the TCP streaming connection with peer 6d4f535bae3ab960 (stream MsgApp v2 reader)
14:13:31.592825 W | rafthttp: lost the TCP streaming connection with peer 6d4f535bae3ab960 (stream Message reader)
14:13:31.693275 E | rafthttp: failed to dial 6d4f535bae3ab960 on stream Message (dial tcp [::1]:2380: getsockopt: connection refused)
14:13:31.693289 I | rafthttp: peer 6d4f535bae3ab960 became inactive
14:13:31.936678 W | rafthttp: lost the TCP streaming connection with peer 6d4f535bae3ab960 (stream Message writer)

此时最好备份 etcd 数据,以便在出现问题时提供降级路径:

$ etcdctl snapshot save backup.db

3. 替换为 etcd v3.3 二进制文件并启动新的 etcd 进程

新的 v3.3 etcd 将向集群发布其信息:

14:14:25.363225 I | etcdserver: published {Name:s1 ClientURLs:[http://localhost:2379]} to cluster a9ededbffcb1b1f1

验证每个成员以及整个集群是否使用新的 v3.3 etcd 二进制文件变为健康状态:

$ ETCDCTL_API=3 /etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
localhost:22379 is healthy: successfully committed proposal: took = 5.540129ms
localhost:32379 is healthy: successfully committed proposal: took = 7.321771ms
localhost:2379 is healthy: successfully committed proposal: took = 10.629901ms

在集群全部升级完成之前,已升级的成员将记录如下警告。这是正常现象,当所有 etcd 集群成员都升级到 v3.3 后,警告将停止:

14:15:17.071804 W | etcdserver: member c89feb932daef420 has a higher version 3.3.0
14:15:21.073110 W | etcdserver: the local etcd version 3.2.7 is not up-to-date
14:15:21.073142 W | etcdserver: member 6d4f535bae3ab960 has a higher version 3.3.0
14:15:21.073157 W | etcdserver: the local etcd version 3.2.7 is not up-to-date
14:15:21.073164 W | etcdserver: member c89feb932daef420 has a higher version 3.3.0

4. 对所有其他成员重复步骤 2 到步骤 3

5. 完成

当所有成员都升级后,集群将报告成功升级到 3.3:

14:15:54.536901 N | etcdserver/membership: updated the cluster version from 3.2 to 3.3
14:15:54.537035 I | etcdserver/api: enabled capabilities for version 3.3
$ ETCDCTL_API=3 /etcdctl endpoint health --endpoints=localhost:2379,localhost:22379,localhost:32379
localhost:2379 is healthy: successfully committed proposal: took = 2.312897ms
localhost:22379 is healthy: successfully committed proposal: took = 2.553476ms
localhost:32379 is healthy: successfully committed proposal: took = 2.517902ms

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