etcd 进阶杂谈

从2020年到现在,对于etcd的技术恐惧持续了很长时间,偶然发现极客时间有一门课程《etcd实战课》,读了下开篇词,深有感触,是时候踏出舒适区,系统性的学习一下etcd了。本文正是对etcd学习的一个总结,从一个新手的角度回顾一下etcd学习的知识点。

etcd是什么

按照官网的描述,etcd是一个分布式的key-value存储系统。分布式和存储这两个关键字哪个都不简单,组合到一起更是让人望而生畏。

如果只是一个简单的key-value存储系统,etcd用不到花这么多年的时间持续的优化。那么在这个key-value的基础上,etcd又扩充了哪些能力,导致etcd给人的感觉这么的复杂呢?

etcd的技术栈

image.png

etcd与raft的关系

image.png

简单来说,raft是共识算法的一种实现,有leader选举、日志复制、日志存储。raft提供了输入、输出的相关接口,比如raft输出的日志同步消息(Ready接口)要经过etcdserver提供的网络功能进行传输,etcdserver处理完请求之后,要驱使raft进行下一个消息的处理。

etcd技术演进

etcd做为一个基础组件,本身必须具备一定的高可用,需要多副本部署。etcd引入了raft算法,raft算法包括leader选举、日志复制、状态机。这样etcd首先具备了多副本部署的数据协调能力。为了设计上的简单化,写操作只能由leader进行处理,由leader将数据同步到各个follower节点。这样一份数据就在多个节点上都存在,读请求任意节点都可以处理,这就是分布式存储的意义吗?

接触过openstack的知道,openstack社区推荐的一个集群大小建议是小于500台,然而kubernetes社区推荐的一个集群大小建议是小于5000台,10倍的差距一方面得益于kubernetes优良的设计,etcd在性能提升中也扮演了非常重要的角色。

etcd 基于raft 实现了 分布式,基于boltdb实现key-value存储,那么etcd又在此之上扩充了哪些能力呢?

  • lease
    lease是etcd提供的一个附加了ttl(time to live)属性的功能。比如创建了一个过期时期600秒的lease,又将几个key附加到了这个lease上,那么在600s之后,这个lease和这个lease关联的key都会被etcd自动清理掉,根据业务需要,所以需要保持lease,需要client定期为lease续期(keepalive)。

    lease相关的接口,包括 创建、撤销(删除)、续期、关联(attach key to lease)操作。相应得,etcd有两个goroutine来管理lease,一是定期更新lease的到期时间,二是删除过期的lease,当集群的lease数非常多时,效率也是个问题,为此etcd使用最小堆这种数据结构来管理lease,最小堆的查询时间复杂度为O(1),这样每次只需要遍历堆顶lease是否过期即可,大大减少了cpu的消耗。

  • watch
    watch是指etcd可以实时将key的变更通知到client。比如client通过watch接口告知etcd自己关注money的变化,假如money有变化的话,etcd会实时的推送给client money的变化。

    etcd支持监听key以及范围key,如何高效的根据key查找到对应的client watcher呢?etcd使用了map和区间树两种数据结构来实现高效的查找。

    watch是如何监测到key变化并进行通知呢?是在事务结束时,将变更打包成event,通知到etcdserver。

