Etcd是CoreOS基于 Raft协议 开发的分布式key-value存储,可用于服务发现、共享配置以及一致性
保障(如数据库选主、分布式锁等)。
在分布式系统中,如何管理节点间的状态一直是一个难题,etcd像是专门为集群环境的服务发现
和注册而设计,它提供了数据TTL失效、数据改变监视、多值、目录监听、分布式锁原子操作等
功能,可以方便的跟踪并管理集群节点的状态。
将数据存储在分层组织的目录中,如同在标准文件系统中watchable,监测特定的键或目录以进行更改,并对值的更改做出反应Raft算法保证一致性上面是重点,下面了解即可:
etcd 是一个键值存储的组件,其他的应用都是基于其键值存储的功能展开。
支持动态存储(内存)以及静态存储(磁盘)。存储方式,采用类似目录结构。(kvindex BTree,blotdb B+Tree)叶子节点才能真正存储数据,相当于文件。叶子节点的父节点一定是目录,目录不能存储数据# B+树与B-树的区别
1. 从存储数据来看
b-树,叶子节点跟非叶子节点都储存数据
b+树,只有在叶子节点储存数据
2. 从结构上来看
B树:平衡多叉搜索树;
B+树:有序双向链表(作用是优化范围查询)+平衡多叉搜索树;
# B+树的特点:
1、B+树的层级更少:相较于B树B+每个非叶子节点存储的关键字数更多,树的层级更少所以查询数据更快;
2、B+树查询速度更稳定:B+所有关键字数据地址都存在叶子节点上,所以每次查找的次数都相同所以查询速度要比B树更稳定;
3、B+树天然具备排序功能:B+树所有的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候更方便,数据紧密性很高,缓存的命中率也会比B树高。
4、B+树全节点遍历更快:B+树遍历整棵树只需要遍历所有的叶子节点即可,,而不需要像B树一样需要对每一层进行遍历,这有利于数据库做全表扫描。
# B树相对于B+树的优点
如果经常访问的数据离根节点很近,而B树的非叶子节点本身存有关键字其数据的地址,所以这种数据检索的时候会要比B+树快。
基于 Raft 算法的 etcd 天生就是这样一个强一致性、高可用的服务存储目录。
用户可以在 etcd 中注册服务,并且对注册的服务配置 key TTL,定时保持服务的心跳以达到监控健康状态的效果。

在分布式系统中,最适用的一种组件间通信方式就是消息发布与订阅。
配置共享中心,数据提供者在这个配置中心发布消息,而消息使用者则订阅他们 关心的主题,一旦主题有消息发布,就会实时通知订阅者。集中管理。etcd节点上注册一个Watcher并等待,以后每次配置有更新的时候,etcd都会实时通知订阅者,以此达到 获取最新配置信息 的目的。
TTL(time to live)指的是给一个key设置一个有效期,到期后这个key就会被自动删掉,这在很多分布式锁的实现上都会用到,可以保证锁的实时有效性。
Atomic Compare-and-Swap(CAS)指的是在对key进行赋值的时候,客户端需要提供一些条件,当这些条件满足后,才能赋值成功。这些条件包括:
prevExist:key当前赋值前是否存在prevValue:key当前赋值前的值prevIndex:key当前赋值前的Index这样的话,key的设置是有前提的,需要知道这个key当前的具体情况才可以对其设置。
Raft协议基于quorum机制,即大多数同意原则,任何的变更都需超过半数的成员确认

