etcd 分布式存储

450 阅读6分钟

etcd基础架构

image.png etcd最初的目标是作为一个提供键值存储的服务。存储服务的配置信息必然要高可用,不然会导致集群无法变更、影响副本从而导致影响用户层面,所以要分布式存储。而分布式就会导致多节点的数据一致性问题,所以引入共识算法Raft。 数据模型(存储引擎)采用B-tree和blotdb实现了一个MVCC数据库,通过扁平的kv提供可靠的事务、多key原子更新,且基于blotdb持久化,降低了etcd内存占用。

etcd读写流程

客户端发起请求:etcdctl解析参数,通过clientv3库对象使用KVServer模块的API来访问server。通过Round-robin轮询的负载均衡选择一个etcdserver接口长连接,然后调用Range RPC方法,基于gRPC协议向server发送请求。Server中有拦截器+hook实现流量日志、行为检查等。请求到达,server根据Servicename和RPC Method将请求转发到对应的handler实现,handler会把拦截器串联执行,然后再执行请求。

写流程:image.png 客户端的请求经过gRPC拦截器,经过Quota配额模块判断文件大小是否超过配额,然后进入KVServer模块。Server模块会进行限速/鉴权/大包判断避免雪崩,然后KVServer模块会为这个请求生成一个唯一ID,并与消息通知channel关联,然后向Raft模块提交一个put提案(Server会等待写入结果并通过channel返回,若超过timeout则会报错)。此提案会被Leader节点通过RaftHTTP网络模块转发给多个节点,Follower节点持久化日志条目。Leader节点还会从raft模块获取已提交的日志条目广播给集群,同时把Leader信息持久化到WAL日志,保证集群一致性、可恢复性,传递给Apply模块生成快照,然后通过MVCC模块执行提案,MVCC中存储版本号树与kv数据(实现为boltdb)。MVCC多版本控制对应版本号,所以要保证版本号幂等性、数据操作与版本号更新原子性,所以我们用Raft日志条目中的index字段来去重等(外部唯一自增ID,同boldb中的key),然后etcd将value和版本号等信息序列化成二进制数据,顺序写入并提交事务(也不是直接提交,而是通过合并 + 异步定时机制来批量提交,提高吞吐量)。更新状态机。

线性读:通过ReadIndex机制保证一致性读取。集群收到读请求后,会先去Leader获取“最新已提交索引”,此时Leader会为防止脑裂而发送心跳,半数确认后返回“committed index”给处理读请求的子节点。子节点则会等待“committed index”,然后与自己的“状态机已应用索引”比较,直至子节点索引赶上Leader后,才会允许访问数据。 赶上后,就会与状态机中的MVCC模块交互。etcd通过内存B tree 和 基于内存B+ tree的kv键值库boltdb来实现MVCC,通过存储递增版本号-kv信息来实现版本控制(B树存储版本号,B+树存储version-kv) (默认线性读,若使用串行读则不会经过Raft层和逻辑层。早起etcd3.0读请求是走Raft保证一致性,Raft log read依赖磁盘IO,性能不如ReadIndex)

Raft算法

Lease租约:

Lease模块架构:image.png

它通过Lessor模块提供的API来进行创建Lease/撤销Lease/续期,然后再通过Grant接口把Lease存在内存的ItemMap中,还会持久化到boltdb中。KV模块的API提供了--lease参数,用于关联内存中的Lease和key。关联是MVCC起作用,而MVCC在持久化存储kv时,保存在boltdb中的value是一个kv结构体,包含关联的LeaseID,所以不怕etcd重启后内存中的关联消失。

续期:节点主动向协调服务上报健康状态,Lease模块保证在约定的TTL内,etcd server不会删除它关联的k-v。而我们续期的TTL不能过长也不能过短,过长导致无法及时删除,影响锁的可用性;过短导致Lease频繁被请求创建,etcd无法支持大规模的Lease数量。所以我们可以把TTL属性保持在Lease上面,可以做到Lease复用,而且使用gRPC进行多路复用,一个连接可以同时支持多个Lease续期。

过期删除:Leader节点负责在Lease模块中按照TTL维护了一个最小堆,然后Lessor每隔500ms进行一次检查,把过期的元素加入淘汰列表,然后Leader会把过期的LeaseID保存在一个channel中,server会定期从channel中获取ID并发起revoke请求,通过Raft Log传递给Follower节点。

Watch

MVCC

etcd在每次修改key时会生成一个全局递增的版本号,然后通过B-tree保存用户key与版本号之间的关系,再以版本号-用户kv作为键值对保存到boltdb中(用户kv由用户key、value、create_revision、mod_revision、version、lease组成)。

事务

Apply模块执行事务操作,进行 if-then/else完成事务操作。若在写操作只是写入内存但还没持久化时出错回滚,则会重放WAL日志条目,保证原子性;若在MVCC写事务完成、server响应完成、boltdb事务提交goroutine,但事务批量持久化入磁盘时发生crash

etcd分布式锁

分布式锁要满足安全性、互斥性、活性、可用性。 互斥性:我们可以通过etcd的事务机制来进行判断获取锁(互斥表现为IF判断key的mod_revision和create_revision是否相同),通过判断client是否成功创建一个固定的key,来获取分布式锁(即key的create_revision = 0),若多个client同时写入则比较revision,最小的client成功获取锁;或者通过多个client创建prefix相同,但名称不同的key,key的创建版本号小就是获取锁成功的client。 安全性:etcd基于Raft共识算法实现,写请求已经经过集群多数节点确认,即保证了持久化成功,不会出现Redis中主备异步复制导致的数据丢失,具备高可靠和强一致存储。 活性(避免死锁):Lease活性检测机制,通过心跳机制+自动释放,来解决crash故障、集群网络隔离引发的死锁问题,当超过Lease TTL时,锁会被自动释放。 可用性:etcd的watch特性提供高效的数据监听能力。client获取锁失败时,会各自监听prefix相同且revision比自己小的key,它释放锁后自己才有机会获取锁;client收到Watch Delete事件后,可以快速判断自己是否有资格获取锁,减少了锁的不可用时间。