func (tw *watchableStoreTxnWrite) End() {
    changes := tw.Changes()

    rev := tw.Rev() + 1
    evs := make([]mvccpb.Event, len(changes))
    for i, change := range changes {
        evs[i].Kv = &changes[i]
    }

    // end write txn under watchable store lock so the updates are visible
    // when asynchronous event posting checks the current store revision
    tw.s.notify(rev, evs)
}
  • 认证鉴权
    在一些场景中,etcd要为多个用户服务,这就必然涉及到认证鉴权的问题,认证和鉴权需要区分一下,认证是指一个用户是否是合法用户,鉴权是指一个用户是否具有操作一个key的权限,这里的操作可以指读写删除。比如一个公司内的员工佩戴工牌可以自由进出公司的大门,但是销售人员进不了机房,普通员工进不了董事长的办公室。
  • 限制
    etcd存储的是一些关键的配置信息,并不是数据,所以没有数据分片的能力,boltdb大小建议是 小于8GB,单个key的value大小限制是1.5M。正是etcd的产品定位和这些限制,保证了etcd的高性能。
  • 限速
    etcd目前的限速是比较简单的,这里的限速不是指限制客户端访问的qps,而是指apply与commit的差值,这个差值是代码中写死的5000。如果差值超过5000,etcd将拒绝写入。apply是指数据已经更新到boltdb持续化存储中,commit是指数据已经提交到raft日志中。
  • mvcc (Multi-Version Concurrency Control)
    etcd可以保存一个key的多个历史版本,并基于mvcc实现了简单的事务隔离。

etcd 的存储

etcd的存储是让人很容易迷惑的地方,这里首先接受一个etcd写入一个key-value的流程。leader收到一个put hello=world请求,leader将此put操作打包成一个提案(proposal)递交给raft模块,raft模块将此提案同步给各个follower节点,各个follower节点从raft模块获取到这个提案,应用到raft的存储中,并追加到wal中,随后回复给leader此提案已提交。leader收到follower节点的已提交回复后,如果集群中的多数节点都为已提交,那么各个节点的etcdserver 就可以将此提案更新到boltdb持久化存储中。

  • raft unstable 存储
    leader接受到提案后,再未同步到其他follower之前,需要保存提案,此时提案保存在leader raft中的unstable存储中,就是一个数组
  • raft 稳定存储
    当提案被raft模块同步到各个节点时,节点需要保存这些已经被提交的提案,此时这些变更的提案被保存在raft的稳定存储中,也是一个数组。
    目前etcd raft存储的数据结构是MemoryStorage。
// MemoryStorage implements the Storage interface backed by an
// in-memory array.
type MemoryStorage struct {
    // Protects access to all fields. Most methods of MemoryStorage are
    // run on the raft goroutine, but Append() is run on an application
    // goroutine.
    sync.Mutex

    hardState pb.HardState
    snapshot  pb.Snapshot
    // ents[i] has raft log position i+snapshot.Metadata.Index
    ents []pb.Entry
}
  • wal
    当follower节点收到提案时,首先会将提案内容保存到wal中,并调用fsync将提案持久化到磁盘中,之后再追加到raft 基于内存的稳定内存中,wal这个词并不陌生,二阶段提交的一种解决方案,节点异常时,通过重放wal中的变更,可以保证数据的一致性。
  • boltdb
    boltdb是一个开源的key-value存储数据库,etcd基于boltdb存储用户的key-value数据。etcd数据目录中member/snap/db就是key-value数据在磁盘上的文件。etcd可以保存一个key的多个历史版本,为了提高性能,boltdb存储的是etcd版本号与key-value的对应的关系,并不是key与value的对应关系。
  • keyIndex
    etcd的查询操作也可以理解为两阶段查询,首先从keyIndex中根据key查找到key的revisions,然后再根据revisions从boltdb中查询。

etcd的snapshot

在etcd中,多个场景下的操作都叫snapshot,这样不加区分的命名,增加了我们理解的难度。

  • raft中的snapshot
    raft的稳定存储是基于内存和数据结构中的数据进行存储的,每一次对于key-value的变更事件都会保存到raft的稳定存储中,久而久之,etcd肯定会因为内存占用超限被oom掉,所以需要有一定的机制清理raft的稳定存储,etcd中的snapshot-count(默认值为100000)的配置就是这个意义,当变更次数达到这个值时,etcd就会做一次清理操作,这个操作叫做snapshot是否合理呢?
  • etcd中的snapshot
    当集群新加入一个节点时,leader需要向新节点同步数据,同步数据的方式也叫snapshot,其实就是将db文件发送给新节点,用于新节点快速跟上leader的数据。
  • etcdctl中的snapshot
    etcdctl有个子命令叫做snapshot,这里的snapshot是指对etcd的数据做一个快照,为什么不叫备份呢?是因为snapshot会多存储一些元数据信息吗?

