etcd 官网地址 https://etcd.io/ 最新版本3.5.7
etcd 官网文档地址 https://etcd.io/docs/v3.5/
etcd 源码地址 https://github.com/etcd-io/etcd
etcd是一个强一致、可靠的分布式键值存储,使用Go语言开发(docker和k8s也是),其提供可靠的分布式键值(key-value)存储、配置共享和服务发现等功能,即使在集群脑裂网络分区情况下也可以优雅地处理leader选举;官方上有明确说明etcd是一个CNCF项目。可以说,etcd 已经成为了云原生和分布式系统的存储基石。
分布式系统中的数据分为控制数据和应用数据。etcd的使用场景默认处理的数据都是控制数据,对于应用数据,只推荐数据量很小,但是更新访问频繁的情况。应用场景有如下几类
如果需要一个分布式存储仓库来存储配置信息,并且希望这个仓库读写速度快、支持高可用、部署简单、支持http接口,那么就可以使用云原生项目etcd。
etcd实现的绝大多数功能Zookeeper都能实现,那为何还要用etcd?相较之下,Zookeeper有如下缺点:
而etcd作为一个后起之秀,对比Zookeeper其优点如下
etcd作为一个年轻的项目,正在高速迭代和开发中,这既优点也是缺点。优点在于它的未来具有无限的可能性,缺点是版本的迭代导致其使用的可靠性无法保证,无法得到大项目长时间使用的检验。但由于CoreOS、Kubernetes和Cloudfoundry等知名项目均在生产环境中使用了etcd,所以总的来说etcd值得去尝试。
etcd按照分层模型可分为 Client 层、API 网络层、Raft 算法层、逻辑层和存储层。各层功能如下:
Client 层:Client 层包括 client v2 和 v3 两个大版本 API 客户端库,提供了简洁易用的 API,同时支持负载均衡、节点间故障自动转移,可极大降低业务使用 etcd 复杂度,提升开发效率、服务可用性。
API 网络层:API 网络层主要包括 client 访问 server 和 server 节点之间的通信协议。一方面,client 访问 etcd server 的 API 分为 v2 和 v3 两个大版本。v2 API 使用 HTTP/1.x 协议,v3 API 使用 gRPC 协议。同时 v3 通过 etcd grpc-gateway 组件也支持 HTTP/1.x 协议,便于各种语言的服务调用。另一方面,server 之间通信协议,是指节点间通过 Raft 算法实现数据复制和 Leader 选举等功能时使用的 HTTP 协议。etcdv3版本中client 和 server 之间的通信,使用的是基于 HTTP/2 的 gRPC 协议。相比 etcd v2 的 HTTP/1.x,HTTP/2 是基于二进制而不是文本、支持多路复用而不再有序且阻塞、支持数据压缩以减少包大小、支持 server push 等特性。因此,基于 HTTP/2 的 gRPC 协议具有低延迟、高性能的特点,有效解决etcd v2 中 HTTP/1.x 性能问题。
Raft 算法层:Raft 算法层实现了 Leader 选举、日志复制、ReadIndex 等核心算法特性,用于保障 etcd 多个节点间的数据一致性、提升服务可用性等,是 etcd 的基石和亮点。
功能逻辑层:etcd 核心特性实现层,如典型的 KVServer 模块、MVCC 模块、Auth 鉴权模块、Lease 租约模块、Compactor 压缩模块等,其中 MVCC 模块主要由 treeIndex(内存树形索引) 模块和 boltdb(嵌入式的 KV 持久化存储库) 模块组成。treeIndex 模块使用B-tree 数据结构来保存用户 key 和版本号的映射关系,使用B-tree是因为etcd支持范围查询,使用hash表不适合,从性能上看,B-tree相对于二叉树层级较矮,效率更高;boltdb是个基于 B+ tree 实现的 key-value 键值库,支持事务,提供 Get/Put 等简易 API 给 etcd 操作。
存储层:存储层包含预写日志 (WAL) 模块、快照 (Snapshot) 模块、boltdb 模块。其中 WAL 可保障 etcd crash 后数据不丢失,boltdb 则保存了集群元数据和用户写入的数据。
etcd 是典型的读多写少存储,在我们实际业务场景中,读一般占据 2/3 以上的请求。
读请求:客户端通过负载选择一个etcd节点发出读请求,API接口层提供Range RPC方法,etcd服务端拦截gRPC 读请求后调用的处理请求。
写请求:客户端通过负载均衡选择一个etcd节点发起请求etcd服务端拦截gRPC写请求,涉及校验和监控后KVServer向raft模块发起提案,内容写入数据命令,经过网络转发,当集群中多数节点达成一致持久化数据后,状态变更MVCC模块执行提案内容。
etcd客户端工具通过etcdctl执行一个读命令,解析完请求中的参数创建clientv3 库对象,然后通过EndPoint列表使用Round-Robin负载均衡算法选择一个etcd server节点,调用 KVServer API模块基于 HTTP/2 的 gRPC 协议的把请求发送给 etcd server,拦截器拦截,主要做一些校验和监控,然后调用KVserver模块的Range接口获取数据。读操作的核心步骤:
线性读是相对串行读来讲的概念,集群模式下会有多个etcd节点,不同节点间可能存在一致性的问题。串行读直接返回状态数据,不需要与集群中其他节点交互。这种方式速度快,开销小,但是会存在数据不一致的情况。
线性读则需要集群成员之间达成共识,存在开销,响应速度相对慢,但是能保证数据的一致性,etcd默认的读模式线性读。
etcd中查询请求,查询单个键或者一组键及查询数量,到底层实际会调用Range keys方法。
流程如下:
ReadTx和BatchTx是两个几口,用于读写请求创建Backend结构体,默认也会创建readTx和batchTx。readTx实现了ReadTx,负责处理只读请求batchTx,实现了BatchTx接口,负责处理读写请求。
对于上层的键值存储,它会利用返回的Revision从正真的存储数据中的BoltDB中,查询当前key对应的Revsion数据。BoltDB内部用类似buctket的方式存储对应MySQL中的表结构,用户key数据存放bucket的名字是key etcd mvcc元数据存放bucket的meta。
核心模块的功能:
写操作涉及核心模块功能如下:
Quoto模块
KVServer模块
WAL模块
Apply模块
MVCC模块
日志由一个个递增的有序序号索引标识。Leader维护了所有Follow节点的日志复制进度,在新增一个日志后,会将其广播给所有Follow节点。Follow节点处理完成后,会告知Leader当前已复制的最大日志索引。Leader收到后,会计算被一半以上节点复制过的最大索引位置,标记为已提交位置,在心跳中告诉Follow节点。只有被提交位置以前的日志才会应用到存储状态机。
在本地安装、运行和测试etcd的单成员集群,部署详细可以查看下上一篇《云原生API网关全生命周期管理Apache APISIX探究实操》中有关于etcd单节点部署,单节点部署完毕后验证读写和查看版本信息如下:
静态地启动etcd集群要求集群中的每个成员都认识集群中的其他成员;但通常集群成员的ip可能事先未知,可以通过发现服务引导etcd集群。在生产环境中,为了整个集群的高可用,etcd 正常都会集群部署,避免单点故障。引导 etcd 集群的启动有以下三种机制:
在部署之前已经知道了集群成员、它们的地址和集群的大小,name可以通过设置initial-cluster标志来使用脱机引导配置。分别在各个节点上执行下面语句
etcd --name infra1 --initial-advertise-peer-urls http://192.168.3.111:2380 \
--listen-peer-urls http://192.168.3.111:2380 \
--listen-client-urls http://192.168.3.111:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.3.111:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster infra1=http://192.168.3.111:2380,infra2=http://192.168.3.112:2380,infra3=http://192.168.3.113:2380 \
--initial-cluster-state new
etcd --name infra2 --initial-advertise-peer-urls http://192.168.3.112:2380 \
--listen-peer-urls http://192.168.3.112:2380 \
--listen-client-urls http://192.168.3.112:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.3.112:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster infra1=http://192.168.3.111:2380,infra2=http://192.168.3.112:2380,infra3=http://192.168.3.113:2380 \
--initial-cluster-state new
etcd --name infra3 --initial-advertise-peer-urls http://192.168.3.113:2380 \
--listen-peer-urls http://192.168.3.113:2380 \
--listen-client-urls http://192.168.3.113:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.3.113:2379 \
--initial-cluster-token etcd-cluster-1 \
--initial-cluster infra1=http://192.168.3.111:2380,infra2=http://192.168.3.112:2380,infra3=http://192.168.3.113:2380 \
--initial-cluster-state new
也可以通过nohup &后台启动etcd,获取集群的member信息
etcdctl --endpoints=192.168.5.52:2379 member list
# 创建日志目录
mkdir /var/log/etcd
# 创建数据目录
mkdir /data/etcd
mkdir /home/commons/data/etcd
发现URL标识唯一的etcd集群。每个etcd实例共享一个新的发现URL来引导新集群,而不是重用现有的发现URL。如果没有可用的现有集群,则使用discovery.etc .io托管的公共发现服务。要使用“new”端点创建一个私有发现URL,使用命令:
# 通过curl生成
curl https://discovery.etcd.io/new?size=3
https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200
# 通过上面返回组装
ETCD_DISCOVERY=https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200
--discovery https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200
分别在各个节点上执行下面语句
etcd --name myectd1 --data-dir /home/commons/data --initial-advertise-peer-urls http://192.168.5.111:2380 \
--listen-peer-urls http://192.168.5.111:2380 \
--listen-client-urls http://192.168.5.111:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.5.111:2379 \
--discovery https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200
etcd --name myectd2 --data-dir /home/commons/data --initial-advertise-peer-urls http://192.168.5.112:2380 \
--listen-peer-urls http://192.168.5.112:2380 \
--listen-client-urls http://192.168.5.112:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.5.112:2379 \
--discovery https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200
etcd --name myectd3 --data-dir /home/commons/data --initial-advertise-peer-urls http://192.168.5.113:2380 \
--listen-peer-urls http://192.168.5.113:2380 \
--listen-client-urls http://192.168.5.113:2379,http://127.0.0.1:2379 \
--advertise-client-urls http://192.168.5.113:2379 \
--discovery https://discovery.etcd.io/d45c453e99404bcb4b0b30b0ff924200
#写入KV
etcdctl put /key1 value1
etcdctl put /key2 value2
etcdctl put /key3 value3
# 范围,左闭右开
etcdctl get /key1 /key3
# 以十六进制格式读取key foo值的命令:
etcdctl get /key1 --hex
# 仅打印value
etcdctl get /key1 --print-value-only
# 前缀匹配和返回条数
etcdctl get --prefix /key --limit 2
# 按照key的字典顺序读取,大于或等于
etcdctl get --from key /key1
# 监听key,可以获取key变更信息
etcdctl watch /key1
# 重新修改
etcdctl put /key1 value111
# 读取版本
etcdctl get /key1 --rev=5
# 删除key
etcdctl del /key3
# 租约,例如授予60秒生存时间的租约
etcdctl lease grant 60
lease 5ef786eee44b831d granted with TTL(60s)
# 写入带租约
etcdctl put --lease=5ef786eee44b831d /key4 value4
# 撤销租约
etcdctl lease revoke 32695410dcc0ca06
# 授权创建角色
etcdctl role add testrole
etcdctl role list
etcdctl role grant-permission testrole read /permission
etcdctl role revoke-permission testrole /permission
etcdctl role del testrole
# 授权创建用户
etcdctl user add testuser
etcdctl user list
etcdctl user passwd
etcdctl user get testuser
etcdctl user del testuser
etcdctl user grant-role testuser testrole
# 创建测试账号2
etcdctl role add testrole2
etcdctl role grant-permission testrole2 readwrite /permission
etcdctl user add testuser2
etcdctl user grant-role testuser2 testrole2
#1. 添加root角色
etcdctl role add root
#2. 添加root用户
etcdctl user add root
#3. 给root用户授予root角色
etcdctl user grant-role root root
#4.激活auth
etcdctl auth enable
etcdctl put /permission all2 --user=testuser2
etcdctl get /permission --user=testuser2
etcdctl get /permission --user=testuser
etcdctl put /permission allhello --user=testuser
# 直接带上密码
etcdctl --user='testuser2' --password='123456' put /permission all2
# 集群鉴权
etcdctl --endpoints http://192.168.3.111:2379,http://192.168.3.111:2379,http://192.168.3.111:2379 --user=root --password=123456 auth enable
etcdctl --endpoints http://192.168.3.111:2379,http://192.168.3.111:2379,http://192.168.3.111:2379 --user=root:123456 auth enable