一致性模块是和其他etcd节点做一致性协商,保持数据、集群一致性。
日志模块是写入的wal日志。
状态机就是将数据写入存储(这个状态机又分为内存kvindex、物理存储blotdb)。
参考动画理解:http://thesecretlivesofdata.com/raft/
选举方法
election timeout(随机值),如果在这一时间周期内没有收到来自leader 的 heartbeat,节点将发起选举:将自己切换为 candidate 之后,向集群中其它 follower节点发送请求,询问其是否选举自己成为leader,其他节点收到请求就会同意,因此其实这个election timeout就决定了谁是leader,同时被创建?看下面。过半数节点的接受投票后,节点即成为 leader,开始接收保存 client 的数据并向其它的 follower节点同步日志。如果有两个节点随机timeout一样,没有达成一致,两个节点选自己都是candidate,则两个candidate随机选择一个等待间隔(150ms ~ 300ms)再次发起投票,得到集群中半数以上follower接受的candidate将成为leader, 如何规避这个问题?etcd节点数需为奇数,3个节点,两个投自己,剩余那个节点的票决定谁是leader。定时向 follower 发送heartbeat来保持其地位。任何时候如果其它 follower 在 election timeout 期间都没有收到来自 leader 的heartbeat,同样会将自己的状态切换为 candidate 并发起选举。每成功选举一次,新 leader的任期(Term)都会比之前leader 的任期大1。当Leader接收到客户端的日志(事务请求)后先把该日志追加到本地的Log中,然后通过heartbeat把该Entry同步给其他Follower,Follower接收到日志后记录日志然后向Leader回复ACK,当Leader收到大多数(n/2+1)Follower的ACK信息后将该日志设置为已提交并追加到本地磁盘中,并在下个heartbeat中Leader将通知所有的Follower将该日志存储在自己的本地磁盘中。
上述两点的理解,建议参考下面图解。
提出问题:什么是分布式系统一致性?

首先像上面的单节点系统,就不存在一致性问题,但是存在单点故障问题
注意:黑虚线代表condidate,黑实线代表leader地位,黑色小球代表请求,红色小球代表心跳信息,但是logEntry数据、ack信息也可以附带在心跳包里,红色SET 5,代表写入wal日志,黑色SET 5代表写入存储。

上述图示展示了选举和日志复制过程。

大家可以看到,多了一个term任期,任期越大的,越有话语权,这极大的避免了脑裂的问题。还有就是leader发的heartbeat包,会更新其他两个etcd节点的election timeout,保持自己的领导地位。

我们可以看到node A变成了新的leader,且任期变成2,那么即使B起来了,也要变成follower,任期大,更牛皮。
当然奇数个节点也可能选举失败,只是奇数个可能性要更小。比如3个节点挂了一个节点。其他两个节点election timeout一样,票数都未超过半数。就选举失败,又要重新选举了,而且如果有多个节点election timout一样,那选举失败概率更大。

这个流程中发生了两次选举才成功选出node c leader。第一次,node A、node D各有两票,都未超半数,选举失败,如果是3个节点,就可以避免这种情况,两个一起选,最后那个节点给谁,谁就超过了半数。第二次,node c先发起投票,真悬,node c投票数据包还没到node b,b的投票数据包就出来了,因此node b获得投自己的1票,node c 3票当选第五任期的leader。

网络分区下,下面两个节点不超过半数,因此数据只会写入日志,而不会写入存储。上面节点超过半数,超时后发现没有leader,然后筛选leader出来之后,node D任期为2,这上面接收到的数据变更是可以写入存储的。当网络分区消失之后,下面任期1的leader听任期2的node D leader的话,然后变成follower。然后下面两个节点由于有未提交的log。因此会回滚日志,然后同步新的leader的日志,然后将set 8写入存储。
Raft 4.2.1引入的新角色, 当出现一个etcd集群需要增加节点时,新节点与Leader的数据差异较大,需要较多数据同步才能跟
上leader的最新的数据。此时Leader的网络带宽很可能被用尽,进而使得leader无法正常保持心跳。进而导致follower重新发起投票。进而可能引发etcd集群不可用。Learner角色只接收数据而不参与投票,因此增加learner节点时,集群的quorum不变。

