云原生 K8S 精选的分布式可靠的键值存储 etcd 原理和实践
概述
定义
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 的使用场景默认处理的数据都是控制数据,对于应用数据,只推荐数据量很小,但是更新访问频繁的情况。应用场景有如下几类
键值存储的配置管理
服务注册与发现
消息发布与订阅
负载均衡
分布式通知与协调
分布式锁、分布式队列
集群监控与 Leader 选举
如果需要一个分布式存储仓库来存储配置信息,并且希望这个仓库读写速度快、支持高可用、部署简单、支持 http 接口,那么就可以使用云原生项目 etcd。获取地址:http://www.jnpfsoft.com/?from=infoq
特性
接口简洁:使用标准 HTTP 工具(如 curl)读取和写入值。
KV 存储:将数据存储在按层次结构组织的目录中,就像在标准文件系统中一样。
监听变化:观察特定键或目录的变化,并对值的变化做出反应。
可靠:通过 Raft 协议实现分布式功能。
安全:可选 SSL 客户端证书认证,用于密钥过期的可选 ttl。
快速:基准测试为 10,000 写入/秒。
为何使用 etcd
etcd 实现的绝大多数功能 Zookeeper 都能实现,那为何还要用 etcd?相较之下,Zookeeper 有如下缺点:
复杂:Zookeeper 的部署维护复杂,管理员需要掌握一系列的知识和技能;而 Paxos 强一致性算法也是素来以复杂难懂而闻名于世;另外,Zookeeper 的使用也比较复杂,需要安装客户端,官方只提供了 java 和 C 两种语言的接口。
Java 编写:Java 本身就偏向于重型应用,它会引入大量的依赖;而运维人员则普遍希望机器集群尽可能简单,维护起来也不易出错。
发展缓慢:Apache 基金会项目特有的“Apache Way”在开源界饱受争议,其中一大原因就是由于基金会庞大的结构以及松散的管理导致项目发展缓慢。
而 etcd 作为一个后起之秀,对比 Zookeeper 其优点如下
简单:使用 Go 语言编写部署简单;使用 HTTP 作为接口使用简单;使用 Raft 算法保证强一致性让用户易于理解。
数据持久化:etcd 默认数据一更新就进行持久化。
安全:etcd 支持 SSL 客户端安全认证。
etcd 作为一个年轻的项目,正在高速迭代和开发中,这既优点也是缺点。优点在于它的未来具有无限的可能性,缺点是版本的迭代导致其使用的可靠性无法保证,无法得到大项目长时间使用的检验。但由于 CoreOS、Kubernetes 和 Cloudfoundry 等知名项目均在生产环境中使用了 etcd,所以总的来说 etcd 值得去尝试。
术语
Alarm:当集群需要操作员干预以保持可靠性时,etcd 服务器就会发出警报
Authentication:认证管理 etcd 资源的用户访问权限。
client:客户端连接到 etcd 集群以发出服务请求,例如获取键-值对、写入数据或监视更新。
Cluster:集群由几个成员组成;每个成员中的节点遵循 raft 共识协议进行日志复制。集群接收来自成员的提案,提交并申请到本地存储。
Compaction:压缩将丢弃给定修订之前的所有 etcd 事件历史记录和被取代的键。它用于回收 etcd 后端数据库中的存储空间。Election
Election:作为共识协议的一部分,etcd 集群在其成员之间举行选举,以选择领导人。
Endpoint:指向 etcd 服务或资源的 URL。
Key:用于在 etcd 中存储和检索用户定义值的用户定义标识符。
Key range:一组键,其中包含单个键、所有 x 的词法间隔(A < x <= b)或所有大于给定键的键。
Keyspace:etcd 集群中所有键的集合。
Lease:一种短期可再生合同,相当于租期,到期时删除与其相关的 key。
Member:参与服务 etcd 集群的逻辑 etcd 服务器。
Modification Revision:保存对给定键的最后一次写操作的第一个修订。
Peer:Peer 是同一集群的另一个成员。
Proposal:提案是需要通过 Raft 协议的请求(例如写请求、配置更改请求)。
Quorum:修改集群状态所需的协商一致的活动成员数量。Etcd 要求会员过半数才能达到法定人数。
Revision:64 位集群范围的计数器,从 1 开始,每次修改 keyspace 时递增。
Role:权限单位,一组 key 范围内的权限单位,可授予一组用户进行访问控制。
Snapshot:etcd 集群状态的时间点备份。
Store:支持集群 keyspace 的物理存储。
Transaction:一组原子执行的操作。事务中的所有修改键共享相同的修改修订。
Key Version:自创建 key 以来对其进行写操作的次数,从 1 开始。不存在或已删除的密钥的版本号为 0。
Watcher:客户端打开一个监视器来观察给定键范围的更新。
架构
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 接口获取数据。读操作的核心步骤:
线性读 ReadIndex 模块
MVCC(包含 treeindex 和 BlotDB)模块
线性读是相对串行读来讲的概念,集群模式下会有多个 etcd 节点,不同节点间可能存在一致性的问题。串行读直接返回状态数据,不需要与集群中其他节点交互。这种方式速度快,开销小,但是会存在数据不一致的情况。
线性读则需要集群成员之间达成共识,存在开销,响应速度相对慢,但是能保证数据的一致性,etcd 默认的读模式线性读。
etcd 中查询请求,查询单个键或者一组键及查询数量,到底层实际会调用 Range keys 方法。
流程如下:
在 treeIndex 中根据键利用 BTree 快速查询该键对应索引项 KeyIndex,索引项中包含 Revison
根据查询到的版本号信息 Revision,在 Backend 的缓存 Buffer 中用二分法查找,如命中则直接返回
若缓存中不符合条件,在 BlotDB 中查找,(基于 BlotDB 的索引),查询后返回键值对的信息。
ReadTx 和 BatchTx 是两个几口,用于读写请求创建 Backend 结构体,默认也会创建 readTx 和 batchTx。readTx 实现了 ReadTx,负责处理只读请求 batchTx,实现了 BatchTx 接口,负责处理读写请求。
对于上层的键值存储,它会利用返回的 Revision 从正真的存储数据中的 BoltDB 中,查询当前 key 对应的 Revsion 数据。BoltDB 内部用类似 buctket 的方式存储对应 MySQL 中的表结构,用户 key 数据存放 bucket 的名字是 key etcd mvcc 元数据存放 bucket 的 meta。
核心模块的功能:
KVServer 串行读:状态机数据返回、无需通过 Raft 协议与集群进行交互。它具有低延时、高吞吐量的特点,适合对数据一致性要求不高的场景。线性读:etcd 默认读模式是线性读,在延时和吞吐量上相比串行读略差一点,适用于对数据一致性要求高的场景。当收到一个线性读请求时,它首先会从 Leader 获取集群最新的已提交的日志索引。
Leader 收到 ReadIndex 请求时,为防止脑裂等异常场景,会向 Follower 节点发送心跳确认,一半以上节点确认 Leader 身份后才能将已提交的索引 (committed index) 返回给节点。节点则会等待,直到状态机已应用索引 (applied index) 大于等于 Leader 的已提交索引时 (committed Index),然后去通知读请求,数据已赶上 Leader,你可以去状态机中访问数据了。
MVCC 多版本并发控制 (Multiversion concurrency control) 模块是为了解决 etcd v2 不支持保存 key 的历史版本、不支持多 key 事务等问题而产生的。etcd 保存一个 key 的多个历史版本的方案为:每次修改操作,生成一个新的版本号 (revision),以版本号为 key, value 为用户 key-value 等信息组成的结构体。
treeIndex 基于 btree 库实现,只保存用户的 key 和相关版本号信息。而用于的 key,value 数据则存储在 boltdb 里面,相比于 etcd v2 全内存存储,etcd v3 对内存要求更低。
buffer 并不是所有请求都一定要从 boltdb 获取数据。etcd 出于数据一致性、性能等考虑,在访问 boltdb 前,首先会从一个内存读事务 buffer 中,二分查找你要访问 key 是否在 buffer 里面,若命中则直接返回。
boltdb 若 buffer 未命中,此时就真正需要向 boltdb 模块查询数据了。
写操作
客户端通过负载均衡算法选择一个 etcd 节点,发起 gRPC 调用。
etcd Server 收到客户端请求。
经过 gRPC 拦截,Quota 校验,Quota 模块用于校验 etcd db 文件大小是否超过了配额。
KVserver 模块将请求发送给本模块的 raft,负责与 etcd raft 模块进行通信,发起一个提案,命令为 put foo bar,即使用 put 方法将 foo 更新为 bar。
提案经过转发之后,半数节点成功持久化。
MVCC 模块更新状态机。
写操作涉及核心模块功能如下:
Quoto 模块
client 端发起 gRPC 调用到 etcd 节点,和读请求不一样的是,写请求需要经过流程二 db 配额(Quota)模块。
当 etcd server 收到 put/txn 等写请求的时候,会首先检查下当前 etcd db 大小加上你请求的 key-value 大小之和是否超过了配额(quota-backend-bytes)。如果超过了配额,它会产生一个告警(Alarm)请求,告警类型是 NO SPACE,并通过 Raft 日志同步给其它节点,告知 db 无空间了,并将告警持久化存储到 db 中。
配额为'0'表示使用 etcd 默认的 2GB 大小,可以根据业务常见进行调优。etcd 社区建议不超过 8G。如果填小于 0 的数,表示禁用配额功能,但这会让 db 大小处于失控状态,导致性能下降,所以不建议使用。
KVServer 模块
etcd 是基于 Raft 算法实现节点间数据复制的,因此它需要将 put 写请求内容打包成一个提案消息,提交给 Raft 模块。
WAL 模块
Raft 模块收到提案后,如果当前节点是 Follower,它会转发给 Leader,只有 Leader 才能处理写请求。Leader 收到提案后,通过 Raft 模块输出待转发给 Follower 节点的消息和待持久化的日志条目,日志条目则封装了提案内容。
Apply 模块
put 请求如果在执行提案内容的时候 crash 了,重启恢复的时候,会从 WAL 中解析出 Raft 日志条目内容,追加到 Raft 日志的存储中,并重放已提交的日志提案给 Apply 模块执行。
etcd 是个 MVCC 数据库,每次更新都会生成新的版本号。如果没有幂等性保护,同样的命令,一部分节点执行一次,一部分节点遭遇异常故障后执行多次,则系统的各节点一致性状态无法得到保证,导致数据混乱,这是严重故障。
Raft 日志条目中的索引(index)字段是全局单调递增的,每个日志条目索引对应一个提案,在 db 里面也记录下当前已经执行过的日志条目索引。
MVCC 模块
Apply 模块判断此提案未执行后,就会调用 MVCC 模块来执行提案内容。MVCC 主要由两部分组成,一个是内存索引模块 treeIndex,保存 key 的历史版本号信息,另一个是 boltdb 模块,用来持久化存储 key-value 数据。
日志复制
日志由一个个递增的有序序号索引标识。Leader 维护了所有 Follow 节点的日志复制进度,在新增一个日志后,会将其广播给所有 Follow 节点。Follow 节点处理完成后,会告知 Leader 当前已复制的最大日志索引。Leader 收到后,会计算被一半以上节点复制过的最大索引位置,标记为已提交位置,在心跳中告诉 Follow 节点。只有被提交位置以前的日志才会应用到存储状态机。
部署
单示例快速部署
在本地安装、运行和测试 etcd 的单成员集群,部署详细可以查看下上一篇《云原生 API 网关全生命周期管理 Apache APISIX 探究实操》中有关于 etcd 单节点部署,单节点部署完毕后验证读写和查看版本信息如下:
多实例集群部署
静态地启动 etcd 集群要求集群中的每个成员都认识集群中的其他成员;但通常集群成员的 ip 可能事先未知,可以通过发现服务引导 etcd 集群。在生产环境中,为了整个集群的高可用,etcd 正常都会集群部署,避免单点故障。引导 etcd 集群的启动有以下三种机制:
静态
etcd 动态发现
DNS 发现
静态
在部署之前已经知道了集群成员、它们的地址和集群的大小,name 可以通过设置 initial-cluster 标志来使用脱机引导配置。分别在各个节点上执行下面语句
也可以通过 nohup &后台启动 etcd,获取集群的 member 信息
etcd 动态发现
发现 URL 标识唯一的 etcd 集群。每个 etcd 实例共享一个新的发现 URL 来引导新集群,而不是重用现有的发现 URL。如果没有可用的现有集群,则使用 discovery.etc .io 托管的公共发现服务。要使用“new”端点创建一个私有发现 URL,使用命令:
分别在各个节点上执行下面语句
常见命令
文章转载自:itxiaoshen
评论