还不了解 etcd?一文带你快速入门(万字长文)

2020-12-01 09:56:47 浏览数 (1)

2018年12月 etcd 作为孵化项目 CNCF(云原生计算基金会),几天前 CNCF 宣布 etcd 正式毕业,成为 CNCF 顶级项目。CNCF 官方表示 etcd 项目的采用率持续增加,也有稳定的治理流程,功能已达一定成熟度。

etcd 已经被许多公司用于实际生产,其中包括亚马逊、百度、思科、EMC、谷歌、阿里、华为、IBM、Red Hat、Uber、Verizon 等;而且其身影也出现在包括 Kubernetes、CoreDNS、M3、Rook 以及 TiKV 等项目当中。

前面的系列 etcd 文章已经部分介绍了 etcd。本文将会做一个快速入门的介绍,带领大家十分钟快速入门云原生存储组件 etcd,主要内容如下(本文较长,建议收藏):

  • etcd 简介,以及其应用场景
  • etcd 的多种安装模式及高可用运维
  • etcd v3 的架构解析
  • etcdctl 的实践应用

1 etcd 介绍

2013 年 6 月,CoreOS 发起了 etcd 项目。etcd 使用 Go 语言实现,是分布式系统中重要的基础组件,目前最新版本为 V3.4.9。etcd 可以用来构建高可用的分布式键值数据库,根据官网介绍,总结来说有如下的特点:

  • 简单:etcd 的安装简单,且为用户提供了 HTTP API,用户使用起来也很简单
  • 存储:etcd 的基本功能,数据分层存储在文件目录中,类似于我们日常使用的文件系统
  • Watch 机制:Watch 指定的键、前缀目录的更改,并对更改时间进行通知
  • 安全通信:SSL 证书验证
  • 高性能:etcd 单实例可以支持 2k/s 读操作,官方也有提供基准测试脚本
  • 一致可靠:基于 Raft 共识算法,实现分布式系统数据的高可用性、一致性

etcd 是一个分布式键值存储数据库,支持跨平台,拥有强大的社区。etcd 的 Raft 算法,提供了可靠的方式存储分布式集群涉及的数据。etcd 广泛应用在微服务架构和 Kubernates 集群中,不仅可以作为服务注册与发现,还可以作为键值对存储的中间件。从业务系统 Web 到 Kubernetes 集群,都可以很方便地从 etcd 中读取、写入数据。

2 etcd 应用场景

etcd 在稳定性、可靠性和可伸缩性表现极佳,同时也为云原生应用系统提供了协调机制。etcd 经常用于服务注册与发现的场景,此外还有键值对存储、消息发布与订阅、分布式锁等场景。

2.1 键值对存储

如下是官方对 etcd 的描述:

❝A highly-available key value store for shared configuration and service discovery. 一个用于配置共享和服务发现的键值存储系统。 ❞

从其定义来看,etcd 是一个「键值存储」的组件,存储是 etcd 最基本的功能,其他应用场景都是建立在 etcd 的可靠存储上。etcd 的存储有如下特点:

  • 采用键值对数据存储,读写性能一般高于关系型数据库;
  • etcd 集群分布式存储,多节点集群更加可靠;
  • etcd 的存储采用类似文件目录的结构:
    • 叶子节点存储数据,其他节点不存储,这些数据相当于文件;
    • 非叶节点一定是目录,这些节点不能存储数据。

比如 Kubernetes 将一些元数据存储在 etcd 中,将存储状态数据的的复杂工作交给 etcd,Kubernetes 自身的功能和架构能够更加专注。

2.2 服务注册与发现

分布式环境中,业务服务多实例部署,这个时候涉及到服务之间调用,就不能简单使用硬编码的方式指定服务实例信息。服务注册与发现就是解决如何找到分布式集群中的某一个服务(进程),并与之建立联系。

服务注册与发现涉及三个主要的角色:服务请求者、服务提供者和服务注册中心。

三大支柱

服务提供者启动的时候,在服务注册中心进行注册自己的服务名、主机地址、端口等信息;服务请求者需要调用对应的服务时,一般通过服务名请求服务注册中心,服务注册中心返回对应的实例地址和端口;服务请求者获取到实例地址、端口之后,绑定对应的服务提供者,实现远程调用。

etcd 基于 Raft 算法,能够有力地保证分布式场景中的一致性。各个服务启动时注册到 etcd 上,同时为这些服务配置键的 TTL 时间,定时保持服务的心跳以达到监控健康状态的效果。通过在 etcd 指定的主题下注册的服务也能在对应的主题下查找到。为了确保连接,我们可以在每个服务机器上都部署一个 Proxy 模式的 etcd,这样就可以确保访问 etcd 集群的服务都能够互相连接。

2.3 消息发布与订阅

在分布式系统中,服务之间还可以通过消息通信,即消息的发布与订阅。通过构建一个消息中间件,服务提供者发布对应主题的消息,而消费者则订阅他们关心的主题,一旦对应的主题有消息发布,即会产生订阅事件,消息中间件就会通知该主题所有的订阅者。

如微服务架构中的认证鉴权服务,Auth 服务的实例地址、端口和实例节点的状态存放在 etcd 中,客户端应用订阅对应的主题,而 etcd 设置 key TTL 可以确保存储的服务实例的健康状态。

2.4 分布式锁

分布式系统中涉及到多个服务实例,存在跨进程之间资源调用,对于资源的协调分配,单体架构中的锁已经无法满足需要,需要引入分布式锁的概念。分布式锁可以将资源标记存储,这里的存储不是单纯属于某个进程,而是公共存储,诸如 Redis、Memcache、关系型数据库、文件等。

etcd 基于 Raft 算法,实现分布式集群的一致性,存储到 etcd 集群中的值必然是全局一致的,因此基于 etcd 很容易实现分布式锁。分布式锁有两种使用方式:保持独占和控制时序。

保持独占,从字面可以知道,所有获取资源的请求,只有一个成功。etcd 通过分布式锁原子操作 CAS 的 API,设置 prevExist 值,从而保证在多个节点同时去创建某个目录时,最后只有一个成功,创建成功的请求获取到锁。