安全性是用于保证每个节点都执行相同序列的安全机制,如当某个Follower在当前Leader commit Log时变得不可用了,稍后可能该Follower又会被选举为Leader,这时新Leader可能会用新的Log覆盖先前已committed的Log,这就是导致节点执行不同序列;Safety就是用于保证选举出来的Leader一定包含先前 committed Log的机制;
选举安全性(Election Safety):每个任期(Term)只能选举出一个Leader
Leader日志完整性(Leader Completeness):当Log在任期Term1被Commit后,那么以后任期Term2、Term3…等的Leader必须包含该Log;Raft在选举阶段就使用Term的判断用于保证完整性:当请求投票的该Candidate的Term较大或Term相同Index更大则投票,否则拒绝该请求。

wal日志是二进制的,解析出来后是以上数据结构LogEntry。其中第一个字段type,只有两种,一种是0表示Normal,1表示ConfChange(ConfChange表示 Etcd 本身的配置变更同步,比如有新的节点加入等)。第二个字段是term,每个term代表一个主节点的任期,每次主节点变更term就会变化。第三个字段是index,这个序号是严格有序递增的,代表变更序号。第四个字段是二进制的data,将raft request对象的pb结构整个保存下。etcd 源码下有个tools/etcddump-logs,可以将wal日志dump成文本查看,可以协助分析Raft协议。
Raft协议本身不关心应用数据,也就是data中的部分,一致性都通过同步wal日志来实现,每个节点将从主节点收到的data apply到本地的存储,Raft只关心日志的同步状态,如果本地存储实现的有bug,比如没有正确的将data apply到本地,也可能会导致数据不一致。

kvindex(用BTree写的),这个是做数据索引,读数据从磁盘读,那么性能肯定不行,一般的关系型数据库,有表,有字段,行列,索引的添加其实可以有很多种,但是etcd因为是键值对存储,因此kvindex其实只需要存储key的索引就行了。数据真正落盘是在blotdb,这个是B+Tree写的。监听机制要的数据从哪里去获得,就是在WatchableStore种存储的kvstore中会删除, 然后backend还会根据leaseMap(维护的是一个k: ttl的map)去从blotdb中删除过期的key value。疑问:为什么Etcd的内存数据使用Kvindex(BTree),磁盘数据使用B+Tree?
答:
B+Tree更稳定,由于只在叶子结点存储数据,因此它的层级更少,它的查询次数更少,由于磁盘IO是非常慢的,采用B+Tree这样会减少磁盘io的查询。查询效率才是需要首要考虑的。查询离根节点近的数据非常快,可以将热点数据放在离根近的地方。范围查询时很快,因为B树其实是个二分法,但是B+树是个N分法即N叉树,二分法每次筛选掉一半的节点,N分法每次只能筛选掉1/n的节点,因此二分法范围查询更快,你可能会说B+树不是有个有序双向链表可以进行加速范围查询吗,但是据说Etcd的B+树实现并没有有序双向链表,这个可能是有一定的考量吧,更深层次就不研究了。etcd v3 的watch机制支持watch某个固定的key,也支持watch一个范围(可以用于模拟目录的结构的watch),所以 watcherGroup 包含两种watcher,一种是 key watchers,数据结构是每个key对应一组watcher,另外一种是 range watchers, 数据结构是一个 IntervalTree,方便通过区间查找到对应的watcher。
同时,每个 WatchableStore 包含两种 watcherGroup,一种是synced,一种是unsynced,前者表示该group的watcher数据都已经同步完毕,在等待新的变更,后者表示该group的watcher数据同步落后于当前最新变更,还在追赶。
当 etcd 收到客户端的watch请求,如果请求携带了revision参数,则比较请求的revision和store当前的revision,如果大于当前revision,则放入synced组中,否则放入unsynced组。同时 etcd 会启动一个后台的goroutine持续同步unsynced的watcher,然后将其迁移到synced组。也就是这种机制下,etcd v3 支持从任意版本开始watch,没有v2的1000条历史event表限制的问题(当然这是指没有compact的情况下)
注意:watcherGroup一个是包含两种watcher,一个是WatchableStore包含两种watcherGroup,别混淆了,watcherGroup有两种一个是synced,unsynced,他就是对watcherGroup打标记分类罢了。
etcd v3 store 分为两部分,一部分是内存中的索引,kvindex,是基于Google开源的一个Golang的btree实现的,另外一部分是后端存储(落盘)。按照它的设计,backend可以对接多种存储,当前使用的boltdb。boltdb是一个单机的支持事务的kv存储,etcd 的事务是基于boltdb的事务实现的。etcd 在boltdb中存储的key是revision,value是 etcd 自己的key-value组合,也就是说 etcd 会在boltdb中把每个版本都保存下,从而实现了多版本机制。
revision主要由两部分组成,第一部分main rev,每次事务进行加一,第二部分sub rev,同一个事务中的每次操作加一。
etcd 提供了命令和设置选项来控制compact,同时支持put操作的参数来精确控制某个key的历史版本数。
内存kvindex保存的就是key和revision之前的映射关系,用来加速查询。
参考文档:
ETCD官网Data_Model建议看下Physical view
etcd数据存储及数据多版本控制实现逻辑

