云原生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工具(如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:客户端打开一个监视器来观察给定键范围的更新。

架构

image-20230317111158166

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方法。

image-20230322173134586

流程如下:

  • 在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模块更新状态机。

image-20230322173338807

写操作涉及核心模块功能如下:

  • 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节点。只有被提交位置以前的日志才会应用到存储状态机。

image-20230322174146831

部署

单示例快速部署

在本地安装、运行和测试etcd的单成员集群,部署详细可以查看下上一篇《云原生API网关全生命周期管理Apache APISIX探究实操》中有关于etcd单节点部署,单节点部署完毕后验证读写和查看版本信息如下:

image-20230317113456175

多实例集群部署

静态地启动etcd集群要求集群中的每个成员都认识集群中的其他成员;但通常集群成员的ip可能事先未知,可以通过发现服务引导etcd集群。在生产环境中,为了整个集群的高可用,etcd 正常都会集群部署,避免单点故障。引导 etcd 集群的启动有以下三种机制:

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

静态

在部署之前已经知道了集群成员、它们的地址和集群的大小,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

etcd 动态发现

# 创建日志目录
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

image-20230321154946095

# 直接带上密码
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
  • 本人博客网站IT小神 www.itxiaoshen.com
posted @ 2023-03-22 23:35  itxiaoshen  阅读(242)  评论(0编辑  收藏  举报