控制时序,有点类似于队列缓冲,所有的请求都会被安排分配资源,但是获得锁的顺序也是全局唯一的,执行按照先后的顺序。etcd 提供了一套自动创建有序键的 API,对一个目录的建值操作,这样 etcd 会自动生成一个当前最大的值为键,并存储该值。同时还可以使用 API 按顺序列出当前目录下的所有键值。

3 etcd 的多种安装方法

etcd 的安装非常简单,我们在本小节实践 etcd 单机和集群的几种安装方式。

3.1 etcd 概念词汇表

下文的安装实践可能会涉及到 etcd 相关的概念词汇,因此我们首先了解下 etcd 的概念词汇表,以便于下文的理解。

  • Raft:分布式一致性算法;
  • Node:Raft 状态机实例;
  • Member:管理着 Node 的 etcd 实例,为客户端请求提供服务;
  • Cluster:etcd 集群,由多个 Member 构成;
  • Peer:同一个 etcd 集群中的另一个 Member;
  • Client:客户端,向 etcd 发送 HTTP 请求;
  • WAL:持久化存储的日志格式,预写式日志;
  • Snapshot:etcd 数据快照,防止 WAL 文件过多而设置的快照。

在了解了 etcd 的常用术语之后,我们下面开始介绍 etcd v3 的架构。

etcd 的安装有多种方式,笔者以 Centos 7 和 MacOS 10.15 为例,可以通过 yum install etcdbrew install etcd进行安装。

然而通过系统工具安装的 etcd 版本比较滞后,如果需要安装最新版本的 etcd ,我们可以通过二进制包、源码编译以及 docker 容器安装。

3.2 etcd 单机安装部署

etcd 的安装有多种方式,笔者以 Centos 7 和 MacOS 10.15 为例,可以通过 yum install etcdbrew install etcd进行安装。

然而通过系统工具安装的 etcd 版本比较滞后,如果需要安装最新版本的 etcd ,我们可以通过二进制包、源码编译以及 docker 容器安装。

3.2.1 二进制安装

在 macOS 下,安装 3.4.5,指定 etcd 版本,执行如下的脚本:

代码语言:javascript复制
ETCD_VER=v3.4.5
GITHUB_URL=https://github.com/etcd-io/etcd/releases/download
DOWNLOAD_URL=${GITHUB_URL}

rm -f /tmp/etcd-${ETCD_VER}-darwin-amd64.zip
rm -rf /tmp/etcd-download-test && mkdir -p /tmp/etcd-download-test