客户端要发起一个写的请求,假设这个请求发给了follower,像预检查,kv这块处理是一致的,最终这个请求会转给一致性模块,一致性模块会去判断自己是不是leader,如果是leader就直接处理,如果不是就转给leader。
上图请求流程:
(1)当客户端对etcd发起请求的时候,如果etcd不是leader的状态而是follower,follower则会将请求转发leader;
如果是leader后, 会对其进行预检查,检查(配额、限速、鉴权【判断请求是否合法】、包大小【需要小于1.5M,过大则会拒绝】)。(2)如果请求本身是合法的,会将请求转发给KVServer处理。
(3)KVserver一致性模块进行数据处理,一致性模块是基于raft协议实现的,这时候的数据本身是处于unstable状态。
(4)当leader该数据处理unstable状态后,会通过rpc通知其他follower也来同步该数据,并且leader本身会在数据同步到日志模块【wal日志, wal日志通过fsync落盘到磁盘中】。而其他follow在同步该数据的时候,本身完成的是步骤3和数据同步到日志模块,follower一致性模块数据变成commited状态,当完成了这些后通过上次rpc返回响应体给leader。
(5)leader在收到了超过半数集群本身确认后,更新MatchIndex,
一致性模块中数据本身由unstable变化成commited状态。这时候通过MVCC模块【treeIndex和BoltDB开源组件组成】进行状态机的写入,将数据同步到treeIndex【会更新modified版本[当前版本号],
generations信息[创建的版本,当前版本数,过往的所有版本号]】。再通过BoltDB落盘到磁盘中。这时候一致性模块数据由commited变化为applied状态。【在这里如果没有要求数据强一致性,弱一致性的话,那么数据在commited状态就认为数据已经同步完成了】。(6)再通过heatbeat将数据同步到follower中MVCC模块中。最终完成数据的一致性。如下图所示。
【如果follower比leader落后好几个版本,leader会通过headbeat带到follower进行同步】。
图中各个组件解析:
预检查
请求到了leader要做一些预检查:
第一个配额,就是etcd有数据大小的配置,你这次写请求还能不能放进去。
第二要限速,作为一个服务器要频繁的调取我的写操作会把我压垮的。
第三就是认证鉴权(你是谁,有没有读写权限)。
第四数据包检查,它会做一些限制,如果数据包超过1.5M,它就不让写了。如果数据包太大了,它反复确认,包括后面做索引,做查询,它的开销就非常的大,会导致etcd的性能直线下降。因为它需要多次确认,需要数据同步,它就会导致数据同步的效率非常低,所以它做了一些限制,让你不要无休止的去增加,把数据变的非常大,在k8s里面,很难将yaml写成1.5M以上的,yaml不是无限增长的,而是有一定限制的。
kvserver
这些预检查做完之后,请求会被发送到主模块叫做kvserver,kvserver接收到请求发现是一次写请求,比如是y=9,它会把请求通过propose方法发到一致性模块。
一致性模块 选主/日志复制
一致性模块实现两件事,它其实就是raft协议的实现者,第一,它会实现选主,第二就是日志复制,它会在etcd的内存里面构建raft log,这个raft log其实是一个数据结构,它会先将自己的信息y=9写到unstable里面,记录了我有一条数据要写入,接下来这个请求会被同时写入到本地的wal log,就是写y=9,将y=9的这条日志写到本地,这个写入最后是要落盘的,不能每次写入都落盘这样效率太低了,wal log落盘其实是由fsync,就相当于前端写在buffer里面,最后通过fsync周期性的将这些事件真正的落在磁盘上面。
写wal log的同时它有另外的goroutine去把同样的message,就是通过一个append message发到其他follower那里,follower那边收到请求之后,它要做同样的事情,就相当于要将写操作写入wal log。
并且写完以后回复response,回到leader这边,leader发现有半数确认了这条日志的写入,那么它就认为这条数据已经commit了,所以它会更新自己的数据结构,更新match index,且raftlog会将y=9写入commited,每条写入都有自增长的index,它会记录match index往前走了。
同时它这个请求会以apply的方式请求状态机来记录这次数据。状态机基于mvcc模块,就是多版本并发控制的这么一个模块,这里面包含了kv的index和blotdb,经过半数确认之后要往mvcc里面去写。apply完成之后y=9就进入了raftlog中的applied。
来看看在memory和boltdb里面怎么存储的,首先wal log,它有type,有index,有term,还有最终的数据,这些数据经过确认之后需要往mvcc里面去写,在treeindex也就是内存里面,它会记录当你写入数据的key,就是y,这次写入9,但是可能有很多次的变更,每次变更都有历史变更信息的,所以在tree index里面,它的key是写入数据的key,但是value并没有保存任何真实的值,而是保存了这个key所有关联的版本信息,这里就会有modfiy version,就是当前最后一次的变更信息,还有一个generation,就是你这个key有可能创建删除很多次的,它每次创建删除都是generation的一代。所以对于历史信息他就会存generation,每个generation里面都有哪些revision,revision又是全局唯一的,所以通过这样的一个索引就知道一个键值以前有过多少版本,当前的版本是什么,以前有多少代,每代有多少版本。
这样其实一个key的完整的生命周期,所有的变更都记录在这里面了。
落盘的地方即boltdb,它也是键值对,但是它的key是revision,value就是key-value的完整的信息。包括创建的revision,modify的revision。key是什么,version是什么,value是什么。
也就是整个数据变更的信息作为value存放在boltdb里面。
这些整个的信息会和版本信息对应。
通过这种方式实现了对任意的一个key完整生命周期的监听,不管更新了多少次,这边都存着一份所有版本的信息。
这也就是为什么,频繁变更的时候blot或者etcd里面的存储数据会变大,因为它所有的历史信息都保存下来了,它占用的空间就大了,所以需要定时compaction。