etcd的压缩机制

etcd具有保存key的多个版本的能力,keyIndex中存储的是key与revision的关系,boltdb中存储的是revision与key-value的关系,那么随着变更次数的增加,etcd内存和占用磁盘的空间很快就会超限,所以要有机制来定期清理历史的key,这个操作叫做compact,etcd支持周期性或者版本号的压缩策略。etcd中默认配置中是没有配置压缩策略的,但是在kubernetes的环境中,查看etcd的日志,发现每5min中就会有一条压缩的日志,这个日志是kube-apiserver的配置etcd-compaction-interval,默认值就是5min。

源码调试etcd

要想更深入的学习etcd相关的知识,还是要深入到源码中。etcd已经走过了近10个年头,相关的代码抽象度也是很高的,没有一定的实践,也不太容易厘清etcd的代码结构。幸运的是etcd是golang编写的,也可以在windows下运行,因此通过使用goland 源码 debug etcd,学习起来效率会更高。最简单的可以单节点运行,学习etcd的读写事务操作的流程,后面可以在一台机器上通过多个不同的端口部署多个etcd,调整选举的超时时间,选择其中的一个进程进行调试即可。

debug的方式比较简单,goland的界面也是简单易懂,按照正常的go程序的debug方式操作就可以 了

image.png

etcd 的监控

etcd提供了非常多的metrics用来观测etcd集群,社区也提供了相应的grafana的dashboard简化配置的复杂度。

如果不理解etcd的整个读写流程,相关的metrics也不容易看懂,最好的方式还是到源码中查看metrics在什么流程下更新,才能更好的理解metrics的含义。

一些常用的metrics,比如db文件大小、网络流量大小,节点间的ttl延迟、磁盘延迟、B+树的分裂与重平衡的耗时,提交的提案数等等。下面四张图是从《etcd实战课》中贴过来的。


disk.png
network.png
mvcc.png
server.png

更多的metrics可以在代码中搜索prometheus.MustRegister

func init() {
    prometheus.MustRegister(rangeCounter)
    prometheus.MustRegister(rangeCounterDebug)
    prometheus.MustRegister(putCounter)
    prometheus.MustRegister(deleteCounter)
    prometheus.MustRegister(txnCounter)
    prometheus.MustRegister(keysGauge)
    prometheus.MustRegister(watchStreamGauge)
    prometheus.MustRegister(watcherGauge)
    prometheus.MustRegister(slowWatcherGauge)
    prometheus.MustRegister(totalEventsCounter)
    prometheus.MustRegister(pendingEventsGauge)
    prometheus.MustRegister(indexCompactionPauseMs)
    prometheus.MustRegister(dbCompactionPauseMs)
    prometheus.MustRegister(dbCompactionTotalMs)
    prometheus.MustRegister(dbCompactionLast)
    prometheus.MustRegister(dbCompactionKeysCounter)
    prometheus.MustRegister(dbTotalSize)
    prometheus.MustRegister(dbTotalSizeInUse)
    prometheus.MustRegister(dbOpenReadTxN)
    prometheus.MustRegister(hashSec)
    prometheus.MustRegister(hashRevSec)
    prometheus.MustRegister(currentRev)
    prometheus.MustRegister(compactRev)
    prometheus.MustRegister(totalPutSizeGauge)
}

func init() {
    prometheus.MustRegister(walFsyncSec)
    prometheus.MustRegister(walWriteBytes)
}

func init() {
    prometheus.MustRegister(leaseGranted)
    prometheus.MustRegister(leaseRevoked)
    prometheus.MustRegister(leaseRenewed)
    prometheus.MustRegister(leaseTotalTTLs)
}

总结

本文从技术演进的角度概括了etcd的功能点,一些注意事项,以及etcd大概的工作流程。水平有高低,细节深似海,表达有出入,有错误也在所难免,不同的时间,有不同的理解。

你可能感兴趣的:(etcd 进阶杂谈)