curl -L ${DOWNLOAD_URL}/${ETCD_VER}/etcd-${ETCD_VER}-darwin-amd64.zip -o /tmp/etcd-${ETCD_VER}-darwin-amd64.zip
unzip /tmp/etcd-${ETCD_VER}-darwin-amd64.zip -d /tmp && rm -f /tmp/etcd-${ETCD_VER}-darwin-amd64.zip
mv /tmp/etcd-${ETCD_VER}-darwin-amd64/* /tmp/etcd-download-test && rm -rf mv /tmp/etcd-${ETCD_VER}-darwin-amd64

/tmp/etcd-download-test/etcd --version
/tmp/etcd-download-test/etcdctl version

执行完上面的脚本,控制台即可输出如下的结果:

代码语言:javascript复制
etcd Version: 3.4.5
Git SHA: c65a9e2dd
Go Version: go1.12.12
Go OS/Arch: darwin/amd64

根据控制台的显示,etcd 安装成功,版本为 etcd 3.4.5,基于的 Go 语言版本为 1.12.12

Linux 上的操作也类似,Centos 7 系统执行的脚本可以参考 etcd 官方提供的脚本。

我们在这一小节演示了单机 etcd 在 macOS 和 Linux 上的安装。etcd 在 Windows 系统的安装比较简单,下载可执行文件,其中 etcd.exe 是 etcd Server,etcdctl.exe 命令行工具。

3.2.2 源码安装

对于那些想尝试最新版本的同学,可以从 master 分支构建 etcd。使用源码安装,首先需要确保本地的 Go 语言环境。如未安装,请参考 https://golang.org/doc/install。需要 Go 版本为 1.13 ,来构建最新版本的 etcd。本地的 Go 版本为:

代码语言:javascript复制
$ go version

go version go1.14.2 darwin/amd64

基于 master 分支构建 etcd,脚本如下:

代码语言:javascript复制
git clone https://github.com/etcd-io/etcd.git
cd etcd
./build

安装完记得执行测试命令,确保 etcd 源码编译安装成功。

3.3 etcd 集群部署

etcd 是分布式环境中重要的中间件,一般在生产环境不会单节点部署 etcd,为了 etcd 的高可用,避免单点故障,etcd 通常都是集群部署。本小节将会介绍如何进行 etcd 集群部署。引导 etcd 集群的启动有以下三种方式:

  • 静态指定
  • etcd 动态发现
  • DNS 发现

静态指定的方式需要事先知道集群中的所有节点。在许多情况下,群集成员的信息是动态生成。这种情况下,可以在动态发现服务的帮助下启动 etcd 群集。

下面我们将会分别介绍这几种方式。

3.3.1 静态方式启动 etcd 集群

如果想要在一台机器上实践 etcd 集群的搭建,可以通过 goreman 工具。

goreman 是一个 Go 语言编写的多进程管理工具,是对 Ruby 下广泛使用的 foreman 的重写(foreman 原作者也实现了一个 Go 版本:forego,不过没有 goreman 好用)。

我们需要确认 Go 安装环境,然后直接执行:

代码语言:javascript复制
go get github.com/mattn/goreman

编译后的文件放在 GOPATH/bin 中,GOPATH/bin目录已经添加到了系统

HostName

ip

客户端交互端口

peer 通信端口

infra1

127.0.0.1

2379

2380

infra2

127.0.0.1

22379

22380

infra3

127.0.0.1

32379

32380

Procfile 脚本如下:

其他两个 etcd 成员的配置类似,不在赘述。配置项说明如下:

  • --name:etcd 集群中的节点名;
  • --listen-peer-urls:用于节点之间通信的地址,可以监听多个;
  • --initial-advertise-peer-urls:与其他节点之间通信的地址;
  • --listen-client-urls:监听客户端通信的地址,可以有多个;
  • --advertise-client-urls:用于客户端与节点通信的地址;
  • --initial-cluster-token:标识不同 etcd 集群的 token;
  • --initial-cluster:即指定的 initial-advertise-peer-urls 的所有节点;
  • --initial-cluster-state:new,新建集群的标志。

注意上面的脚本,etcd 命令执行时需要根据本地实际的安装地址进行配置。下面我们启动 etcd 集群。

代码语言:javascript复制
goreman -f /opt/procfile start

使用如上的命令启动启动 etcd 集群,启动完成之后查看集群内的成员。

代码语言:javascript复制
$ etcdctl --endpoints=http://localhost:22379  member list

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

我们在单机搭建的伪集群成功,需要注意的是在集群启动时,我们通过静态的方式指定集群的成员,在实际环境中,集群成员的地址端口等信息可能不会事先知道。这时候就需要采用动态发现的机制。

3.3.2 docker 启动集群

etcd 使用 gcr.io/etcd-development/etcd 作为容器的主要加速器, quay.io/coreos/etcd 作为辅助的加速器。可惜这两个加速器我们都没法访问,如果下载不了,可以使用笔者提供的地址:

代码语言:javascript复制
docker pull bitnami/etcd:3.4.7

然后将拉取的镜像重新 tag:

代码语言:javascript复制
docker image tag bitnami/etcd:3.4.7 quay.io/coreos/etcd:3.4.7

镜像设置好之后,我们启动 3 个节点的 etcd 集群,脚本命令如下:

代码语言:javascript复制
REGISTRY=quay.io/coreos/etcd

# For each machine
ETCD_VERSION=3.4.5
TOKEN=my-etcd-token
CLUSTER_STATE=new
NAME_1=etcd-node-0
NAME_2=etcd-node-1
NAME_3=etcd-node-2
HOST_1= 192.168.202.128
HOST_2= 192.168.202.129
HOST_3= 192.168.202.130
CLUSTER=${NAME_1}=http://${HOST_1}:2380,${NAME_2}=http://${HOST_2}:2380,${NAME_3}=http://${HOST_3}:2380
DATA_DIR=/var/lib/etcd

# For node 1
THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
docker run 
  -p 2379:2379 
  -p 2380:2380 
  --volume=${DATA_DIR}:/etcd-data 
  --name etcd ${REGISTRY}:${ETCD_VERSION} 
  /usr/local/bin/etcd 
  --data-dir=/etcd-data --name ${THIS_NAME} 
  --initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://0.0.0.0:2380 
  --advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://0.0.0.0:2379 
  --initial-cluster ${CLUSTER} 
  --initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}

# For node 2
THIS_NAME=${NAME_2}
THIS_IP=${HOST_2}
docker run 
  -p 2379:2379 
  -p 2380:2380 
  --volume=${DATA_DIR}:/etcd-data 
  --name etcd ${REGISTRY}:${ETCD_VERSION} 
  /usr/local/bin/etcd 
  --data-dir=/etcd-data --name ${THIS_NAME} 
  --initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://0.0.0.0:2380 
  --advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://0.0.0.0:2379 
  --initial-cluster ${CLUSTER} 
  --initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}

# For node 3
THIS_NAME=${NAME_3}
THIS_IP=${HOST_3}
docker run 
  -p 2379:2379 
  -p 2380:2380 
  --volume=${DATA_DIR}:/etcd-data 
  --name etcd ${REGISTRY}:${ETCD_VERSION} 
  /usr/local/bin/etcd 
  --data-dir=/etcd-data --name ${THIS_NAME} 
  --initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://0.0.0.0:2380 
  --advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://0.0.0.0:2379 
  --initial-cluster ${CLUSTER} 
  --initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}

注意,上面的脚本是部署在三台机器上面,每台机器执行对应的脚本即可。在运行时可以指定 API 版本:

代码语言:javascript复制
docker exec etcd /bin/sh -c "export ETCDCTL_API=3 && /usr/local/bin/etcdctl put foo bar"

docker 的安装方式比较简单,读者根据需要可以定制一些配置。

3.4 动态发现启动 etcd 集群

在大部分的情况下,集群成员的地址端口号等信息,并不会提前知道。在这种情况下,需要使用自动发现来引导 etcd 集群,而不是指定静态配置,这个过程被称为「发现」。我们启动三个 etcd,具体对应如下:

HostName

ip

客户端交互端口

peer 通信端口

etcd1

192.168.202.128

2379

2380

etcd2

192.168.202.129

2379

2380

etcd3

192.168.202.130

2379

2380

3.4.1 协议的原理

基于 Discovery Service Protocol ,新的 etcd 节点加入时,使用共享 URL 在启动阶段发现集群中的其它成员。需要注意的是,发现协议仅在集群启动阶段生效,不能用于其它阶段。

这其中涉及到发现令牌,用来标识唯一的 etcd 集群。一个发现令牌只能代表一个 etcd 集群。只要此令牌上的发现协议启动,即使启动失败,也不能用于引导其他的 etcd 集群。

所有 etcd 节点都与发现服务通信,并生成集群成员列表。随后每个新节点使用此列表启动服务,该列表与 --initial-cluster 选项具有相同的功能,用以设置集群的成员信息。

3.4.2 获取 discovery 的 token

生成将标识新集群的唯一令牌。在以下步骤中,它将用作发现键空间中的唯一前缀。一种简单的方法是使用 uuidgen,我们运行之后的结果为:

代码语言:javascript复制
$ uuidgen

9CBAF7C8-123B-4E72-89C9-F81E4BC5F173
3.4.3 指定集群的大小

获取令牌时,必须指定群集大小。发现服务使用该大小来了解何时发现了最初将组成集群的所有成员。

代码语言:javascript复制
curl -X PUT http://10.0.10.10:2379/v2/keys/discovery/9CBAF7C8-123B-4E72-89C9-F81E4BC5F173/_config/size -d value=3

我们需要把该 url 地址 http://10.0.10.10:2379/v2/keys/discovery/9CBAF7C8-123B-4E72-89C9-F81E4BC5F173 作为 --discovery 参数来启动 etcd。

节点会自动使用 http://10.0.10.10:2379/v2/keys/discovery/9CBAF7C8-123B-4E72-89C9-F81E4BC5F173 目录进行 etcd 的注册和发现服务。

3.4.4 公共发现服务

如果我们没有可用的本地 etcd 集群,则可以使用 etcd 官方提供的公共访问的 etcd 集群地址。

公共发现服务 discovery.etcd.io 以相同的方式工作,但是有一层修饰,可以提取丑陋的 URL,自动生成 UUID,并提供针对过多请求的保护。公共发现服务在其上仍然使用 etcd 群集作为数据存储。

代码语言:javascript复制
$ curl http://discovery.etcd.io/new?size=3

http://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de
3.4.5 以动态发现方式启动集群

etcd 发现模式下,启动 etcd 的命令如下:

代码语言:javascript复制
# etcd1 启动
$ /opt/etcd/bin/etcd  --name etcd1 --initial-advertise-peer-urls http://192.168.202.128:2380 
  --listen-peer-urls http://192.168.202.128:2380 
  --data-dir /opt/etcd/data 
  --listen-client-urls http://192.168.202.128:2379,http://127.0.0.1:2379 
  --advertise-client-urls http://192.168.202.128:2379 
  --discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de

# etcd2 启动
 /opt/etcd/bin/etcd  --name etcd2 --initial-advertise-peer-urls http://192.168.202.129:2380 
  --listen-peer-urls http://192.168.202.129:2380 
  --data-dir /opt/etcd/data 
  --listen-client-urls http://192.168.202.129:2379,http://127.0.0.1:2379 
  --advertise-client-urls http://192.168.202.129:2379 
  --discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de

# etcd3 启动
 /opt/etcd/bin/etcd  --name etcd3 --initial-advertise-peer-urls http://192.168.202.130:2380 
    --listen-peer-urls http://192.168.202.130:2380 
    --data-dir /opt/etcd/data 
    --listen-client-urls http://192.168.202.130:2379,http://127.0.0.1:2379 
    --advertise-client-urls http://192.168.202.130:2379 
    --discovery https://discovery.etcd.io/3e86b59982e49066c5d813af1c2e2579cbf573de

我们在每次启动新 etcd 集群时,使用新的 discovery token 进行注册。如果初始化启动的节点超过了请求 token 时指定的数量,则多的节点会自动转为 Proxy 模式的 etcd。需要注意的是,在我们完成了集群的初始化后,这些信息就失去了作用。当需要增加节点时,需要使用 etcdctl 进行操作。

3.4.6 结果验证

集群启动好之后,进行验证,我们看一下集群的成员:

代码语言:javascript复制
$ /opt/etcd/bin/etcdctl member list
# 结果如下
    40e2ac06ca1674a7, started, etcd3, http://192.168.202.130:2380, http://192.168.202.130:2379, false
    c532c5cedfe84d3c, started, etcd1, http://192.168.202.128:2380, http://192.168.202.128:2379, false
    db75d3022049742a, started, etcd2, http://192.168.202.129:2380, http://192.168.202.129:2379, false

可以看到,集群中的三个节点都是健康的正常状态。以动态发现方式启动集群成功。

4 etcd v3 的架构解析

etcd v2 和 v3 在底层使用同一套 Raft 算法的两个独立应用,相互之间接口不一样、存储不一样,两个版本的数据互相隔离。由 etcd v2 升级到 etcd v3 的情况下,原有数据只能通过 etcd v2 接口访问,v3 接口创建的数据只能通过 新的 v3 的接口访问。本文重点讲解 v3 版本。etcd 总体的架构图如下所示:

分布式集群一般由奇数个节点组成,etcd 同样也是,多个 etcd 节点通过 Raft 算法相互协作。Raft 算法会选择一个主节点作为 Leader,负责数据同步和分发。当 Leader 发生故障时,系统会自动选择另一个节点作为 Leader,以再次完成数据同步。当 etcd 完成内部状态和数据协作时,仅需选择一个节点即可读取和写入数据。

Quorum(翻译成法定人数,简单理解为定义一个最少写入同步到多少个节点才算成功写入)机制是 etcd 中的关键概念。它定义为 (n 1)/2,表示群集中超过一半的节点构成 Quorum。在三节点集群中,只要有两个节点可用,etcd 仍将运行。同样,在五节点集群中,只要有三个节点可用,etcd 仍会运行。这是 etcd 群集高可用性的关键。

为了使 etcd 在某些节点出现故障后继续运行,必须解决分布式一致性问题。在 etcd 中,分布式共识算法由 Raft 实现。下面简要描述该算法。Raft 共识算法仅在任何两个 Quorum 具有共享成员时才可以工作。也就是说,任何有效的仲裁必须包含一个共享成员,该成员包含集群中所有已确认和已提交的数据。基于此,etcd 为 Raft 共识算法设计了一种数据同步机制,用于在更新 Leader 后同步最后 Quorum 所提交的所有数据。这样可以确保在群集状态更改时数据的一致性。

etcd 具有复杂的内部机制,但为客户端提供了简单直接的 API。如上图所示,我们可以通过 etcd 客户端访问群集数据,或通过 HTTP 直接访问 etcd,这与 curl 命令类似。etcd 中的数据结构很简单,我们可以将 etcd 的数据存储理解为存储键值数据的有序映射。etcd 还提供了一种 Watch 机制来为客户端订阅数据更改的事件。Watch 机制实时获取 etcd 中的增量数据更新,以使数据与 etcd 同步。

5 etcdctl 的实践应用

etcdctl 是 etcd 的命令行客户端,用户通过 etcdctl 直接跟 etcd 进行交互,可以实现 HTTP API 的功能。在测试阶段,etcdctl 可以方便对服务进行操作或更新数据库内容。对于刚入门的同学,建议通过 etdctl 来熟悉 etcd 相关操作。需要注意的是,etcdctl 在两个不同的 etcd 版本下的行为方式也完全不同。

代码语言:javascript复制
export ETCDCTL_API=2
export ETCDCTL_API=3

这里主要以讲解 API 3 为主。etcdctl 的命令分为数据库和非数据库两类操作。

5.1 常用命令介绍

首先查看一下 etcd 的版本:

代码语言:javascript复制
$ etcd --version

etcd Version: 3.4.7
Git SHA: e784ba73c
Go Version: go1.12.12
Go OS/Arch: linux/amd64

我们的版本是 etcd 3.4.7 ,下面介绍下 etcdctl 常用的命令。

代码语言:javascript复制
$ etcdctl -h

NAME:
 etcdctl - A simple command line client for etcd3.

USAGE:
 etcdctl [flags]

VERSION:
 3.4.7

API VERSION:
 3.4


COMMANDS:
 alarm disarm  Disarms all alarms
 alarm list  Lists all alarms
 auth disable  Disables authentication
 auth enable  Enables authentication
 check datascale  Check the memory usage of holding data for different workloads on a given server endpoint.
 check perf  Check the performance of the etcd cluster
 compaction  Compacts the event history in etcd
 defrag   Defragments the storage of the etcd members with given endpoints
 del   Removes the specified key or range of keys [key, range_end)
 elect   Observes and participates in leader election
 endpoint hashkv  Prints the KV history hash for each endpoint in --endpoints
 endpoint health  Checks the healthiness of endpoints specified in `--endpoints` flag
 endpoint status  Prints out the status of endpoints specified in `--endpoints` flag
 get   Gets the key or a range of keys
 help   Help about any command
 lease grant  Creates leases
 lease keep-alive Keeps leases alive (renew)
 lease list  List all active leases
 lease revoke  Revokes leases
 lease timetolive Get lease information
 lock   Acquires a named lock
 make-mirror  Makes a mirror at the destination etcd cluster
 member add  Adds a member into the cluster
 member list  Lists all members in the cluster
 member promote  Promotes a non-voting member in the cluster
 member remove  Removes a member from the cluster
 member update  Updates a member in the cluster
 migrate   Migrates keys in a v2 store to a mvcc store
 move-leader  Transfers leadership to another etcd cluster member.
 put   Puts the given key into the store
 role add  Adds a new role
 role delete  Deletes a role
 role get  Gets detailed information of a role
 role grant-permission Grants a key to a role
 role list  Lists all roles
 role revoke-permission Revokes a key from a role
 snapshot restore Restores an etcd member snapshot to an etcd directory
 snapshot save  Stores an etcd node backend snapshot to a given file
 snapshot status  Gets backend snapshot status of a given file
 txn   Txn processes all the requests in one transaction
 user add  Adds a new user
 user delete  Deletes a user
 user get  Gets detailed information of a user
 user grant-role  Grants a role to a user
 user list  Lists all users
 user passwd  Changes password of user
 user revoke-role Revokes a role from a user
 version   Prints the version of etcdctl
 watch   Watches events stream on keys or prefixes

OPTIONS:
      --cacert=""    verify certificates of TLS-enabled secure servers using this CA bundle
      --cert=""     identify secure client using this TLS certificate file
      --command-timeout=5s   timeout for short running command (excluding dial timeout)
      --debug[=false]    enable client-side debug logging
      --dial-timeout=2s    dial timeout for client connections
  -d, --discovery-srv=""   domain name to query for SRV records describing cluster endpoints
      --discovery-srv-name=""   service name to query when using DNS discovery
      --endpoints=[127.0.0.1:2379]  gRPC endpoints
  -h, --help[=false]    help for etcdctl
      --hex[=false]    print byte strings as hex encoded strings
      --insecure-discovery[=true]  accept insecure SRV records describing cluster endpoints
      --insecure-skip-tls-verify[=false] skip server certificate verification
      --insecure-transport[=true]  disable transport security for client connections
      --keepalive-time=2s   keepalive time for client connections
      --keepalive-timeout=6s   keepalive timeout for client connections
      --key=""     identify secure client using this TLS key file
      --password=""    password for authentication (if this option is used, --user option shouldn't include password)
      --user=""     username[:password] for authentication (prompt if password is not supplied)
  -w, --write-out="simple"   set the output format (fields, json, protobuf, simple, table)

可以看到,etcdctl 支持的命令很多,常用的命令选项:

代码语言:javascript复制
--debug 输出 CURL 命令,显示执行命令的时候发起的请求
--no-sync 发出请求之前不同步集群信息
--output, -o 'simple' 输出内容的格式(simple 为原始信息,json 为进行 json 格式解码,易读性好一些)
--peers, -C 指定集群中的同伴信息,用逗号隔开(默认为: "127.0.0.1:4001")
--cert-file HTTPS 下客户端使用的 SSL 证书文件
--key-file HTTPS 下客户端使用的 SSL 密钥文件
--ca-file 服务端使用 HTTPS 时,使用 CA 文件进行验证
--help, -h 显示帮助命令信息
--version, -v 打印版本信息

下面我们将介绍其中常用的数据库命令。

5.2 数据库操作

etcd 在键的组织上采用了如同类似文件目录的结构,即层次化的空间结构,我们可以为键指定单独的名字。etcd 数据库提供的操作,则主要围绕对键值和目录的增删改查。

5.2.1 键操作

set 指定某个键的值。例如:

代码语言:javascript复制
$ etcdctl put /testdir/testkey "Hello world"
$ etcdctl put /testdir/testkey2 "Hello world2"
$ etcdctl put /testdir/testkey3 "Hello world3"

成功写入三对键值,/testdir/testkey、/testdir/testkey2 和 /testdir/testkey3。

get 获取指定键的值。例如:

代码语言:javascript复制
$ etcdctl get /testdir/testkey
Hello world

get 十六进制读指定的值:

代码语言:javascript复制
$ etcdctl get /testdir/testkey --hex
x2fx74x65x73x74x64x69x72x2fx74x65x73x74x6bx65x79 #键
x48x65x6cx6cx6fx20x77x6fx72x6cx64 #值

加上 --print-value-only 可以读取对应的值。

get 范围内的值

代码语言:javascript复制
 $ etcdctl get /testdir/testkey /testdir/testkey3

/testdir/testkey
Hello world
/testdir/testkey2
Hello world2

可以看到,获取了大于等于 /testdir/testkey,且小于 /testdir/testkey3 的键值对。testkey3 不在范围之内,因为范围是半开区间 [testkey, testkey3), 不包含 testkey3。

获取某个前缀的所有键值对,通过 --prefix 可以指定前缀:

代码语言:javascript复制
$ etcdctl get --prefix /testdir/testkey
/testdir/testkey
Hello world
/testdir/testkey2
Hello world2
/testdir/testkey3
Hello world3

这样既可获取所有以 /testdir/testkey 开头的键值对。当前缀获取的结果过多时,还可以通过 --limit=2 限制获取的数量:

代码语言:javascript复制
etcdctl get --prefix --limit=2 /testdir/testkey

读取键过往版本的值 应用可能想读取键的被替代的值。例如,应用可能想通过访问键的过往版本来回滚到旧的配置。或者,应用可能想通过多个请求来得到一个覆盖多个键的统一视图,而这些请求可以通过访问键历史记录而来。因为 etcd 集群上键值存储的每个修改都会增加 etcd 集群的全局修订版本,应用可以通过提供旧有的 etcd 修改版本来读取被替代的键。现有如下这些键值对:

代码语言:javascript复制
foo = bar         # revision = 2
foo1 = bar2       # revision = 3
foo = bar_new     # revision = 4
foo1 = bar1_new   # revision = 5

以下是访问以前版本 key 的示例:

代码语言:javascript复制
$ etcdctl get --prefix foo # 访问最新版本的 key
foo
bar_new
foo1
bar1_new

$ etcdctl get --prefix --rev=4 foo # 访问第 4 个版本的 key
foo
bar_new
foo1
bar1

$ etcdctl get --prefix --rev=3 foo #  访问第 3 个版本的 key
foo
bar
foo1
bar1

$ etcdctl get --prefix --rev=2 foo #  访问第 3 个版本的 key
foo
bar

$ etcdctl get --prefix --rev=1 foo #  访问第 1 个版本的 key

读取大于等于指定键的 byte 值的键 应用可能想读取大于等于指定键 的 byte 值的键。假设 etcd 集群已经有下列键:

代码语言:javascript复制
a = 123
b = 456
z = 789

读取大于等于键 b 的 byte 值的键的命令:

代码语言:javascript复制
$ etcdctl get --from-key b
b
456
z
789

删除键。客户端应用可以从 etcd 数据库中删除指定的键。假设 etcd 集群已经有下列键:

代码语言:javascript复制
foo = bar
foo1 = bar1
foo3 = bar3
zoo = val
zoo1 = val1
zoo2 = val2
a = 123
b = 456
z = 789

删除键 foo 的命令:

代码语言:javascript复制
$ etcdctl del foo
1 # 删除了一个键

删除从 foo to foo9 范围的键的命令:

代码语言:javascript复制
$ etcdctl del foo foo9
2 # 删除了两个键

删除键 zoo 并返回被删除的键值对的命令:

代码语言:javascript复制
$ etcdctl del --prev-kv zoo
1   # 一个键被删除
zoo # 被删除的键
val # 被删除的键的值

删除前缀为 zoo 的键的命令:

代码语言:javascript复制
$ etcdctl del --prefix zoo
2 # 删除了两个键

删除大于等于键 b 的 byte 值的键的命令:

代码语言:javascript复制
$ etcdctl del --from-key b
2 # 删除了两个键
5.2.2 watch 历史改动

watch 可以用来监测一个键值的变化,当该键值更新,控制台就会输出最新的值。例如:用户更新 watchkey 键值为 newwatchvalue。

代码语言:javascript复制
$ etcdctl watch  watchkey
# 在另外一个终端: etcdctl put  watchkey newwatchvalue
watchkey
newwatchvalue

从 foo to foo9 范围内键的命令:

代码语言:javascript复制
$ etcdctl watch foo foo9
# 在另外一个终端: etcdctl put foo bar
PUT
foo
bar
# 在另外一个终端: etcdctl put foo1 bar1
PUT
foo1
bar1

以 16 进制格式在键 foo 上进行观察的命令:

代码语言:javascript复制
$ etcdctl watch foo --hex
# 在另外一个终端: etcdctl put foo bar
PUT
x66x6fx6f          # 键
x62x61x72          # 值

观察多个键 foo 和 zoo 的命令:

代码语言:javascript复制
$ etcdctl watch -i
$ watch foo
$ watch zoo
# 在另外一个终端: etcdctl put foo bar
PUT
foo
bar
# 在另外一个终端: etcdctl put zoo val
PUT
zoo
val

查看 key 的历史修订版本。客户端应用需要获取某个键的所有修改。那么客户端应用连接到 etcd,watch 对应的 key 即可。如果 Watch 的过程中,etcd 或者客户端应用出错,又恰好发生了改动,这种情况下客户端应用可以在 Watch 时指定历史修订版本。 假设我们完成了下列操作序列:

代码语言:javascript复制
$ etcdctl put foo bar         # revision = 2
OK
$ etcdctl put foo1 bar1       # revision = 3
OK
$ etcdctl put foo bar_new     # revision = 4
OK
$ etcdctl put foo1 bar1_new   # revision = 5
OK

观察历史改动:

代码语言:javascript复制
# 从修订版本 2 开始观察键 `foo` 的改动
$ etcdctl watch --rev=2 foo
PUT
foo
bar
PUT
foo
bar_new

从上一次历史修改开始观察:

代码语言:javascript复制
# 在键 `foo` 上观察变更并返回被修改的值和上个修订版本的值
$ etcdctl watch --prev-kv foo
# 在另外一个终端: etcdctl put foo bar_latest
PUT
foo         # 键
bar_new     # 在修改前键 foo 的上一个值
foo         # 键
bar_latest  # 修改后键 foo 的值

压缩修订版本。etcd 保存了历史修订版本,客户端应用可以读取键的历史版本。大量的历史版本数据,会占据很多存储,因此需要压缩历史修订版本。经过压缩,etcd 会删除历史修订版本,释放出资源。压缩修订版本之前的版本数据不可访问。压缩修订版本的命令如下所示:

代码语言:javascript复制
$ etcdctl compact 5
compacted revision 5  $ etcdctl get --rev=4 foo
{"level":"warn","ts":"2020-05-04T16:37:38.020 0800","caller":"clientv3/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"endpoint://client-c0d35565-0584-4c07-bfeb-034773278656/127.0.0.1:2379","attempt":0,"error":"rpc error: code = OutOfRange desc = etcdserver: mvcc: required revision has been compacted"}
Error: etcdserver: mvcc: required revision has been compacted
5.2.3 租约

授予租约 客户端应用可以为 etcd 数据库存储内的键授予租约。当 etcd 中的键被授予租约时,该键的存活时间与租约的时间绑定,而租约的存活时间相应的被 time-to-live (TTL)管理。在租约授予时每个租约的最小 TTL 值由客户端应用指定。当租约的 TTL 到期,即代表租约就过期,此时该租约绑定的键都将被删除。

代码语言:javascript复制
# 授予租约,TTL 为 100 秒
$ etcdctl lease grant 100
lease 694d71ddacfda227 granted with TTL(10s)

# 附加键 foo 到租约 694d71ddacfda227
$ etcdctl put --lease=694d71ddacfda227 foo10 bar
OK

建议时间设置久一点,否则来不及操作会出现如下的错误:

代码语言:javascript复制

撤销租约 应用通过租约 id 可以撤销租约。撤销租约将删除所有它附带的 key。假设我们完成了下列的操作:

代码语言:javascript复制
$ etcdctl lease revoke 694d71ddacfda227
lease 694d71ddacfda227 revoked

$ etcdctl get foo10

刷新租期 应用程序可以通过刷新其 TTL 来保持租约活着,因此不会过期。

代码语言:javascript复制
$ etcdctl lease keep-alive 694d71ddacfda227
lease 694d71ddacfda227 keepalived with TTL(100)
lease 694d71ddacfda227 keepalived with TTL(100)
...

查询租期 应用程序可能想要了解租赁信息,以便它们可以续订或检查租赁是否仍然存在或已过期。应用程序也可能想知道特定租约所附的 key。

假设我们完成了以下一系列操作:

代码语言:javascript复制
$ etcdctl lease grant 300
lease 694d71ddacfda22c granted with TTL(300s)

$ etcdctl put --lease=694d71ddacfda22c foo10 bar
OK

获取有关租赁信息以及哪些 key 使用了租赁信息:

代码语言:javascript复制
$ etcdctl lease timetolive 694d71ddacfda22c
lease 694d71ddacfda22c granted with TTL(300s), remaining(282s)

$ etcdctl lease timetolive --keys 694d71ddacfda22c
lease 694d71ddacfda22c granted with TTL(300s), remaining(220s), attached keys([foo10])

6 etcd 安全运维

etcd 支持通过 TLS 协议进行的加密通信。TLS 通道可用于对等体之间的加密内部群集通信以及加密的客户端流量。

6.1 TLS 与 SSL

互联网信息明文传播,带来了窃听风险,即第三方可以窃取通信内容;篡改风险,通信内容被篡改;冒充风险,身份被冒充,从而参与通信。

通过 SSL/TLS 协议实现互联网的通信安全。SSL/TLS 协议可以解决上述提到的三个问题,通信加密,使得第三方无法窃听;校验机制防止篡改,一旦被篡改,通信双方会立刻发现;安全身份证书防止身份被冒充。

TLS 是安全传输层协议,如果需要实现 HTTPS 加密访问,保障通信数据的安全,就需要 SSL 证书,下面我们开始实践如何实现 etcd 集群成员之间的通信。

6.2 进行 TLS 加密实践

为了进行实践,我们将会安装一些实用的命令行工具,这包括 cfssl、cfssljson。

CFSSL 是 CloudFlare 的 PKI/TLS 工具,既是一个命令行工具,同时又可以用于签名,作为 HTTP API 服务器,验证和绑定 TLS 证书。它需要 Go 1.12 才能构建。环境配置如下:

HostName

ip

客户端交互端口

peer 通信端口

etcd1

192.168.202.128

2379

2380

etcd2

192.168.202.129

2379

2380

etcd3

192.168.202.130

2379

2380

6.2.1 安装 cfssl
代码语言:javascript复制
$ ls ~/Downloads/cfssl
cfssl-certinfo_1.4.1_linux_amd64 cfssl_1.4.1_linux_amd64          cfssljson_1.4.1_linux_amd64
代码语言:javascript复制
chmod  x cfssl_1.4.1_linux_amd64 cfssljson_1.4.1_linux_amd64 cfssl-certinfo_1.4.1_linux_amd64

mv cfssl_1.4.1_linux_amd64 /usr/local/bin/cfssl
mv cfssljson_1.4.1_linux_amd64 /usr/local/bin/cfssljson
mv cfssl-certinfo_1.4.1_linux_amd64 /usr/bin/cfssl-certinfo

安装完成之后,查看版本信息的结果:

代码语言:javascript复制
$ cfssl version

Version: 1.4.1
Runtime: go1.12.12
6.2.2 配置 CA 并创建 TLS 证书

我们将使用 CloudFlare's PKI 工具 cfssl 来配置 PKI Infrastructure,然后使用它去创建 Certificate Authority(CA), 并为 etcd 创建 TLS 证书。

首先创建 ssl 配置目录:

代码语言:javascript复制
mkdir /data/etcd/{bin,cfg,ssl} -p
cd /data/etcd/ssl/

etcd ca 配置:

代码语言:javascript复制
cat << EOF | tee ca-config.json
{
  "signing": {
    "default": {
      "expiry": "87600h"
    },
    "profiles": {
      "etcd": {
         "expiry": "87600h",
         "usages": [
            "signing",
            "key encipherment",
            "server auth",
            "client auth"
        ]
      }
    }
  }
}
EOF

etcd ca 证书:

代码语言:javascript复制
cat << EOF | tee ca-csr.json
{
    "CN": "etcd CA",
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [
        {
            "C": "CN",
            "L": "Shanghai",
            "ST": "Shanghai"
        }
    ]
}
EOF

生成 CA 凭证和私钥:

代码语言:javascript复制
$ cfssl gencert -initca ca-csr.json | cfssljson -bare ca

2020/04/30 20:36:58 [INFO] generating a new CA key and certificate from CSR
2020/04/30 20:36:58 [INFO] generate received request
2020/04/30 20:36:58 [INFO] received CSR
2020/04/30 20:36:58 [INFO] generating key: rsa-2048
2020/04/30 20:36:58 [INFO] encoded CSR
2020/04/30 20:36:58 [INFO] signed certificate with serial number 252821789025044258332210471232130931231440888312

$ ls

ca-config.json  ca-csr.json  ca-key.pem  ca.csr  ca.pem

etcd server 证书:

代码语言:javascript复制
cat << EOF | tee server-csr.json
{
    "CN": "etcd",
    "hosts": [
    "192.168.202.128",
    "192.168.202.129",
    "192.168.202.130"
    ],
    "key": {
        "algo": "rsa",
        "size": 2048
    },
    "names": [
        {
            "C": "CN",
            "L": "Beijing",
            "ST": "Beijing"
        }
    ]
}
EOF

生成 server 证书:

代码语言:javascript复制
cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=etcd server-csr.json | cfssljson -bare server
2020/04/30 20:44:37 [INFO] generate received request
2020/04/30 20:44:37 [INFO] received CSR
2020/04/30 20:44:37 [INFO] generating key: rsa-2048
2020/04/30 20:44:37 [INFO] encoded CSR
2020/04/30 20:44:37 [INFO] signed certificate with serial number 73061688633166283265484923779818839258466531108

ls
ca-config.json  ca-csr.json  ca-key.pem  ca.csr  ca.pem  server-csr.json  server-key.pem  server.csr  server.pem

启动 etcd 集群,命令的配置如下:

代码语言:javascript复制
#etcd1 启动

$ /data/etcd/bin/etcd --name etcd1 --initial-advertise-peer-urls https://192.168.202.128:2380 
     --listen-peer-urls https://192.168.202.128:2380 
     --listen-client-urls https://192.168.202.128:2379,https://127.0.0.1:2379 
     --advertise-client-urls https://192.168.202.128:2379 
     --initial-cluster-token etcd-cluster-1 
     --initial-cluster etcd1=https://192.168.202.128:2380, etcd2=https://192.168.202.129:2380, etcd3=https://192.168.202.130:2380 
     --initial-cluster-state new 
     --client-cert-auth --trusted-ca-file=/data/etcd/ssl/ca.pem 
     --cert-file=/opt/etcd/ssl/server.pem --key-file=/data/etcd/ssl/server-key.pem 
     --peer-client-cert-auth --peer-trusted-ca-file=/data/etcd/ssl/ca.pem 
     --peer-cert-file=/opt/etcd/ssl/server.pem --peer-key-file= /data/etcd/ssl/server-key.pem

#etcd2、etcd3 类似,此处省略

通过三台服务器的控制台可以知道,集群已经成功建立,我们进行验证:

查看三个节点的健康状况,endpoint health ;其次,查看集群的成员列表,是否输出三个成员;最后,经过 TLS 加密的 etcd 集群,在进行操作时,需要加上认证相关的信息,尝试先写再读的操作是否能够正常。

按照上述的步骤进行操作,这个验证的实践交给读者自行尝试。

6.2.3 自动证书

如果集群需要加密的通信但不需要经过身份验证的连接,则可以将 etcd 配置为自动生成其密钥。在初始化时,每个成员都基于其通告的 IP 地址和主机创建自己的密钥集。

在每台机器上,etcd 将使用以下标志启动:

代码语言:javascript复制
$ etcd --name etcd1 --initial-advertise-peer-urls https://192.168.202.128:2380 
  --listen-peer-urls https://192.168.202.128:2380 
  --listen-client-urls https://192.168.202.128:2379,https://127.0.0.1:2379 
  --advertise-client-urls https://10.0.1.10:2379 
  --initial-cluster-token etcd-cluster-1 
  --initial-cluster infra0=https://192.168.202.128:2380,infra1=https://192.168.202.129:2380,infra2=https://192.168.202.130:2380 
  --initial-cluster-state new 
  --auto-tls 
  --peer-auto-tls

由于自动签发证书并不认证身份,因此直接 curl 会返回错误。需要使用 curl 的 -k 命令屏蔽对证书链的校验。

7 小结

etcd 具有极佳的稳定性、可靠性、可伸缩性,为云原生分布式系统提供了必要的协调机制。

本文主要介绍了 etcd 的入门知识以及相关的实践。etcd 被设计为大型分布式系统的存储基石,etcd 以一致且容错的方式存储元数据。etcd 集群旨在提供具有稳定性、可靠性、可伸缩性和性能的键值存储。使用 etcd 的常见分布式场景包括领导者选举、键值对存储、分布式锁和消息订阅与发布等。

实践是最好的学习方式,在看完本文之后,希望大家能够参照本文的介绍,进行相应的实践,期待你的讨论交流。

0 人点赞