有个matchindex,在leader这一端,它会保存一个信息,第一个term大家都写了a和b,然后后面就变为第二个term,在第二个term里面又发生了很多的变更,wal log里面存了a b c d e f g,这里有8次变更,每次变更都会在etcd里面去维护一个自身的index(matchindex),代表leader和哪个index保持一致了。
这里有leader a,follower b c,以提交的日志是代表超过半数确认的日志,这三个人组成的集群超过半数确认的是到第7个index,也就是当前的match index是7,第8还没有被多数确认,它并不是一个真正确认过的index,如果重新选举的话,c的log本身比leader的commit index是要小的,如果c去作为新的leader就有可能丢数据了。
之所以记录index,就是用来确保说拥有最新数据的这些人,才可以去做新的leader,c是做不了新的leader的,所以通过index从leader这边记录一下,最新的commit log的index已经到哪里了,来确保新的leader永远包含所有已经确认的数据,通过这样的方式保证数据的一致性。
etcdctl member list --write-out=table
±-----------------±--------±--------±----------------------±----------------------±-----------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
±-----------------±--------±--------±----------------------±----------------------±-----------+
| 8e9e05c52164694d | started | default | http://localhost:2380 | http://localhost:2379 | false |
±-----------------±--------±--------±----------------------±----------------------±-----------+
etcdctl get x -w=json
{“header”:{“cluster_id”:14841639068965178418,“member_id”:10276657743932975437,“revision”:2,“raft_term”:2},“kvs”:[{“key”:“eA==”,“create_revision”:2,“mod_revision”:2,“version”:1,“value”:“MA==”}],“count”:1}
etcdctl get x --rev=2 #这个指的是revision版本,不是version版本
x
0
灾备命令:
etcdctl --endpoints https://127.0.0.1:3379 --cert /tmp/etcd-certs/certs/127.0.0.1.pem --key /tmp/etcd-certs/certs/127.0.0.1-key.pem --cacert /tmp/etcd-certs/certs/ca.pem snapshot save snapshot.db
etcdctl snapshot restore snapshot.db \
--name infra2 \
--data-dir=/tmp/etcd/infra2 \
--initial-cluster
infra0=http://127.0.0.1:3380,infra1=http://127.0.0.1:4380,infra2=http://127.0.0.1:5380 \
--initial-cluster-token etcd-cluster-1 \
--initial-advertise-peer-urls http://127.0.0.1:5380
单个对象不建议超过1.5M
默认容量2G
数据目录不建议超过8G
结论:boltdb的数据盘大小不应该大于8G,因为boltdb其实是会用到mmap内存映射的,数据盘的8G也会映射到内存中8G,太大我猜测应该是寻址代价太大了,而且实际etcd使用也不会使用8G,网易这边etcd一个50节点的k8s业务集群,一个数据目录才2G左右。
• 设置etcd存储大小
$ etcd --quota-backend-bytes=$((8*1024*1024))
• 写爆磁盘
$ while [ 1 ]; do dd if=/dev/urandom bs=1024 count=1024 | ETCDCTL_API=3 etcdctl put key || break; done
• 查看endpoint状态
$ ETCDCTL_API=3 etcdctl --write-out=table endpoint status
• 查看alarm
$ ETCDCTL_API=3 etcdctl alarm list
• 清理碎片
$ ETCDCTL_API=3 etcdctl defrag
• 清理alarm
$ ETCDCTL_API=3 etcdctl alarm disarm
# keep one hour of history,auto compact
$ etcd --auto-compaction-retention=1
# compact up to revision 3
$ etcdctl compact 3
$ etcdctl defrag
Finished defragmenting etcd member[127.0.0.1:2379]
etcd-operator: coreos开源的,基于kubernetes CRD完成etcd集群配置。
https://github.com/coreos/etcd-operator
Etcd statefulset Helm chart: Bitnami(powered by vmware)
https://bitnami.com/stack/etcd/helm
https://github.com/bitnami/charts/blob/master/bitnami/etcd

etcd是kubernetes的后端存储
对于每一个kubernetes Object,都有对应的storage.go 负责对象的存储操作
pkg/registry/core/pod/storage/storage.go
API server 启动脚本中指定etcd servers集群
# https:// localhost:4002
/usr/local/bin/kube-apiserver --etcd_servers=https://localhost:4001 --etcdcafile=/etc/ssl/kubernetes/ca.crt--storage-backend=etcd3 --etcd-serversoverrides=/events
这种拓扑将相同节点上的控制平面和etcd成员耦合在一起。优点在于建立起来非常容易,并且对副本的管理也更容易。但是,堆叠式存在耦合失败的风险。如果一个节点发生故障,则etcd成员和控制平面实例都会丢失,并且集群冗余也会受到损害。可以通过添加更多控制平面节点来减轻这种风险。因此为实现集群高可用应该至少运行三个堆叠的Master节点,在三个master节点都是物理机且都是io隔离,内存、cpu充足的情况下可以使用。

该拓扑将控制平面和etcd成员解耦。如果丢失一个Master节点,对etcd成员的影响较小,并且不会像堆叠式拓扑那样对集群冗余产生太大影响。但是,此拓扑所需的主机数量是堆叠式拓扑的两倍。具有此拓扑的群集至少需要三个主机用于控制平面节点,三个主机用于etcd集群,且网络开销会更大一点,当然这个影响几乎可以忽略不计。

答:如果是1个master,性能最高,数据安全性没法保证。
如果是3个master,数据有冗余,但是对运维要求很高,如果坏了一个master,集群仍可以正常工作,但是需要立即修复。
如果是5个master,性能最差,需要的机器也更多,但是对运维的压力要小。
答:不会,因为peer越多,数据写入时需要确认的人就越多
答:etcd的动态扩缩容是很麻烦的,因为需要基于配置,除非到每个member中都刷一下配置。
针对每一个object,apiserver和etcd之间的Connection -> stream 共享
HTTP/2的特性:
Stream quota(定义了一个TCP连接里面支持多少个并发的HTTP/2请求)
带来的问题?对于大规模集群,会造成链路阻塞,node无法上报状态信息,变成unknown状态,node上的pod被control manager的pod驱逐器驱逐,导致大规模的pod搬迁。
10000个 pod,一次 list 操作需要返回的数据可能超过100M


通过流量控制工具(Traffic Control)提高etcd成员之间发送数据的优先级来避免。对于磁盘延迟,典型的旋转磁盘写延迟约为10毫秒。对于SSD(Solid State Drives,固态硬盘),延迟通常低于1毫秒。HDD(Hard Disk Drive,硬盘驱动器)或者网盘在大量数据读写操作的情况下延时会不稳定。因此强烈建议使用SSD。
同时为了降低其他应用程序的I/O操作对etcd的干扰,建议将etcd的数据存放在单独的磁盘内。也可以将不同类型的对象存储在不同的若干个etcd集群中,比如将频繁变更的event对象从主etcd集群中分离出来,以保证主集群的高性能。在APIServer处这是可以通过参数配置的。这些etcd集群最好也分别能有一块单独的存储磁盘。
如果不可避免地,etcd和其他的业务共享存储磁盘,那么就需要通过下面ionice命令对etcd服务设置更高的磁盘I/O优先级,尽可能避免其他进程的影响。
$ ionice -c2 -n0 -p 'pgrep etcd'
etcd以日志的形式保存数据,无论是数据创建还是修改,它都将操作追加到日志文件,因此日志文件大小会随着数据修改次数而线性增长。
当Kubernetes集群规模较大时,其对etcd集群中的数据更改也会很频繁,集群日记文件会迅速增长。
为了有效降低日志文件大小,etcd会以固定周期创建快照保存系统的当前状态,并移除旧日志文件。另外当修改次数累积到一定的数量(默认是10000,通过参数“–snapshot-count”指定),etcd也会创建快照文件。
如果etcd的内存使用和磁盘使用过高,可以先分析是否数据写入频度过大导致快照频度过高,确认后 可通过调低快照触发的阈值来降低其对内存和磁盘的使用。
存储空间的配额用于控制etcd数据空间的大小。合理的存储配额可保证集群操作的可靠性。如果没有存储配额(可利用整块磁盘空间)或配额过大,etcd的性能会因为存储空间的持续增长而严重下降。如果设置的存储配额太小,一旦其中一个节点的后台数据库的存储空间超出了存储配额,etcd就会触发集群范围的告警,并将集群置于只接受读和删除请求的维护模式。只有在释放足够的空间、消除后端数据库的碎片和清除存储配额告警之后,集群才能恢复正常操作。
etcd会为每个键都保存了历史版本。为了避免出现性能问题或存储空间消耗完导致写不进去的问题,这些历史版本需要进行周期性地压缩。压缩历史版本就是丢弃该键给定版本之前的所有信息,节省出来的空间可以用于后续的写操作。etcd支持自动压缩历史版本。在启动参数中指定参数“--auto-compaction”,其值以小时为单位。也就是etcd会自动删除该值设置的时间窗口之前的历史版本。
压缩历史版本,相当于离散地抹去etcd存储空间某些数据,etcd存储空间中将会出现碎片。这些碎片无法被后台存储使用,却仍占据节点的存储空间。因此定期消除存储碎片,将释放碎片化的存储空间,重新调整整个存储空间。
当网络延迟和磁盘延迟固定的情况下,可以优化etcd运行参数来提升集群的工作效率。etcd基于Raft协议进行Leader选举,当Leader选定以后才能开始数据读写操作,因此频繁的Leader选举会导致数据读写性能显著降低。可以通过调整心跳周期(Heatbeat Interval)和选举超时时间(Election Timeout),来降低Leader选举的可能性。
心跳周期是控制Leader以何种频度向Follower发起心跳通知。心跳通知除表明Leader活跃状态之外,还带有待写入数据信息,Follower依据心跳信息进行数据写入,默认心跳周期是100ms。
选举超时时间定义了当Follower多久没有收到Leader心跳,则重新发起选举,该参数的默认设置是1000ms。如果etcd集群的不同实例部署在延迟较低的相同数据中心,通常使用默认配置即可。如果不同实例部署在多数据中心或者网络延迟较高的集群环境,则需要对心跳周期和选举超时时间进行调整。建议心跳周期参数推荐设置为接近etcd多个成员之间平均数据往返周期的最大值,一般是平均RTT的0.55-1.5倍。如果心跳周期设置得过低,etcd会发送很多不必要的心跳信息,从而增加CPU和网络的负担。
如果设置得过高,则会导致选举频繁超时。选举超时时间也需要根据etcd成员之间的平均RTT时间来设置。选举超时时间最少设置为etcd成员之间RTT时间的10倍,以便对网络波动。
心跳间隔和选举超时时间的值必须对同一个etcd集群的所有节点都生效,如果各个节点配置不同,就会导致集群成员之间协商结果不可预知而不稳定。
备份设置的考虑点:

etcd的默认工作目录下会生成两个子目录:wal和snap。wal是用于存放预写式日志,其最大的作用是记录整个数据变化的全部历程。所有数据的修改在提交前,都要先写入wal中。
snap是用于存放快照数据。为防止wal文件过多,etcd会定期(当wal中数据超过10000条记录时,由参数“–snapshot-count”设置)创建快照。当快照生成后,wal中数据就可以被删除了。
如果数据遭到破坏或错误修改需要回滚到之前某个状态时,需要做两步操作:一是从快照中恢复数据主体,但是未被拍入快照的数据会丢失;第二步执行所有WAL中记录的修改操作,从数据主体恢复到数据损坏之前的状态,恢复时间较长。


注意:红色的就是挂了或者网络不通!
下图是两个etcd实例挂了:

Case: 网络分区出现
Group#1: master-1, master-2
Group#2: master-3, master-4, master-5

无论是网络分区,还是部分etcd实例挂掉,apiserver都会 通过对etcd实例的七层测活发现不可用的etcd节点,然后发现不可用之后,apiserver会自杀,kubelet就会转而去连接其他apiserver实现对k8s集群的保护。 怎么保证etcd和apiserver是一对一连接的呢?虽然这种一对一的方式的确能提高很大程度上的读的并发,写肯定还是要到etcd master去做。